Blog / Optimise your MCP tools

How to optimise your MCP tools by knowing how agents call them

Numbers are tokens, the unit AI models read and bill by (roughly ¾ of a word). Sources and method are in the technical details.

16 October 2026 · Leonid Bugaev · measured on Claude Code, Codex, Grok Build and OpenCode

We pointed four coding agents at a real, 99-tool production MCP server and asked them ordinary read-only questions, the kind its users ask every day. Each agent ran its default settings with its owner's usual model, and we ran 40 to 80 jobs per agent on 16 October 2026. The server returned correct data every time. The agents still struggled:

What this costs you

None of this was the model being dim. It came from how each agent loads, searches and caps tools, combined with how the server was written. Once you know how the agents call your tools, most of it is cheap to fix. The rest of this post covers how each agent does it, then a checklist of fixes ranked by impact, then the reference details.

Get your MCP server tested across Claude Code, Codex, Grok Build and OpenCode. We run the jobs your users care about through each agent and show you what breaks, with numbers.

Talk to us

Why it happens: how each agent calls your tools

An MCP server is a menu of actions ("tools") that an AI agent can use, such as "list invoices" or "send invoice". When an agent connects, your server sends the whole menu: every tool's name, a description, and the list of inputs it takes. For the 99-tool server that menu was 68,286 tokens. (A token is the unit models read and bill by, roughly three-quarters of a word.)

That's too much to show the model on every turn, so each agent does something different with it:

So in three of the four agents the model has to find your tool before it can use it, and it finds it by matching words. If the user says "invoice" and your tool is called ledger_dispatch, nothing matches. The diagram shows the steps, and how much of the model's attention (in tokens) each step takes up.

What each agent sends to the model before any search First request: what the model gets about 99 MCP tools OpenCode 1.18.3499 full definitions · 63,245 tok Claude Code 2.1.301names only · ~1.3k tok for 113 names Grok Build 1.0.60server names + counts · 189 tok reminder Codex 0.161.0nothing ("ALL_TOOLS" exists inside exec) OpenCode 2.0.3116 of 99 tools, short descriptions · 881 tok o200k tokens, captured 16 Oct 2026. Bar length is to scale.
Step through it. How each agent discovers and calls one MCP tool from the 99-tool server, and how many tokens of MCP tool information the model is holding at each step. The bar is to scale (full width = 70,000 o200k tokens); captured 16 Oct 2026. The tool's own result isn't counted, because its size depends on your server.

Claude Code 2.1.301

  1. Start: the model gets 113 tool names in a reminder (~1,258 tokens). No schemas.
  2. Model calls ToolSearch with select: and three exact names. Three full definitions (~573 each) are added as real tools.
  3. Model calls the tool directly. Results over 25,000 tokens are saved to a file instead.

The fixes, ranked by impact

We ranked these by what they would have saved on the 99-tool server. The "before" figures are measured. The "after" shapes are examples of the fix, not measurements.

  1. Keep default results small and paged

    Why first: it caused the most failures we saw (76 refusals, 32 cut-offs, 261 recovery reads), and the smallest cap, Grok Build's 20 KB, is only about 5,000 tokens.

    Before: a list call with no arguments returns every record with every field: 46,253 tokens.

    list_items {}  →  { items: [ …all records, all fields… ], nextOffset: null }

    After: a compact first page, a total, a cursor, and filters so the agent doesn't have to page through everything.

    list_items { query?: "acme", status?: "open", limit: 20, fields?: ["id","name","status"] }
    → { summary: "47 items, 20 shown", items: [ {id,name,status} … ], next: "c_20" }
  2. Send the data once

    Before: text content = a JSON copy of the structured content (25,806 of the 46,253 tokens in the biggest result).

    After: the data lives in structured content, and the text is one line: "47 items, 20 shown; next page: c_20".

  3. Put the user's words in tool names and the first line of descriptions

    Claude Code picks tools from names alone, Codex greps names and descriptions with a regex the model writes, and Grok Build ranks by shared keywords. None of them understand synonyms. Codex missing the tools in 8 of 80 runs is what it looks like when the words don't line up.

    Before: ledger_dispatch: "Dispatches the document to the configured recipient."
    After: invoice_send: "Email an invoice to a customer (also called a bill)."

  4. Make validation errors short and fixable

    Before: one bad id returned the whole input specification (12,689 tokens) with the state human_input and "Do not repeat the change automatically."

    After:

    { "error": "groupId must be an id from groups_list", "fix": "call groups_list and pass one of its ids", "retryable": true }

    Save "ask the user" for real decisions, such as spending money.

  5. Use one vocabulary for states and counts

    Before: a group summary showed "To review 20", which really meant 19 qualified + 1 needing review. Agents reported 20 as "needs review" or said the split didn't exist.

    After: return the counts users ask about, named the way the records name them: { "qualified": 19, "needs_review": 1 }.

  6. Shrink the tool list itself

    This matters most for agents that show the whole menu every time (OpenCode 1.x, or Claude Code behind a company proxy): 63,245 tokens on every request. Merge near-duplicate tools, move long grammars out of parameter schemas, describe a format once rather than repeating the same regex in 40 places, and write server instructions, which Claude Code loads even when it defers tools.

  7. Keep your own noise out of results

    Before: the activity feed returned 200 events, 152 of them records of the agent's own tool calls (98,512 tokens).
    After: leave tool-call records out by default and put them behind a filter.

  8. Label risky tools honestly

    MCP lets each tool carry hints such as "this changes things" (destructiveHint) or "safe to repeat" (idempotentHint). Agents use them to decide when to ask the user first.

    Before: two tools that spend money were marked destructiveHint: false, and a tool that starts paid work was marked idempotentHint: true.
    After: destructiveHint: true, idempotentHint: false on anything that spends money or starts work, so agents ask first and don't retry it.

When a result is too big, agents cut it

Every agent limits how much of one tool result the model may see. The limits are all different, and what happens past them varies too:

In practice, this is where the extra work comes from. The server's biggest default result was 46,253 tokens. Claude Code refused it 76 times in 80 runs, OpenCode cut off 32 results in 40 runs, and each agent spent over 100 extra steps reading files to recover. Every one of those steps costs a model call and adds a chance of a wrong answer. If your default results stay under about 5,000 tokens, none of this happens in any of the four agents. The exact limits, settings and messages are in the technical details below.

How we measured

Follow-up checks (same evening): to settle two open questions, we watched the live traffic of 3 Codex runs and 2 OpenCode 2 runs, one run per question. These are small samples, meant to show what happens, not how often.

Limits: one server, one day, and one model per agent (each agent ran its owner's usual model), so the success rates don't rank the agents. These apps change weekly. Full method and the source of every number are in the technical details.

Technical details: how each agent works inside, exact limits, settings and sources of every number
How to read the numbers. Unless a number says otherwise, it's a count of o200k_base tokens (the open-source tiktoken library), taken from the recorded request or result as compact JSON. "Own usage" means the agent's or model provider's own reported input tokens, which use that model's tokenizer, so they don't compare exactly with o200k figures. Every number carries the agent version and the date. The server is anonymised; we call it "the 99-tool server". The wire captures used a local stub that served the server's exact recorded tool list, so the tool definitions are real while the data behind them wasn't. The result-size and failure numbers come from live read-only runs against the real server.

Side by side

One task: "fetch the activity log for the newest records from the last 7 days", with the same 99 tool definitions. All three captured agents picked the right tool.

Agent (version, 16 Oct 2026)In context at startAfter a searchOn a simple tool callRun total (own usage)
Claude Code 2.1.301113 tool names (~1.3k o200k); 0 MCP schemas; first body 17,564full schemas of selected tools only (3 here; ~573 each), sent as real toolsdirect call; result capped at 25k tokens, larger saved to a file76,334 input (3 requests)
Codex 0.161.0no MCP names or schemas; first body 15,383 (exec description 6,471)whatever the model's JS prints: 56,619 for a broad regex (truncated to 11,244), 1.8–3k for narrow onescalled from JS inside exec137,344 input (6 requests)
Grok Build 1.0.60server names + tool counts (189); 0 schemas; first body 19,674full schemas of every hit as text: 4,957 for limit 10; 14,762 over four searchesthrough use_tool157,524 (5 requests)
OpenCode 1.18.34all 99 schemas: 63,245; body 82,603no searchdirect call; definitions re-sent each requestnot captured on this task; 341,615 avg per question in live runs
OpenCode 2.0.31partial catalog: 16 of 99 tools, one-line descriptions, 881; first body ~5,270 (live, interactive app)search() inside execute: 2,879 and 5,059 (live)called from JS inside executenot run on this task; live: 12,451 peak request (9 requests) and 64,918 peak (10 requests)

Each agent in detail

Claude Code: names up front, schemas on request

What we saw (Claude Code 2.1.301, claude-opus-5-5, 16 Oct 2026). The first request to the model carried 13 tools in a 17,564-token body. Those were the core tools, a tool called ToolSearch, and a stub placeholder marked defer_loading. None of the 99 MCP tools were in it. Instead, the first message had a reminder saying, roughly, "the following deferred tools are now available via ToolSearch; their schemas are NOT loaded", followed by a plain list of 113 names (99 MCP tools plus 14 built-ins). That list is about 1,258 tokens.

The model then called ToolSearch with a select: query naming three tools exactly. It picked them from the names alone. No keyword search ran. The result came back as tool_reference blocks. The next request carried those three definitions in full (16 tools in total), and the model called the right one. One full definition from this server was 573 tokens.

The model requests in that run were 23,110, 26,398 and 26,826 input tokens (own usage). Most of that was served from the prompt cache.

What the docs say. Anthropic's MCP docs describe the same design: "Only tool names and server instructions load at session start." Tool search is on by default, and the ENABLE_TOOL_SEARCH variable controls it:

  • unset: defer all MCP tools (the default);
  • false: load everything up front;
  • auto / auto:N: load up front while the definitions are under 10% (or N%) of the context window, and defer them all once they reach it.

What turns it off. We measured the extra first-request input (own usage, "say hi" prompt, 16 Oct 2026) with the 99-tool server attached compared to without it:

Claude Code 2.1.301 setupExtra input from 99 tools
Default (tool search on)+1,715
ENABLE_TOOL_SEARCH=false+107,541
Custom ANTHROPIC_BASE_URL (a local proxy)+107,542 (all 99 schemas on the wire)
Proxy plus ENABLE_TOOL_SEARCH=trueback to default (22,446 total)
--tools ""110,998 total: preloaded

The proxy case is documented: Claude Code turns tool search off when ANTHROPIC_BASE_URL points somewhere other than Anthropic, "since most proxies don't forward tool_reference blocks". The --tools "" case caught us out. That flag removes the built-in tools, ToolSearch included, so everything gets preloaded, and setting ENABLE_TOOL_SEARCH=true didn't bring it back. If you test Claude Code with a stripped tool set or through a gateway, you're probably testing a different product from what your users run.

The result limit. Separately from search, Claude Code caps the size of a single MCP result. The docs give a warning above 10,000 tokens and a default limit of 25,000 tokens (MAX_MCP_OUTPUT_TOKENS). A text result over 50,000 characters is saved to a file in any case. Over the limit, Claude Code saves the result under ~/.claude/projects/…/tool-results and gives the model a message naming the file (details and exact message below). A server can raise the limit for one tool with _meta["anthropic/maxResultSizeChars"].

The 99-tool server's main list tool returns 46,253 tokens with default arguments. In our live runs (Claude Code 2.1.301, 16 Oct 2026), the model got "Error: result (218,237 characters) exceeds maximum allowed tokens" instead of the data. That happened 40 times in 23 of 40 runs in one round and 36 times in 22 of 40 in a repeat. To recover, the model made 138 extra file reads and searches across the first round. It usually got there, but slowly.

Codex: tools live inside a code tool

What we saw (Codex CLI 0.161.0, gpt-6-astra, ChatGPT sign-in, 16 Oct 2026). Codex's code mode is on by default. The first request was 15,383 tokens. It held only a few namespaces, mainly exec, whose description alone is 6,471 tokens. There was no tool_search tool, and the server's name appeared nowhere in the request. The exec description tells the model that some nested tools may be omitted, and that it should filter ALL_TOOLS by name and description.

So the model wrote JavaScript. Its first attempt was a broad regex over every tool: ALL_TOOLS.filter(x => /servername/i.test(…)). That printed 56,619 tokens of tool metadata, which Codex cut down to 11,244 before the model saw it. The next two filters were narrower (1,777 and 3,030 tokens). Then the model called the tool from code (await tools.mcp__server__tool({…})) and got it right. Codex reported 137,344 input tokens for the turn (own usage, 120,576 of them cached).

No preload. With the "say hi" prompt, the first request was 17,098 input tokens (own usage) with or without the server attached, repeated in 4 of 5 control runs. The definitions cost nothing until the model goes looking.

What the source says. The public Codex repo defines ALL_TOOLS as a global inside the code-mode runtime. No ranking or BM25 sits in front of it. The "search" is whatever filter the model writes.

Where it breaks. If the model never thinks to look, it never finds the tools. In 80 live read-only runs against the real server (two rounds of 40, 16 Oct 2026), Codex didn't find the server's tools in 8. It said no connection was available and tried the browser instead. When it did find them, it mostly answered well. Also, turning code mode off (features.code_mode_host=false) didn't give classic direct tools in 0.161; the run just failed with "code-mode host is disabled".

Grok Build: a local BM25 index behind two tools

What we saw (Grok Build 1.0.60, grok-4.7, 16 Oct 2026). The first request had 28 tools (19,674 tokens), including search_tool and use_tool. No MCP tool definitions were in it. A reminder after the user's message said which servers were connected and how many tools each had, something like "MCP servers connected: … (99 tools)". It's 189 tokens. It lists no tool names, and if the server sends no instructions, no description either.

The model searched with a natural-language query and a limit of 10. The search ran on the client. It returned the full input schema of every hit, 4,957 tokens in one result, and the top hit was the right tool. The model searched three more times to check alternatives (4,313, 3,143 and 2,349 tokens). Then it called the tool as use_tool({tool_name, tool_input}). The schemas never become real tools in the request. They stay in the history as plain text and get re-read on every later turn.

What the source says. Grok Build's tool_index.rs checks for an exact match on the qualified or bare tool name first. Otherwise it ranks by BM25 over a document built from each tool. The MCP docs describe the two tools and the server__tool naming.

The --trust trap. Our first Grok run had the server configured in the project folder, and the folder wasn't trusted. In headless mode Grok didn't start the server and didn't warn us either; grok mcp doctor was the only thing that said so. The model ran 9 searches that found nothing, then spent 17 turns reading files around the home directory looking for the server. The run used 626k tokens. With --trust, the same task took 157.5k (38,868 input + 118,656 cache reads, own usage). If your docs tell people to add your server per project, mention this.

OpenCode: everything, every time (version 1)

What we saw (OpenCode 1.18.34 and 1.19.3, 16 Oct 2026). The first request carried 109 tools, the 99 MCP definitions among them, at 63,245 tokens. The whole body was 82,603. Version 1.19.3, the current default install, was identical. With the "say hi" prompt, attaching the server added 73,415 input tokens (own usage, default provider).

OpenCode 1.x sends that again with every model request. In 40 live runs with GLM-5.3-Flash (1,000,000-token window), it averaged 3.7 model requests per question, so roughly 230k tokens of definitions per question. Its largest single request was 131,714 tokens (own usage): big, but it fit. The window wasn't the problem in that test. The bill and the noise were. OpenCode also cuts off large tool results and saves the rest to a file. It did so with 32 results in 24 of its 40 runs.

The upside is that the model never has to find anything. In those 40 runs OpenCode got 39 right, the best of the three agents we ran live. With a large window and a cheap model, eager loading is a legitimate trade. It just isn't free.

OpenCode 2: Code Mode with a short menu

OpenCode 2.0.31 is a separate, opt-in release. Its docs say MCP tools go through Code Mode by default (codemode defaults to true), grouped as tools.server.tool(…).

What we saw (OpenCode 2.0.31, interactive app driven through a terminal, live read-only server, its default model, 16 Oct 2026, 2 runs). The system prompt carries a "partial catalog": - server (99 tools, 16 shown), then 16 tool signatures with descriptions cut to about 120 characters (881 tokens). It tells the model to call search(…) inside execute for everything else. The first request was about 5,270 tokens. In both runs the model searched once (2,879 and 5,059 tokens back), then called the tools from code. In the first run its code filtered the results, so only 188 to 1,001 tokens per step reached the model, although the code pulled 2.4 MB from the server over 8 calls. Both answers were correct.

Where it breaks. In the second run the model returned two whole activity pages. OpenCode cut each at 51,200 bytes (14,098 and 16,343 tokens), and the model then searched and read the saved files (4 extra steps). Peak request: 64,918 tokens.

Headless mode. In headless opencode run (with or without --server, two-turn, project config, mcp add, codemode:false) the server's tools never reached the model, although the logs said connected … tools=99. The same config works in the interactive app, where the first message was sent about 20 seconds after start. Our best guess, not confirmed: in the shipped binary the MCP tool registry is filled once when it starts and refreshed on a change event, so a headless run can build its first catalog before the server's tools arrive. If you test OpenCode 2 in CI, check that your tools are in the catalog.

Output caps in detail

Search decides whether your tool gets found. Output caps decide whether the model ever sees what your tool returned. All four agents cap a single tool result, and each does it differently: by tokens, by bytes, or by lines. All but one then save the full result to a file and leave the model to dig through it.

Agent (version)Default capChange it withOver the capVerified by
Claude Code 2.1.30125,000 tokens (warning above 10,000); text over 50,000 characters is saved to a file regardlessMAX_MCP_OUTPUT_TOKENS; per tool _meta["anthropic/maxResultSizeChars"] (up to 500,000 chars)No data inline. Saved to a file, and the model is told to read it in chunksdocs + live runs
OpenCode 1.18.342,000 lines or 51,200 bytes, whichever comes firstconfig tool_output.max_lines / max_bytesKeeps the head, saves the rest to a file (files are cleaned up after 7 days)shipped binary + live runs
Codex 0.161.0 (code mode)10,000 tokens per exec output by Codex's own estimate (the call can ask for a different max_output_tokens)per callKeeps the start and end, drops the middle with a warning. No filesource + capture + 3 live runs (wire)
OpenCode 2.0.312,000 lines or 51,200 bytes (same as 1.x)not checkedKeeps the head, saves the full output to a file: [showing lines 1-866 of 2589; full output saved to …]shipped binary + 1 live run
Grok Build 1.0.6020,000 bytesGROK_MAX_MCP_OUTPUT_BYTES or MAX_MCP_OUTPUT_BYTES, or [mcp] max_output_bytesKeeps the first 20 KB and saves the full result under the session's mcp/ foldersource only (not hit in our runs)

What the model actually sees

Claude Code (live run, 16 Oct 2026, paths shortened):

Error: result (98,689 characters) exceeds maximum allowed tokens. Output has been saved to ~/.claude/projects/…/tool-results/mcp-server-list_tool-….txt.
Format: JSON with schema: {…, items: [{...}], limit: number, offset: number, nextOffset: null, …}
Use jq to make structured queries (find a value, filter by field).
REQUIREMENTS FOR SUMMARIZATION/ANALYSIS/REVIEW:
- You MUST read the content from the file at … in sequential chunks until 100% of the content has been read.
- Note: this file is JSON, so a long value (or the whole file) is a single line. Read's offset/limit cannot split a line, …

It arrives as an error, with a schema sketch, and it tells the model to read everything. On a one-line JSON file, that's awkward. In our first round of 40 live runs, this happened 40 times in 23 runs, and the model made 138 extra file reads and searches to recover. The repeat round: 36 times in 22 runs.

OpenCode 1.18.34 (live run, 16 Oct 2026, path shortened):

…80400 bytes truncated…

The tool call succeeded but the output was truncated. Full output saved to: …/opencode/tool-output/tool_…
Use Grep to search the full content or Read with offset/limit to view specific sections.

The model does get the first 50 KB, so it often has enough to start. In 40 live runs, 32 results in 24 runs were cut this way (29 from the server's main list tool), followed by 123 extra file reads and searches. When the session can use sub-agents, the binary has a different version of the message that tells the model not to read the file itself but to hand it to a sub-agent. We didn't see that one in our runs.

Codex 0.161.0 (capture, 16 Oct 2026, stub server with the real tool list):

Warning: truncated output (original token count: 56619)
Total output lines: 2
[{"name":"mcp__server__activity_since","description":"Everything that happened in …

There's no file to fall back on. The model has to write a narrower filter and run it again, which is what it did (1,777 and then 3,030 tokens).

Live check (3 runs against the real server, model traffic recorded, 16 Oct 2026, one run each of three questions). In every run the model's first two looks at the tool list were cut: 54,579 to 159,362 tokens (Codex's own count) down to about 9,000. Every result it printed whole was cut as well: the main list tool's page (133,088 and 129,770 tokens), an activity page (86,562) and the list of campaigns (19,746). Each time Codex kept about 10,000 tokens, with …N tokens truncated… in the middle. Each time, the model recovered by writing code that filtered the result or kept it in a variable, then printed only what it needed (22 to 5,115 tokens). In two of the three runs that meant calling the same tool again, so the server got one extra call of about 520 KB and 350 KB. All three answers were correct. Peak requests were 47,814, 55,145 and 79,856 tokens (own usage), over 8 to 9 model requests.

Grok Build 1.0.60: according to the source, the model gets the first 20 KB, then [MCP output truncated: showing first 20 KB of N. Full output written to: …]. For JSON it adds a hint to query the saved file with a shell tool. Our Grok runs never returned a result that large, so this part is from source only.

What it costs

  • A cap protects the context window, but it doesn't make the call cheaper. Each recovery step is another model request that re-reads the whole conversation. Our Claude Code runs averaged about 210k to 235k input tokens per question (own usage). Most of that was cached, but none of it was free.
  • The caps disagree. A 46,253-token result (218,237 characters in the live error message) is refused outright by Claude Code, cut to 50 KB by OpenCode and to 20 KB by Grok Build, and inside Codex code mode it gets the middle cut out. The same server gives four different experiences.
  • JSON on one line defeats line-based reading. Both Claude Code's and Grok Build's messages warn about this.

For server authors: stay under every cap

  • Aim for under about 5,000 tokens per default result (well under Grok's 20 KB, which is the smallest cap). Treat 10,000 tokens as the ceiling. Above that, Claude Code warns and Codex starts cutting.
  • Page by default. A small limit, a cursor, and a total count, so the agent can tell there's more.
  • Let callers pick fields (fields: ["id","name","status"]) and return a compact shape unless they ask for more.
  • Lead with a summary ("47 results, 20 shown, next cursor …") so a cut-off head still answers the question.
  • Don't send the same data twice as text and structured content. That alone roughly halves the largest results we measured.
  • If a tool really must return a lot (a schema, a file tree), Claude Code lets you raise that one tool's limit with anthropic/maxResultSizeChars. Use it sparingly.

Can I have 1,000 tools?

Technically, in some agents, yes. Claude Code's docs say it has no fixed per-server cap and the practical limit is your context budget. Grok Build and Codex never put definitions in the first request, so a long list doesn't cost anything up front. Whether it works is a different question, and here's what we measured with 99:

Our honest answer: you can expose 1,000 tools, but you shouldn't make an agent choose between them. Group them into fewer, broader tools with a parameter, or ship a small default set and put the long tail behind one documented search or a separate server. Then test with the agents your users run, in their default settings. We haven't measured a 1,000-tool server. Everything above comes from one 99-tool server.

What we didn't test

If you'd like these measurements for your own MCP server, CLI or API, across the agents your users run, talk to us or read the FAQ. More notes are on the blog.

Get your MCP server tested across Claude Code, Codex, Grok Build and OpenCode. We run the jobs your users care about through each agent and show you what breaks, with numbers.

Talk to us

Sources