MCP Tools Are Not Endpoints
Point an OpenAPI-to-MCP generator at a service-desk API and you have a working server before lunch: one tool per operation, every parameter typed, every description lifted from the spec. The demo works. Then a support lead asks the agent the question they ask every morning, "what's open for the network team, worst SLA first?", and for twenty open tickets the agent spends 43 calls copying IDs from one response into the next request.
Thesis: An MCP tool should be shaped like one job a caller can name, with the plumbing folded in and the decisions left out: consolidate the mechanics an endpoint list exposes, never the judgment that belongs to the agent or the policy that belongs to the backend.
I've said the MCP "bloat" complaint is "a dev/build issue not a protocol one." Mirroring endpoints one-to-one is the build choice that produces it, and the advice against it is not new: Anthropic's tool-writing guide calls tools that "merely wrap existing software functionality or API endpoints" a common error, and Block's playbook says to design "top-down from workflows, not bottom-up from API endpoints." What those guides leave open is where consolidation stops. This series once drew that line at sequencing; this post moves it to the kind of step being sequenced. Dropping servers an agent never calls is a separate fix; the question here is the tools inside one it does call.
What a Generated Server Hands the Model
An endpoint list was designed for a programmer who reads the docs once. A model reads it fresh on every task, and four things go wrong.
Selection gets harder as the list grows. Anthropic reports that the most common tool-use failures are "wrong tool selection and incorrect parameters," worst when names are near-twins; hiding most of the list behind tool search raised Opus 4 from 49% to 74% on its internal MCP evaluations. A 2025 preprint, RAG-MCP, reports 13.62% selection accuracy with every MCP schema in the prompt, against 43.13% with retrieval in front.
CRUD verbs make the model the orchestrator. GET /groups, GET /tickets and GET /slas/{id} are each correct and useless until something sequences them. Every step is a reasoning round trip, and every intermediate response lands in context.
Plumbing leaks into the conversation. The model must carry IDs it cannot read, follow cursors through results it did not need, and resolve "the database on-call group" to a UUID before it can do anything.
Errors are written for programmers. 422 {"error":"FIELD_REQUIRED","field":"close_code"} tells a developer to open the docs. It tells a model to guess. The MCP spec expects better: tool execution errors "contain actionable feedback that language models can use to self-correct."
Count Is the Wrong Axis
The obvious fix is to cut the number of tools, and my own record shows why that is not enough. The January post diagnosed the Ema MCP server's 7 mode-switched "super tools" as parameter sprawl, 30-plus parameters per tool, with the lesson "Fewer parameters beats fewer tools"; by March it was 9 domain-scoped tools with a method parameter, presented as the fix for schema overhead. Across the posts the count went about 45, 7, 20, 6, 9, and no single number was the fix.
That last shape needs a straight answer, because a method parameter is the January mode move again. GitHub's MCP server made it in public in October 2025, folding six pull-request read tools into one pull_request_read, and reported the change "introduced no regressions." Anthropic's tool-definition guide goes further: group related operations "into a single tool with an action parameter. Fewer, more capable tools reduce selection ambiguity." It is advice, not data, and right about the count. But a bundle leaves the operations where they were: GitHub's methods are still get_files, get_diff, get_review_comments, and a later change added a method and more hints because "Models struggle a lot distinguishing the 2." The ambiguity moved inside the enum. Anthropic's own example, create_pr, review_pr, merge_pr, would put a create and a merge behind one destructiveHint as an MCP tool. Against bundling there is only the January case, where parameters sprawled; otherwise a bundle is as good as its methods.
| Shape | What it exposes | Verdict |
|---|---|---|
| Endpoint mirror | One tool per operation | Selection load, model as orchestrator, IDs and cursors in context |
| Bundle | One tool per resource, mode or method enum |
A count fix: fails when parameters sprawl, neutral otherwise; inherits the shape of its methods |
| Orchestrator | One tool per workflow, with branches | Decides for the caller: the ESB this series warned about |
| Intent tool | One tool per job a caller can name | The target: plumbing inside, decisions outside |
An intent tool is not the "intent parsing" that MCP Isn't the Problem lists as an anti-pattern: the model reads the request, and the tool takes typed parameters such as team="Network", never free text.
One Service Desk, Two Servers
Take an illustrative service-desk API with 60-odd operations across tickets, users, groups, SLAs, transitions and a knowledge base. Here is what four routine jobs cost on each server.
| The job | Generated server | Intent server |
|---|---|---|
| "What's open for Network, worst SLA first?" | Look up the group ID, page through tickets, fetch each SLA, fetch each requester: 3 + 2N calls | servicedesk_find_tickets(team="Network", sort="sla_remaining"): 1 call |
| "Hand INC-20431 to Database On-Call" | Resolve the ticket number to an internal ID, resolve the group name, PATCH: 3 calls | servicedesk_assign(ticket="INC-20431", to="Database On-Call"): 1 call |
| "Resolve it with this note" | Fetch allowed transitions, POST, get a 422, guess the code's values, POST again: 3+ calls | servicedesk_change_status(...): 1 call, or one error that names the fix |
| "Is there a known fix?" | Search articles, fetch each one: 1 + N calls | servicedesk_search_kb(query): 1 call, titles and summaries |
| Tools in context | 60-odd | 7 |
The seven are find, get, create, comment, assign, change status and search the knowledge base. get sounds like REST, but it is a job: it takes INC-20431, the number on the support lead's screen, and returns names and states, not raw IDs. Every tool works that way (in Anthropic's example, a concise response took 72 tokens against 206), and each error says what to send next, the case MCP Isn't the Problem made for blocking validation and _next_step hints:
servicedesk_assign(ticket="INC-20431", to="DB oncall")
→ isError: No group named "DB oncall". Closest: "Database On-Call",
"DBA Escalations". Retry with one of these names.
servicedesk_change_status(ticket="INC-20431", status="resolved")
→ isError: Resolving a P2 needs resolution_code: one of Fixed,
Workaround, Duplicate. Retry with resolution_code set.
Reads and writes sit in separate tools, following Block's advice ("If possible, don't mix read and write operations in the same tool"). That lets the server set readOnlyHint and destructiveHint honestly, though the spec tells clients to treat annotations as untrusted unless the server is trusted.
Where Consolidation Stops
MCP as Discovery Protocol warned that the moment MCP "accumulates logic, knowledge, or state," it repeats the enterprise service bus failure. Its second warning sign reads: "Your MCP server calls multiple backend services in sequence. This is orchestration. Expose each backend as a separate tool and let the agent compose the sequence." Its table kept "Sequence operations" and "Transform data" out of a discovery protocol, and MCP: The Semantic Data Layer said a server "orchestrating multi-step flows internally" has rebuilt the ESB. servicedesk_find_tickets calls four endpoints in sequence and drops most of their fields. By those rules it is out.
This post reverses those prescriptions and keeps the rest. The old post left room ("None of these are absolute prohibitions. A small MCP server for a single team might reasonably combine a few operations."), and the January checklist already asked, "Are common multi-step workflows capturable in one call?" The replacement line sorts every step in a chain into one of three kinds:
flowchart LR
Step["One step in a<br/>multi-call chain"] --> Q1{"Same for every caller<br/>and every input?"}
Q1 -->|"Yes: paging, ID lookup,<br/>field projection, fixed joins"| Tool["Fold into the tool<br/>(plumbing)"]
Q1 -->|"No"| Q2{"What kind of choice?"}
Q2 -->|"A judgment about this case"| Agent["Leave to the agent<br/>(decision)"]
Q2 -->|"A rule every caller must obey"| Backend["Enforce in the backend<br/>(policy)"]
style Tool fill:#2d6a4f,color:#fff
style Agent fill:#5a5a8a,color:#fff
style Backend fill:#5a5a8a,color:#fff
style Q2 fill:#5a5a8a,color:#fff
Plumbing is the same for every caller and input: follow the cursor, turn a ticket number into an ID, join the SLA, drop the fields nobody asked for. Folding it in adds no knowledge of the business to the layer. OpenAI's guide says it plainly: "Combine functions that are always called in sequence."
Decisions vary with the case: which ticket matters most, whether to escalate, who should own it. servicedesk_assign takes to as an argument; it never picks the assignee from the ticket's text. Anthropic's schedule_event, which "finds availability and schedules an event," is plumbing if the caller states window, attendees and duration and the tool books the first fit, and a decision once it starts preferring mornings. When a tool must choose among valid options, return the options.
Policy binds every caller: a P2 cannot close without a resolution code. That lives in the backend, where the web app and the CLI hit it too; the tool only translates the refusal into a sentence the model can act on. If the rule exists only in the MCP handler, every other client skips it, and the layer has become the bus.
The rest of the old post stands: Design A's faults ("when to validate, what to roll back to, what sequence to follow") are policy and decisions, so they stay out, as do routing on content, state between calls and any rule that lives only in a handler.
When the Opposite Wins
"Seven tools serve seven jobs. Who serves the rest, and who keeps them current?" This is the strongest objection. Many of the 60-odd operations are plumbing the seven call; the rest, such as bulk import or admin settings, are jobs nobody has asked the agent to do yet. Coverage does drop: an agent that meets an uncovered job fails where a generated server would let it fumble through. Treat those failures as the backlog, and keep the generated server for callers that need full coverage.
Maintenance is the second cost, and it breaks a promise this series made: MCP as Discovery Protocol promised "independent lifecycles," with backend changes that never touch MCP. A curated layer breaks that for its owners, who absorb every backend change in a handler, and keeps it for the agent, whose seven schemas stay put. Build the intent tools on a generated typed client, so a breaking change fails the build rather than the agent, and let the team that owns the API own them; a central MCP team curating every service is how a bus starts.
"Models will get better at picking from many tools." They already are. In the same Anthropic evaluation, Opus 4.5 scored 79.5% before tool search against Opus 4's 49%, and tool search lifted it to 88.1%. But a perfect picker still runs 3 + 2N calls for the morning question and still guesses after a 422. Better selection fixes the first failure in the list, not the other three.
Code- and query-writing callers. With Anthropic's programmatic tool calling, only the model's script sees intermediate results (Anthropic reports 37% fewer tokens on complex research tasks), and Block's Linear server swapped "all 30+ tools" for two GraphQL tools that turn a 4-to-6-call question into one query. For a caller that writes code or queries well and can see the schema, granular tools are a library, and libraries should be granular.
Developer exploration and bootstrapping. An engineer debugging an integration wants the exact endpoint; give them the one-to-one server, not the support agent. Even FastMCP, which ships an OpenAPI converter, calls generation "a great way to get started" and says LLMs do "significantly better" with curated servers (no benchmark given).
What to Do Next
An audit an API team can run on its MCP server:
- Classify every tool as endpoint mirror, bundle or intent tool, and classify each bundle's methods the same way.
- Trace ten real jobs from logs or transcripts and count the tool calls each takes. Anything over three is a candidate.
- Sort each step in those chains. Fold plumbing in (every ID or cursor the model copies between calls is a candidate), leave decisions to the agent, and confirm the backend enforces each policy; if only the handler does, move it down.
- Fire each tool's top three errors. Each must say what was wrong and what to send instead, as an
isErrortool result, not a protocol error. - Split reads from writes and namespace by service. Then set
readOnlyHintanddestructiveHintso clients that trust the server can decide what to confirm. - Read every branch in every handler. Branching on mechanics is fine: a next-page cursor, an error code to translate. Branching on what the content means, or on which business rule applies, is the bus.
- Re-run the ten jobs. Record calls, tokens and success per job. Keep a change only if they moved the right way.
This is Post 3 in the "MCP Architecture" series. MCP as Discovery Protocol set the rule this post revises; Externalize the Knowledge applies it to knowledge. Related reading: MCP Tools People Actually Use for the parameter-sprawl lesson, MCP Isn't the Problem for the bloat critique, and Negotiated Integrations for what agents compose once the plumbing is gone.