Skip to content

Blog Article

We Deleted Our One-Tool MCP Gateway

Why AppHandoff removed its natural-language MCP gateway: 5% of traffic, a 19.7% error rate, 10x latency — and the 24 typed tools that replaced it.

MCPArchitecture

If you've wired more than a couple of MCP servers into an AI agent, you've felt it: the context window fills up before the agent does any work. Load every schema at connect time and you've spent a large chunk of context describing tools the agent may never call. That problem is real, and it's the reason we built ask_apphandoff — a single natural-language gateway tool that routed plain-English intent to the right underlying call.

We shipped it in May 2026, ran it in production for three months, pulled it from the public MCP surface on 1 August, and deleted the code on 6 August. This post is the honest version of that arc: the problem it was meant to solve, what the production numbers actually said, and the design that replaced it.

The tool-sprawl problem is real

Start with the part we still believe. Every tool you register on an MCP server ships its full JSON schema to the model on every request. The context window tax is the obvious cost: dozens of tools at a few hundred tokens of schema each is real budget, gone before the agent reads your actual prompt. On long sessions it compounds, because the schemas ride along on every turn.

Schema bloat makes it worse. Rich tools have nested parameters, enums, and descriptions, so the more capable your server, the heavier each schema and the worse the ratio of "tools described" to "tools used." Then there's agent confusion: a flat list of similarly named tools invites wrong picks. The model burns turns choosing, second-guessing, and retrying. More surface area means more ways to misfire.

This isn't an AppHandoff-specific failure — it's structural to MCP. The protocol is generous about what you can expose, and that generosity turns into a tax the moment your server gets useful. The disagreement is only about the fix.

What we built

ask_apphandoff collapsed the surface to one entry. The agent sent a natural-language query and a project id; the gateway routed (an LLM mapped intent to the tools that satisfy it), invoked them server-side where the full schemas lived, and paraphrased the result in plain language. Mutating actions stopped for a confirmation round-trip before committing.

On a whiteboard this is a clean trade: one schema in context instead of forty, and one place to enforce a write policy. It demos beautifully. The trouble only shows up in aggregate, months in, when you look at what agents actually did with it.

What the production data said

We pulled 30 days of MCP traffic before making the call. Four numbers decided it. Adoption was flat: the gateway carried 304 of about 6,300 tool calls, roughly 5% — given the choice, agents overwhelmingly called the typed tools directly. It failed far more often: a 19.7% error rate, the worst of any high-traffic tool, against 1.4–3.5% for direct reads. It was slow: 3.2s average and 10.3s at p95, against about 0.3s direct — roughly 10x, which is what an extra inference hop costs. And the one that ended the argument: it silently lost writes — a two-ticket request came back reported as done, having created one ticket with a one-line stub body.

That last failure is the structural one, not a bug we could patch. A paraphrase layer is a lossy summary of what happened, and a lossy summary of a partial write reads exactly like a complete one. The agent gets a confident sentence and no way to tell the difference.

Why routing was the wrong layer

The gateway's bet was that a model whose only job is routing beats an agent picking a tool as a side task. In practice that bet lost for a simpler reason than model quality: we had relocated the ambiguity instead of removing it. The router still had to disambiguate near-synonym tools — it just did so with less context about the caller's actual goal than the calling agent had. We added an inference hop, a failure mode, and a latency budget, and kept the original problem.

Meanwhile the thing tool-calling models are genuinely good at — reading a well-named tool with a precise description and calling it with typed arguments — was the path we had de-emphasized. The 95% of traffic that bypassed the gateway was the answer, months before we acted on it.

What replaced it: fewer tools, better named

The fix for a bloated catalogue turned out to be curating the catalogue. We cut the listed surface from 44 tools to 24 and made the survivors carry their weight.

Merge instead of multiply. The confusable families collapsed into one tool with an include parameter: get_ticket(include: thread | activity | audit | tasks) replaced four separate ticket-reading tools. get_project_summary(include: "rules,presence") absorbed two more. get_accessible_projects took over repo, URL, and project-name matching from a separate resolver tool. One obvious entry per job beats five near-synonyms — and it beats a router that has to tell them apart.

Profiles instead of a gateway. The MCP tools/list call returns a core profile by default; a client that wants everything asks for profile: "full". That gets the context saving the gateway promised without putting an LLM between intent and execution.

Batching instead of paraphrase. batch_tools runs up to 20 typed calls in one request and returns a per-call success or error. It's the multi-step case the gateway was reaching for, minus the silent partial write — every item reports its own outcome.

Gates on the tools that need them. The blanket confirm step is gone, because a policy applied to the whole catalogue mostly taxes the reads that carry no risk. The irreversible verbs carry their own gate instead: delete_project refuses to run without allow_writes: true, and delete_handoff_request archives rather than destroys. Safety lives on the dangerous call, not in front of all of them.

What we'd tell you to do

If your MCP tool list is past twenty and climbing, the context tax is real and you should act on it — but reach for the boring fix first. Merge the tools that overlap, name what's left precisely enough that a model doesn't need a paragraph to choose, describe every argument, and serve a core profile by default. Add an inference layer only if you have measured that curation wasn't enough. We didn't, and it cost us a year-old feature and a public argument we had to retract.

The current surface is live at https://api.apphandoff.com/api/mcp-bot — 24 listed tools, OAuth 2.1, Streamable HTTP. Browse the full reference on the MCP server page. If you have an agent running an old prompt, point it at that page — the retired names are gone from the catalogue and will not resolve.