On this page
- What messages does MCP send between a client and a server?
- What does the initialize exchange look like?
- What does tools/list return?
- What does a tools/call request and result look like?
- What costs tokens in an MCP session?
- Does Claude Code put every tool description in context?
- How does Claude Code decide to call a tool?
- What should MCP server authors do with this?
- Which version of the MCP spec is current?
- How we checked
- FAQ
MCP sends JSON-RPC 2.0 messages: a request that identifies the client and protocol version, a tools/list call that returns every tool's name, description and JSON Schema, and a tools/call request that returns a content array of text. That is the whole conversation between Claude Code and an MCP server for the purposes of calling a tool. We captured it from repowise's own server over stdio, so every message below is real, trimmed only where marked.
What messages does MCP send between a client and a server?
Three groups matter for tools: a handshake or version declaration, discovery with tools/list, and invocation with tools/call. Everything is JSON-RPC 2.0, so each request has an id, a method and params, and each response echoes the id with a result or an error.
The model never speaks MCP. Claude Code translates the model's tool choice into a tools/call request and translates the result back into text the model reads. The protocol lives between the client and the server.
What does the initialize exchange look like?
repowise's server runs on the Python MCP SDK 1.26.0, which opens with initialize (the era before the current revision, covered below). The client sends its protocol version and identity, and the server answers with its capabilities, its name and an instructions string. Here is the real response from repowise mcp, with the instructions string shortened:
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{
"experimental":{},
"prompts":{"listChanged":false},
"resources":{"subscribe":false,"listChanged":false},
"tools":{"listChanged":false}},
"serverInfo":{"name":"repowise","version":"1.26.0"},
"instructions":"repowise indexes this repository's code graph, git history and decisions. One call here often replaces several text searches and file reads.\n- Use search_codebase(query) to find an identifier or literal: ..."}}
Two fields deserve attention. capabilities.tools tells the client the server has tools, and listChanged: false says the list will not change mid-session. The instructions string is the only free-text field the server gets before any tool is listed, and the repowise server uses it to say when each tool applies. The client then sends a notifications/initialized message, which has no id and expects no reply.
What does tools/list return?
It returns one object per tool with a name, a description and an inputSchema, and the Python SDK adds an outputSchema. This is the real get_symbol entry, trimmed to a few parameters (the full one lists seven):
{"name":"get_symbol",
"description":"Follow-up read of one symbol whose id another response already gave you.\n\n**Not an entry point.** ... Returns verified, line-numbered source for one indexed symbol, live range, or omission ref. ...\n\nArgs:\n symbol_id: \"path/to/file.py::Name\", ...",
"inputSchema":{"type":"object","title":"get_symbolArguments",
"properties":{
"symbol_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Symbol Id"},
"depth":{"default":1,"title":"Depth","type":"integer"}}},
"outputSchema":{"type":"object","required":["result"],"title":"get_symbolOutput",
"properties":{"result":{"type":"object","additionalProperties":true,"title":"Result"}}}}
The shape follows the tools page of the spec: name identifies the tool, description is human-readable text the model reads, and inputSchema is JSON Schema for the arguments. The server reads the Python function: its docstring becomes the description, and its type-annotated signature becomes the schema. packages/server/src/repowise/server/mcp_server/tool_symbol.py registers it, and _server.py holds the FastMCP instance that publishes it.
The capture ran in workspace mode, so the list held the ten flagship tools plus list_repos (a single-repo server exposes the flagship tools only). The whole tools/list response was 25,865 characters on the wire.
What does a tools/call request and result look like?
The request names the tool and passes arguments that match its schema. The result carries the answer as a content array of typed blocks, and a flag for errors. This is a real call against the repowise repository:
{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"search_codebase",
"arguments":{"query":"tool_middleware","limit":2}}}
{"jsonrpc":"2.0","id":3,"result":{
"content":[{"type":"text","text":"{\"results\":[{\"type\":\"symbol\",\"symbol_id\":\"packages/server/src/repowise/server/mcp_server/__init__.py::tool_middleware\",\"name\":\"tool_middleware\",\"kind\":\"function\",\"start_line\":89,\"end_line\":219,\"signature\":\"def tool_middleware(fn: Any) -> Any\", ...}],\"mode\":\"symbol\",\"lines\":[...],\"complete\":true, ...}"}],
"structuredContent":{"result":{"results":["..."],"mode":"symbol","lines":["..."]}},
"isError":false}}
The decoded text holds a results array, a lines array of exact matches (the first is {"path":"packages/server/src/repowise/server/mcp_server/__init__.py","line":89,"kind":"definition","text":"def tool_middleware(fn: Any) -> Any:"}) and a _meta block recording the index commit. The spec says a tool that returns structuredContent should also return the serialized JSON as a text block, so the payload appears twice on the wire: once as a string in content, once as an object in structuredContent. Clients that read only text get the same data as clients that read the object.
An unknown tool name comes back from this server as a tool result with isError:true:
{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"Unknown tool: nope"}],"isError":true}}
The 2026-07-28 spec classes an unknown tool as a protocol error, a JSON-RPC error with code -32602, so repowise's isError result is its own choice and not the spec's example. For input and business errors a model can fix, the spec does put them in the result with isError: true, so the model sees the message and can retry.
What costs tokens in an MCP session?
Two things: the tool definitions the model reads before it chooses, and the text of each result it reads after. The JSON-RPC envelope (jsonrpc, id, method) never reaches the model, so it costs nothing in context.
We measured both on the captured traffic. Token counts use tiktoken cl100k_base on compact JSON.
| Payload | Characters | Tokens (cl100k) |
|---|---|---|
initialize instructions string | 1,218 | 279 |
| Tool names | n/a | 30 |
| Tool descriptions | n/a | 3,497 |
| Input schemas | n/a | 1,966 |
| Output schemas | n/a | 371 |
Full tools/list result | 25,865 (whole message) | 6,283 |
search_codebase result text (limit 2) | 9,883 | 2,524 |
get_context result text (one file) | 3,310 | 823 |
Scroll the table sideways to see every column.
Descriptions are the largest part of a tool list, at 3,497 of the 6,283 tokens. Result sizes vary with the question: the search above returned 50 matching lines, which is most of its 9,883 characters, and a file triage card from get_context was a third of that.
Does Claude Code put every tool description in context?
By default it does not. Per the Claude Code MCP documentation, tool search defers tool definitions until Claude needs them: "Only tool names and server instructions load at session start." Claude Code then loads a definition when the model searches for a tool.
That changes what each part of your server is for. The 30 tokens of names and the 279 tokens of instructions are what the model has when it decides whether your server is relevant. The 3,497 tokens of descriptions only enter context after the model has searched and found the tool. The docs add size limits: server instructions and the description of an upfront tool are cut at 4,096 characters, and a description loaded through tool search at 16,384 characters. Settings such as ENABLE_TOOL_SEARCH=false or alwaysLoad for a server move tools back to loading upfront, and then the whole 6,283 tokens in our capture arrive in the first request.
How does Claude Code decide to call a tool?
The model decides, from the text it can see. Claude Code gives the model the tools (or the names and a search tool), the model emits a tool-use block with a tool name and arguments, and Claude Code turns that into tools/call. The spec describes tools as "model-controlled" and adds that clients should keep a human able to deny invocations, which is the permission prompt you see in Claude Code.
So the inputs to the decision are your instructions string, your tool names, and the opening of each description. We measured how much that matters in why your MCP server is probably not being called: the same servers were called on every question in one harness and on none in another, and the cause was tool discovery, not schema size.
Results work the same way in reverse. Claude Code warns when a tool's output exceeds 10,000 tokens and limits it to 25,000 by default, and text results longer than 50,000 characters are saved to a file unless the tool raises its own limit through _meta["anthropic/maxResultSizeChars"]. Our 2,524-token search result sits well inside those bounds.
What should MCP server authors do with this?
Spend your words where the model decides, and keep each result readable in one pass. Concretely:
- Write the
instructionsstring as a router. It loads at session start even when everything else is deferred. repowise's is 279 tokens and names each tool with the situation that calls for it. - Put the trigger first in every description. The
get_symboldescription opens with "Follow-up read of one symbol whose id another response already gave you" and says in the next line that it is not an entry point. Claude Code's docs say tool search reads only the shorter limit, so critical words belong early. In our capture the descriptions are 3,497 of the 6,283tools/listtokens, more than half, which is why their first lines matter most. - Keep
inputSchemahonest and small. Our input schemas total 1,966 tokens. Descriptions on the parameters that need them, defaults on the rest. - Return the answer, then the pointer. Our results carry a
completeflag and an omission ref (repowise#...) so the model can ask for more when it needs it, instead of receiving everything up front. See the MCP setup guide for wiring a server into each client. - Return actionable errors with
isError: true, and order tools deterministically. A message the model can act on beats a protocol error it cannot fix, and the spec recommends a stable order so clients can cache the list and prompt caches hit.
Two Claude Code controls are worth knowing: a tool can set _meta["anthropic/alwaysLoad"] to load upfront, and _meta["anthropic/maxResultSizeChars"] raises its result limit up to a ceiling of 500,000 characters.
For the protocol's background, what is MCP covers the concepts, and the MCP tools overview lists what repowise exposes.
Which version of the MCP spec is current?
As of 15 October 2026 the current specification is revision 2026-07-28, published at modelcontextprotocol.io. It drops the initialize handshake: every request carries protocolVersion, clientInfo and clientCapabilities in its _meta field, and servers implement server/discover. Revisions up to 2025-11-25 open with initialize, which is what the Claude Code MCP documentation means when it says its v2 runtime adds revision 2026-07-28, and the versioning page defines how clients fall back so both eras interoperate. The tools/list and tools/call messages are the same shape in both, apart from that _meta block and a resultType field the new revision adds to results.
How we checked
We ran repowise mcp . over stdio on the repowise repository at commit 215776c2e, sent initialize, notifications/initialized, tools/list and three tools/call requests from a small script, and saved every line in both directions. All JSON above is from that log, trimmed with ... where marked. We counted characters on the raw message lines and tokens with tiktoken (cl100k_base) on compact JSON, so the token figures are a consistent proxy for relative sizes, not Claude's own tokenizer or billing counts. The token tables report sizes we measured, so they are literal numbers and not product tokens.
Spec facts come from the MCP specification (revision 2026-07-28, read 15 October 2026), its versioning and tools pages. Claude Code behaviour (tool search, 4,096 and 16,384 character limits, 10,000 and 25,000 token output thresholds) comes from the Claude Code MCP documentation on the same date. The server code is under packages/server/src/repowise/server/mcp_server.
FAQ
- What does MCP send to Claude Code?
- A JSON-RPC 2.0 message stream. The client declares its protocol version (through
initializeon older revisions, or_metaon each request on revision 2026-07-28), reads tool definitions withtools/list, and runs a tool withtools/call. The server answers each request with a result or an error. - What does a tools/call request look like?
- It is a JSON-RPC request with
method: "tools/call"andparamscontaining the toolnameand anargumentsobject that matches the tool'sinputSchema. The result holds acontentarray of typed blocks, usuallytext, and anisErrorboolean. - Does MCP use HTTP or stdio?
- Both are standard transports. The messages are the same JSON-RPC either way. Our capture ran over stdio, where each message is one line on stdin or stdout. Streamable HTTP carries the same messages in HTTP requests, and the 2026-07-28 revision adds headers such as
MCP-Protocol-Version. - How many tokens do MCP tool definitions use?
- It depends on the server. Our server, with the ten flagship tools plus
list_repos, measured 6,283 tokens for the fulltools/listresult withcl100k_base. With Claude Code's default tool search, only names and server instructions load at session start, so that cost arrives when the model searches for a tool. - What is the difference between content and structuredContent?
contentholds unstructured blocks such as text, andstructuredContentholds the same data as a JSON value that can match a declaredoutputSchema. The spec asks tools that return structured content to also serialize it into a text block for older clients, which is why our results carry the payload twice.- Is a tool error a JSON-RPC error?
- Not usually. The spec reserves JSON-RPC errors for malformed requests and server faults. A failure the model can correct, such as a bad argument, goes in a normal result with
isError: true, so the model reads the message and retries.



