Tool descriptions, resource lists and error messages all end up in front of the model. When Claude picks the wrong tool, the fix is usually in text the server wrote.
An MCP server looks like an API, and it is tempting to write it like one: terse names, a one-line description, an error code when something breaks. But the reader on the other end is not a developer with the docs open. It is a model deciding, one turn at a time, which tool to call, what to pass it, and what to do when it fails. Everything it knows about your server is text you wrote.
Three parts of that text do most of the work: whether your content is exposed as a tool or a resource, how each tool describes itself, and what an error tells the model to do next. A fourth, checking that Claude Code can see the server at all, takes two minutes and saves an hour of rewriting descriptions for a server that never connected.
The MCP specification gives the two primitives different owners. Tools are model-controlled: the model discovers and invokes them “based on its contextual understanding and the user’s prompts.” Resources are application-driven, with the host application “determining how to incorporate context based on their needs”, and each one is identified by a URI.
| ToolModel-controlled | ResourceApplication-driven | |
|---|---|---|
| Who reaches for it | The model, mid-turn, when it judges a call is needed | The host application, or you picking it |
| Identified by | A name, such as orders_search |
A URI, such as db://schema/orders |
| Discovered with | tools/list |
resources/list and resources/templates/list |
| Used with | tools/call, with arguments |
resources/read, with a URI |
| Fits | Queries with parameters, actions, anything with side effects | Reference content: schemas, documentation, catalogues, files |
| In Claude Code | Claude calls it when it decides to | You reference it as @server:resource in a prompt |
Almost anything can be a tool, so that is the wrong question. The useful one is whether the model needs to decide to fetch this, with arguments, at a moment only it can judge. If not, and the content is something Claude would otherwise go looking for, a resource is the better shape.
A database schema is the standard example. Exposed only through tools, every
session starts the same way: Claude calls list_tables, then
describe_table on three or four candidates, before it writes a
single query. Each of those calls is a round trip spent rediscovering
something that changes once a month. Exposed as a resource template,
db://schema/{table}, the schema can be put in front of the model
directly, and the exploration never happens. Stable reference content as
resources is how you cut exploratory tool calls.
Resource templates use URI templates, so one entry covers every table.
Resources can also carry annotations the host may use when deciding what to
include: an audience of user,
assistant or both, a priority from 0 to 1, and a
lastModified time.
The line runs the other way too. A “read” that changes state, such as marking a notification seen, is a tool whatever it is called.
Anthropic’s own guidance on tool definitions does not hedge:
Provide extremely detailed descriptions. This is by far the most important factor in tool performance.
Claude docs, Define tools
A description should say what the tool does, when to use it and when not to, what each parameter means, and what the tool does not return. The same guidance suggests at least three or four sentences for each tool. The failure it prevents is easiest to see with two tools that sound alike.
{
"name": "search",
"description": "Search orders.",
"inputSchema": { "type": "object",
"properties": { "query": { "type": "string" } } }
}
{
"name": "get_order",
"description": "Get an order.",
"inputSchema": { "type": "object",
"properties": { "id": { "type": "string" } } }
}
{
"name": "orders_search",
"description": "Finds orders by customer email, date or status and
returns up to 20 summaries: order ID, date, status and total.
Use it when you do not have an order ID yet. It does not return
line items or shipping details; for those, call orders_get with
an ID from these results. Dates are ISO 8601, e.g. 2026-09-01;
relative dates such as 'last week' are rejected.",
"inputSchema": {
"type": "object",
"properties": {
"customer_email": { "type": "string",
"description": "Exact address, e.g. ana@example.com" },
"placed_after": { "type": "string", "format": "date",
"description": "Earliest order date, inclusive, ISO 8601" },
"status": { "type": "string",
"enum": ["pending", "shipped", "delivered", "refunded"] }
}
}
}
{
"name": "orders_get",
"description": "Returns one order in full: line items, shipping
address, payment status and refund history. Use it only when you
already have an order ID, usually from orders_search. It does not
search: an unknown ID returns a not-found error, not similar orders.",
"inputSchema": {
"type": "object",
"properties": {
"order_id": { "type": "string",
"description": "Order ID in the form ORD-123456" }
},
"required": ["order_id"]
}
}
orders_search and orders_get, not search. Anthropic’s tool-design guidance recommends namespacing by service and by resource, which matters more as a session accumulates servers.
And names its sibling. Semantically similar tools are where misrouting happens, and the description is the only place to draw the boundary.
order_id, not id; customer_email, not query. The same guidance gives user_id over user as its example.
Anywhere the model would otherwise guess: the date format, with an example value, and an enum where the set of values is closed.
What each tool does not return. A model that knows orders_search has no line items will not try to read them from its results.
One difference from the Claude API is worth knowing. A Claude API tool
definition accepts an input_examples field for format-sensitive
inputs. The MCP tool definition has name, title,
description, inputSchema,
outputSchema and annotations, and no field for
examples, so on an MCP server the example values belong in the descriptions.
And when two tools stay confused however well they are described, the fix may be one tool. The same guidance recommends consolidating related operations, because “fewer, more capable tools reduce selection ambiguity.”
The specification defines two ways for a tool call to fail, and they are for different things.
A JSON-RPC error: unknown tools, invalid arguments, server errors.
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Unknown tool: invalid_tool_name"
}
}
A normal result with isError: true: API failures, invalid input data, business logic errors.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [ { "type": "text",
"text": "Failed to fetch weather data:
API rate limit exceeded" } ],
"isError": true
}
}
The second channel is the one you write for the model. It arrives as a tool result with the error flag set, so whatever you put in its content is what the model reasons over when it decides what to do next. Claude’s documentation puts the standard plainly: instead of a generic “failed”, say what went wrong and what to try next, as in “Rate limit exceeded. Retry after 60 seconds.”
In practice that means every execution error answers three questions: what kind of failure this is, whether trying again can help, and what the model should do instead. The field names below are a convention, not part of the specification; what matters is that the answers are there, in words the model can act on.
placed_after must be an ISO 8601 date. Received “last week”.2026-09-07.
Compare those with a stack trace or Error 403. Both are
accurate, and neither tells the model whether to wait, fix its input, try a
different tool or stop. A model left to guess tends to retry, which is
exactly the wrong move for two of the three cases above.
A search that legitimately finds nothing succeeded. Return it as a normal
result, and make it useful: “No orders for ana@example.com since
2026-09-01. Orders older than 90 days are archived; search with an earlier
placed_after to include them.” Flagging an empty result as
an error invites the model to treat a correct answer as a fault to work
around.
Before rewriting a description because Claude ignores a tool, confirm Claude Code has the tool at all. Where the server is configured, and how its credentials stay out of the repository, is covered in Every Claude Code setting has an address.
claude mcp list
Every configured server with a health status, such as ✔ Connected, ! Needs authentication or ✘ Failed to connect.
claude mcp get <name>
The details for one server.
/mcp
Inside a session: the tool count for each connected server, a flag on servers that advertise tools but expose none, and where you complete an OAuth sign-in.
MCP_TIMEOUT
Server startup timeout in milliseconds, for servers that are slow to start. MCP_TIMEOUT=10000 claude allows ten seconds.
MAX_MCP_OUTPUT_TOKENS
Claude Code warns when a tool’s output passes 10,000 tokens and caps it at 25,000 by default. Raising the cap is the fallback; returning only the fields the model needs is the fix.
Decide the fix before opening each one.
list_tables and then describe_table on three or four candidates. The schema changes about once a month.db://schema/{table}Stable reference content the model keeps going looking for. A resource template covers every table, and the exploratory calls disappear.
customers_find when it needed customers_search. Each has a one-line description.What each does, when to use it, when not to, and the name of the other. If they are still confused afterwards, consider merging them into one tool.
"ISO 8601, e.g. 2026-09-01"In the parameter’s description, and in the tool description if it matters. The MCP tool definition has no examples field, so the description is where they go.
"isError": trueName the failure as transient, give the wait, and say what to do if the retry fails too.
"isError": trueSay which permission is missing, and that the next step is to tell the user or escalate. Retrying cannot change the answer.
"isError": falseSay nothing matched and how to widen the search. The call succeeded.
tools/call for a tool name it has never had.JSON-RPC errorUnknown tools and invalid arguments are protocol errors in the specification, not isError results.
claude mcp list/mcp“Needs authentication” means sign in through /mcp. If it is connected, check the tool count there before touching a single description.
Sources: the MCP specification on tools and resources (2025-06-18); Claude’s documentation on defining tools and handling tool calls; Anthropic Engineering, Writing tools for agents; the Claude Code MCP documentation. Checked September 2026.
For the practice half, ExamGauge has 1523 original practice items across four Claude certification exams, scored on the real 100–1000 scale against the 720 cut. The diagnostic is free and needs no card.