Expose Three Operations, Not the Whole OpenAPI Spec
The first time we wired billing to an agent, we converted the whole OpenAPI document. Every path, every method, every tag. 87 tools showed up in the client. It felt generous — look at all the things the agent can do now.
Two weeks later, a support engineer asked the agent to "summarize the account for customer #4471." The agent picked POST /internal/impersonate instead of GET /customers/4471. It was a longer name, it was listed right next to the read endpoint, and the model picked the one that returned a richer object. We caught it in logs. We were lucky.
That is the default footgun of OpenAPI-to-MCP conversion. The spec describes everything the service can do. The agent does not need everything the service can do. It needs the slice that is safe for it to reach for on a Tuesday afternoon.
Why the whole spec is too much
Three things compound:
The model picks from the tool list, not from your intent. When you say "look up customer 4471," the client hands the model a list of tool names and descriptions. GET /customers/{id} and POST /internal/impersonate are both on the list. The model's choice depends on how similar the names look, how long the descriptions are, and whether it has seen the pattern in training. You are one ambiguous prompt away from the wrong tool.
Mutating and destructive endpoints look just like read ones in the list. GET /invoices and DELETE /invoices/{id} are two rows in the same table. The MCP annotations (readOnlyHint, destructiveHint, idempotentHint) help, but they are hints, not guards. A model that is asked to "clean up the test invoices" can and will pick the destructive one.
Internal and admin operations live in the same spec. Billing's OpenAPI doc has /internal/impersonate, /internal/tenant-delete, /admin/replay-webhook. Those exist for our own staff dashboards. They are not for the agent.
The gate we actually use
We expose per-operation, not per-document. The workflow is:
- Upload the OpenAPI spec. The gateway parses it and lists every operation with its method, path, tag, and
operationId. - Start with everything disabled. Do not flip the default to "all on."
- Turn on the read-only slice the agent legitimately needs:
GET /invoices,GET /customers/{id},GET /subscriptions. Maybe a couple of idempotent POSTs for support actions. - Leave mutations off until a specific use case demands them, and when you add one, attach a narrower credential than the one you use for reads.
- Re-upload the spec when it changes; the per-operation toggles stick by
operationId, so a new endpoint ships off by default.
The tool list the model sees shrank from 87 to 9. Impersonate is gone. Tenant-delete is gone. The agent's pick rate for the right tool went from "we review every call" to "we sample weekly."
What to leave off, even when it seems obvious
- Anything under
/internal/,/admin/, or/debug/. Those paths exist for humans with dashboards, not for agents. - Anything that takes a free-text body and mutates production:
POST /users,PATCH /tenants,POST /refunds. If you must expose one, gate it behind a separate access key and a shorter timeout. - Anything that returns bulk data:
GET /export/all-customers. The agent will pull it, put it in context, and you will regret the token cost and the privacy exposure. - Anything that is not in the spec's own tags. If the operation has no tag, it is usually someone's one-off.
The mental model
Think of the OpenAPI spec as the library and the exposed operation list as the bookshelf. The library has 87 books. The bookshelf should hold the 9 the agent is allowed to pull down without asking. Re-uploading the spec adds new books to the library; it does not automatically put them on the shelf.
When you give the agent the whole library, you are not giving it more capability. You are giving it more ways to be wrong. The unit of safety in MCP is not "the spec is accurate." It is "the tool list is short enough that the right choice is the obvious choice."
We built the per-operation toggle into Powerduck precisely for this: upload the spec, uncheck the operations you do not want reachable, ship the MCP URL.