Get in touch
Gabs
Blog

Too many MCP tools? 130 tools for half the context tokens

Too many MCP tools? On SharpOS, 130 tools cost about 32,000 tokens up front, half a strict mirror, with one registry and lazy schemas.

Gabriel EspinheiraSenior Full-Stack Engineer

Listen to this post· AI-narrated

0:00 / 13:22

Too many MCP tools is how people describe it when an agent's context window is half full before the first message. I wanted to know what actually fills it, so on 15/10/2026 I measured the MCP server of SharpOS, the client platform I built for my studio, SharpHaw. Its tools/list holds 130 tools (129 plus one helper) that cover 220 operations, and it weighs about 32,000 tokens. The same 130 tools with their full strict input schemas inlined weigh about 67,400, with names and descriptions unchanged. The two differ only in when the agent loads each tool's exact contract: all of them up front, or one at a time on first use.

This post shows how that server is built, how I measured it, and what it still gets wrong.

TL;DR: Too many MCP tools hurt because of schema weight, more than tool count. Generate REST and MCP from one operation registry, list each tool with a compact discovery schema, and let the agent fetch the strict schema on first use. On SharpOS, 130 tools cost about 32,000 tokens up front, half the 67,400 of a strict mirror.

Why do too many MCP tools eat the context window?

An MCP client calls tools/list once and, unless it defers loading, puts every tool definition into the model's context before the first message: name, description, annotations and the JSON Schema for its input. Anthropic's tool search docs for the Claude Agent SDK (opens in a new tab) say 50 tools can use 10,000 to 20,000 tokens, and that tool selection accuracy degrades past 30 to 50 tools loaded at once.

Teams adding MCP to an existing product usually hit this in one of two ways.

The first is a hand-written tool layer. Someone writes twenty MCP tools by hand next to the REST API, picking the operations they think agents need. It ships fast, and then it drifts. The API gains a field, the tool doesn't. A permission changes in one place and not the other. Before long the agent has a special door into the product, with its own bugs.

The second is the strict mirror. A generator walks the OpenAPI spec and emits a tool for every endpoint, each with its complete schema. Nothing drifts, but the agent pays for every schema before it has done anything.

I wanted neither. SharpOS has one rule for agents: they use the same API I do. If an agent needs a special door, the API is wrong.

How do you turn a REST API into an MCP server without drift?

You stop treating MCP as a second API. SharpOS runs on Convex, and its backend has one registry of 220 operations, such as boards.cards.create or media.assets.list. REST and MCP both read from it. Each operation declares its input schema, its output schema and the function it runs. The REST route and the MCP tool are two adapters over that one definition, so neither can grow a field the other lacks.

Two checks keep the surfaces equal. The MCP catalog asserts, as it is built, that every operation is mapped by exactly one tool, and throws otherwise; a unit test pins the same thing. A generated report, bun run report:mcp, shows it to a human. On 15/10/2026 it printed:

MCP report: MCP_REPORT.md (129 tools, 220 covered operations, 0 missing operations)

So an operation added to the registry without an MCP tool fails the tests instead of quietly shipping REST-only. The hub, the web app people use, calls Convex directly; only REST and MCP pass through the registry. That keeps the agent surface honest, because what I can do through the API, an agent can do through MCP, with the same validation.

Why 130 tools for 220 operations?

One tool per operation would have meant 220 tool definitions. SharpOS groups operations by domain and by semantic action instead. boards_read, for example, is one tool with three actions:

[
  { "action": "list", "operation": "boards.list" },
  { "action": "detail", "operation": "boards.get" },
  { "action": "templates", "operation": "boards.templates.list" }
]

Reads group with reads and writes with writes. Deletes and archives get tools of their own, 21 of them, and a tool is annotated read-only only when every operation behind it is a read. That mapping gives 129 tools. The 130th is a helper, sharpos_tool_schema.

Grouping cuts the tool count. Most of the weight is in the schemas, and that's where the second decision comes in.

What is lazy schema loading for MCP tools?

Every tool in tools/list carries a compact discovery schema: the field names, the descriptions an agent needs to pick the tool, and loose types, or none at all for some fields. The exact, strict contract (types, required fields, enums, additionalProperties: false, the output shape) sits behind the helper. The server tells every client to use it, in the instruction string it sends on initialize:

const SHARPOS_MCP_INSTRUCTIONS =
	"SharpOS tools are grouped by domain and semantic action. Before first use of a tool, call sharpos_tool_schema with its name to load the exact strict input and output contract; this keeps initial discovery compact. ..."

Here is what an agent sees for card_read in tools/list, 523 characters in full:

{
  "name": "card_read",
  "title": "Read card",
  "description": "Read a board card with its resolved properties and source links.",
  "inputSchema": {
    "type": "object",
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "properties": {
      "boardId": {},
      "cardId": {},
      "orgRef": {
        "type": "string",
        "minLength": 1,
        "description": "The target organization ID or slug returned by organizations_list."
      }
    },
    "required": ["boardId", "cardId", "orgRef"],
    "additionalProperties": {}
  },
  "annotations": { "readOnlyHint": true, "destructiveHint": false, "openWorldHint": false }
}

Calling sharpos_tool_schema with card_read returns 7,605 characters: the typed input, the operation it maps to, and the full output schema with every property type a card can carry. An agent reading one card needs that once. An agent that never reads a card shouldn't pay for it on every turn.

The server still validates every call against the strict schema. A loose discovery schema only changes what the agent reads up front, never what the server accepts.

How much context do MCP tools cost? The measurement

I measured the live production server on 15/10/2026 with a short script: initialize, tools/list across every page, then sharpos_tool_schema for each of the 129 tools. I serialised each payload as JSON and divided the characters by four. That's an estimate, not a model tokenizer. JSON's braces and quotes usually tokenize denser than prose, so the real counts are probably higher, but the method is the same for every row, so the ratios hold. I ran it three times. The payload is deterministic, so all three runs gave the same numbers, a range of zero. One setting couldn't be held equal to what most agents see: I used an admin key, and an organization key gets a smaller, filtered list.

Payload, same 130 toolsCharactersTokens (chars ÷ 4)
tools/list as served, compact discovery schemas127,991~32,000
Strict input schemas inlined269,765~67,400
Strict input and output schemas inlined404,067~101,000

The strict version of the same catalog costs 2.1 times what SharpOS serves, and 3.2 times if it ships output schemas too, which a server should do if it wants agents to get validated structured results. A mirror with one tool per operation would add 90 more definitions on top. A first call to the helper costs about 800 tokens on average, and the result stays in the conversation. An agent that touches ten tools in a session carries about 8,000 tokens of strict contracts it actually uses, on top of the 32,000. Against the input-only mirror, the break-even sits around 44 distinct tools in one session, where the helper calls add up to the 35,400 tokens the compact list saved. Against the mirror with output schemas, which is the fair comparison because the helper returns both, it sits around 86.

To measure your own server, call tools/list, serialise the tools array and count it. Then build the variant you're comparing against and count that too. The tool count tells you much less than the byte count.

Doesn't MCP tool search in the client already fix this?

It helps a lot. Claude Code and the Claude Agent SDK turn tool search on by default: tool definitions are withheld, the agent searches the catalog, and up to five matching tools load per search. My own Claude Code sessions load the SharpOS tools that way, deferred until used.

It doesn't make server design irrelevant, for two reasons. First, when a deferred tool does load, it loads whatever definition the server wrote, so a bloated schema still costs the full price at the moment the agent needs it. Second, not every client does it. Anthropic's docs list cases where the SDK falls back to loading everything up front, such as when ANTHROPIC_BASE_URL points to a proxy that isn't first-party, and other clients have their own rules. A server that keeps discovery compact costs less in every client, deferred or not, and the two savings add up.

Scopes and errors an agent can act on

Per-key filtering is the other lever on tools/list, and it depends on scopes nobody can forget. The scope map in SharpOS is typed as exhaustive:

export const OPERATION_SCOPES: Record<
	IntegrationOperation,
	readonly IntegrationScope[]
> = {
	"boards.cards.create": ["boards:write"],
	// ...
}

Because it's a Record over every operation, adding an operation without a scope fails the typecheck instead of shipping open to every key. An agent's key carries only the scopes it was given, never more than the person who issued it holds, and the MCP catalog is filtered per key, so a read-only agent never sees write tools in tools/list at all. That filter is also why an organization key gets a smaller list than the admin key I measured with.

When something fails, the agent needs to know what to do next. Every REST or MCP failure returns a request ID and one of 11 typed error codes, from validation_error and not_found to rate_limited and write_conflict. An agent that gets validation_error can fetch the strict schema and retry. One that gets forbidden knows retrying won't help. The Activity Log in SharpOS records whether each change came from the hub, an API key, MCP or an automation. On a demo organization, an agent created a card over MCP, through the same operation a REST client would call, and the open board showed it about two seconds later with no refresh.

What lazy MCP schemas cost, and what I'd change

The first call to each tool takes an extra round trip. For an agent that uses three tools in a session, that's three small calls, and worth it. For a one-shot agent that calls one tool once, it's pure overhead.

Lazy schemas don't help tool selection. 130 tools is still past the 30 to 50 where Anthropic says selection accuracy drops, and a compact schema doesn't change how many names the agent has to choose from. Grouping 220 operations into 130 tools and per-key filtering shrink that list, and client tool search narrows it further, but on a client without tool search an admin agent still picks from 130.

32,000 tokens is still a lot. Of the 127,991 characters in tools/list, 88,946 are discovery input schemas. The 20 heaviest tools hold 40,395 of those characters, almost half, and most of them write rich content, such as audit ingestion and dataset records, where the discovery schema still describes nested objects. The second heaviest is the helper itself, because its input lists all 129 tool names. The next cut is to flatten those to their top-level fields and leave the nesting to the strict schema.

Grouping has a cost too. The action names are mine, and an agent picks a tool by reading them, so boards_read with detail only works if the description says plainly what detail returns. Every grouped tool puts more weight on its descriptions than a one-operation tool would.

And an instruction is only a request. An agent can skip the helper and call a tool straight from the discovery schema. The server's strict validation catches a bad call and returns validation_error, so nothing breaks, but it costs a wasted round trip. That wasted call is still cheaper than 67,400 tokens in every session.

If I started again, I'd keep the registry and the exhaustive scope map exactly as they are. I'd measure tools/list in CI from day one, with a budget that fails the build, the same way a unit test guards MCP coverage. Today's number should have been a failing test months ago.

Where to start with your own MCP server

If you're adding MCP to a product that already has an API, start from the operations, not the tools. Put every operation in one registry, generate both surfaces from it, and make a missing scope a type error. Then measure your tools/list before you decide what to cut.

I've built SharpOS as its only engineer since March 2026, directing coding agents, and of everything in it, the registry is the decision I'd make again first. The SharpOS case study shows the rest of the platform. More posts on building with agents are on the gabs.app blog. If you're putting agents on your own product and want a second pair of eyes, get in touch.

More writing

All posts