ExamGauge

Your MCP server is a prompt

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.

Tools act, resources are read

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.

Tool or resource
   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.

A description is the tool’s only documentation

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.

Before two tools, one line each
{
  "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" } } }
}
After the same two tools
{
  "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"]
  }
}
Names carry the service

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.

Each says when not to use it

And names its sibling. Semantically similar tools are where misrouting happens, and the description is the only place to draw the boundary.

Parameters are unambiguous

order_id, not id; customer_email, not query. The same guidance gives user_id over user as its example.

Formats are stated

Anywhere the model would otherwise guess: the date format, with an example value, and an enum where the set of values is closed.

Limits are admitted

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.”

An error is an instruction

The specification defines two ways for a tool call to fail, and they are for different things.

Protocol error

A JSON-RPC error: unknown tools, invalid arguments, server errors.

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32602,
    "message": "Unknown tool: invalid_tool_name"
  }
}

Tool execution error

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.

The upstream API is rate-limited

transientRetryable
Message
The orders API allows 30 requests a minute and this server has used them.
Next step
Wait 60 seconds and retry once. If it fails again, tell the user the order system is busy.

The input was the wrong shape

invalid inputRetryable once fixed
Message
placed_after must be an ISO 8601 date. Received “last week”.
Next step
Convert the date and call again, for example 2026-09-07.

The account is not allowed

permissionNot retryable
Message
The connected account cannot view refunds for orders outside its region.
Next step
Do not retry. Tell the user the refund exists and that a regional administrator can see it.

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.

Not every disappointment is an error

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.

Check the server is actually there

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.

Try it

Decide the fix before opening each one.

Before every query, Claude calls list_tables and then describe_table on three or four candidates. The schema changes about once a month.
Expose the schema as resourcesdb://schema/{table}

Stable reference content the model keeps going looking for. A resource template covers every table, and the exploratory calls disappear.

Claude keeps calling customers_find when it needed customers_search. Each has a one-line description.
Rewrite both descriptions

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.

A date parameter receives “last Tuesday” about a third of the time.
State the format, with an example"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.

The upstream API rate-limits the server, and Claude immediately retries the call three times.
Execution error that says when to retry"isError": true

Name the failure as transient, give the wait, and say what to do if the retry fails too.

A user asks about a refund the connected account is not permitted to see.
Execution error marked not retryable"isError": true

Say which permission is missing, and that the next step is to tell the user or escalate. Retrying cannot change the answer.

A search finds no matching orders, correctly.
A normal result, not an error"isError": false

Say nothing matched and how to widen the search. The call succeeded.

The server receives a tools/call for a tool name it has never had.
Protocol errorJSON-RPC error

Unknown tools and invalid arguments are protocol errors in the specification, not isError results.

You added a server, but Claude never uses any of its tools.
Check before rewritingclaude mcp list/mcp

“Needs authentication” means sign in through /mcp. If it is connected, check the tool count there before touching a single description.

The whole method

  1. If the model has to decide to fetch it, with arguments, or it changes something, it is a tool. If it is reference content the model would otherwise go looking for, make it a resource.
  2. Write each description for a reader who has only the description: what, when, when not, the formats, and the tool it is easily confused with.
  3. Make every execution error say what kind it is, whether retrying helps, and what to do instead.
  4. Before blaming the text, check the server connected.

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.

More on this