Maya is a data engineer in Seattle. She has a sidewalk dataset to publish before a Friday deadline, and a new MCP server wired into her agent. She types: "which projects am I in?" The agent pages through every project group in the system, filters the point-of-contact email on each one, finds two that match her address, and answers with complete confidence. It is wrong. The platform has no membership endpoint, so there was nothing to be right about.

Arjun picks it up next morning in Hyderabad and asks for the latest Seattle dataset. He gets a forty-element JSON array in the chat window. Then he asks the agent to upload a 2 GB zip, and it tries to base64-encode the file through the MCP server. Every one of those tool calls succeeded. The API returned 200. (The characters are illustrative; the three failures are from my own logs.)

The tool list was complete. The behaviour was not.

I have been building tdei-mcp, an MCP server over a transit-data platform's REST API. It now exposes 41 curated tdei_* tools.

The first version was generated from the OpenAPI spec. That took an afternoon and produced a tool for every operation, each with the summary line the spec happened to carry. It was complete, well-typed, and the agent used it badly.

The reason is simple once you see it. A tool description is not reference material that a developer consults when stuck. It is the only thing in the model's context at the moment it decides what to do. It is a prompt, loaded on every turn, and whatever it fails to say, the model will fill in by guessing. OpenAPI summaries are written for people who already know the domain. They answer "what does this endpoint do". They never answer "when should you not call this", which is where agents actually fail.

Anthropic's engineering guidance puts it well: write tool descriptions the way you would brief a new hire, and make implicit context explicit. The same post warns that tools which merely wrap existing API endpoints, and tools whose purpose overlaps, are a leading cause of agents choosing badly.

Four ways to put an API in front of an agent

Auto-generate from the spec. One tool per operation. Fast, complete, always in sync with the API. The model inherits whatever vocabulary your spec uses and none of your operational knowledge.

Code execution. Present the servers as code modules and let the agent write a script against them. Anthropic reports one example dropping from about 150,000 tokens to 2,000. It needs a sandbox with resource limits and monitoring, which is real operational overhead.

Deferred loading and tool search. Keep hundreds of definitions out of context and let the model search for the three to five it needs. Anthropic's docs say a typical GitHub, Slack, Sentry, Grafana and Splunk setup burns roughly 55k tokens in definitions before any work starts, and that tool search cuts that by over 85%. The RAG-MCP paper reports the same shape of result from retrieval: selection accuracy of 43.13% against 13.62% for full-context prompting, with over 50% fewer prompt tokens.

Curated tools with written instructions. A small hand-designed surface, each description written as an instruction rather than a summary, plus server-level instructions sent at initialize. More work up front, and the instructions are now code you have to maintain.

Approach Tools in context Where behaviour lives Main cost
Generated from OpenAPI One per operation The model's guesses Unreliable selection
Code execution Few, loaded as modules Agent-written scripts Sandbox and ops burden
Tool search, deferred loading Three to five per turn Search relevance Weak descriptions still lose
Curated plus instructions A designed subset Your written text Hand-maintained prompts

These are not exclusive. tdei-mcp keeps the generated OpenAPI tools available for advanced access and routes normal requests to the curated ones.

What the instructions actually have to say

Six patterns did most of the work. All of them are text, none required a model change.

Server-level instructions. MCP has a server-level instructions field, which the spec describes as "optional natural-language guidance for LLMs on how to use this server effectively". It is returned by server/discover in the current revision, and by the initialize handshake in earlier ones. Clients hand it to the model much like a system prompt. Mine carries two rules that do not belong on any single tool: cache stable reference data within the conversation instead of re-fetching every turn, and render list results as a markdown table with a count summary rather than raw JSON. That one paragraph fixed Arjun's forty-element array across every tool at once.

Negative constraints. The most valuable sentences in my descriptions say what not to do. The project-groups tool reads, in part: "This is NOT membership — TDEI has no membership endpoint... Never page all results plus filter poc emails to infer membership." The identity tool states it is "the ONLY source of the login email — never infer it from project-group poc contacts." Maya's wrong answer was a plausible inference. Only an explicit prohibition removes it.

Name the real endpoint. Each description names the operation and path behind it. This is not for the reader; it stops the model inventing a sibling endpoint that sounds like it should exist.

A deterministic router. Users say projects, orgs, groups and project groups for one concept. Rather than hope 41 descriptions disambiguate that, there is a tdei_resolve_intent tool that maps a plain-language request to one tool and one API operation using plain pattern matching, and returns a structured clarification instead of guessing when the request is genuinely ambiguous. No classifier, no model call, and it is unit-testable.

Four explicit result states. Every tool returns complete, needs_input, ambiguous or unsupported, and carries back the non-secret inputs already accepted so the client can resend them. The server holds no conversation state. The 2026-07-28 MCP spec has since formalised this shape as an input_required tool result, which is a good sign the pattern generalises.

Confirmation and architectural boundaries. Destructive operations require confirm: true in the schema, so the gate survives even if a client ignores annotations — and the spec explicitly tells clients to treat annotations as untrusted unless the server is trusted. The file-handling rule is stated in every relevant description: the server never accepts file bytes, and instead returns a ready-to-run curl command the agent executes on the user's own machine. Maya's 2 GB upload is now a one-hop transfer that never touches my server.

Pattern Failure it prevents Where it goes
Server instructions Raw JSON, redundant re-fetching initialize response
Negative constraints Confident wrong inferences Tool description
Named endpoint and path Invented operations Tool description
Deterministic router Vocabulary collisions A dedicated tool
Explicit result states Silent partial answers Result schema
confirm in the schema Unreviewed destructive calls Input schema

Where the other approaches win

Auto-generation is the right call for an internal API with one or two consumers, or when the spec changes weekly and hand-written instructions would go stale faster than you can fix them. Keeping generated tools behind an allow or deny list, as tdei-mcp does by operation ID, gets you most of the benefit for very little effort.

Code execution wins when the work is genuinely compositional — join three systems, filter, aggregate — and intermediate results would otherwise pass through the model twice. If you already run untrusted code safely, take it.

Tool search wins on breadth. Anthropic's own documentation says selection accuracy degrades once you exceed 30 to 50 available tools. At 41, my curated surface is already inside that band, which is a problem better descriptions do not solve. Deferred loading is on my list, and good descriptions make the search work better rather than replacing it.

Be honest about the maintenance cost too. A test-design document in my own repo still described nine semantic tools days after the implementation had 41. Instructions drift exactly like code comments, and they drift silently because nothing fails to compile. This is team knowledge, not personal knowledge: the reason a description forbids inferring membership from contact emails is a decision someone made once, and it belongs somewhere the next engineer and the next agent can both find it. A shared project-knowledge layer such as ContextHarbor is one way to keep that rationale out of one person's head.

How to choose

If your API has fewer than ten operations and one consumer, generate the tools and move on.

If users ask for outcomes rather than endpoints, hand-write a small curated surface and spend your time on the instructions, not the schemas.

If you are aggregating several servers and past roughly 30 tools, add deferred loading and tool search first, then fix the descriptions the search depends on.

The test I now apply before shipping any MCP tool: read the description aloud and ask whether a competent new engineer could do the task correctly from that text alone, including knowing what not to try. If not, the model cannot either.

If you are putting an existing API in front of agents and the behaviour is not holding up, book a CTO consultation and bring your tool list.