Skip to main content

Command Palette

Search for a command to run...

When the Spec Changes, the MCP Tool List Should Change With It

We hand-maintained a tools.json that matched our OpenAPI doc for about three weeks. Then someone renamed a parameter and the agent started sending the old name to an endpoint that no longer accepted it. The fix was not better discipline. It was letting the spec be the source of truth.

Updated
•4 min read•View as Markdown

The first MCP server we stood up, someone on the team wrote out the tool list by hand. A JSON file with 22 entries, each describing one endpoint we wanted the agent to reach. Names, descriptions, input schemas, output schemas. It felt fine. We read it in code review. We shipped it.

Three weeks later, the billing team renamed a query parameter from customer_id to customerId. They updated the OpenAPI doc, they updated the dashboard, they updated the SDK. They did not update our hand-written MCP tools.json, because nobody on that team knew the file existed.

For another week, the agent kept calling the endpoint with customer_id. The server returned 400 unknown query parameter. The agent retried. It tried the parameter in the body. It tried guessing the camelCase version. It never realized the schema it was given was stale, because the schema looked right — it had been correct three weeks ago. We found the failure in logs.

The second artifact problem

OpenAPI-to-MCP conversion is supposed to eliminate a translation step. Instead, the common setup reintroduces one:

  1. Your service has an OpenAPI doc. That is the contract your server actually implements.
  2. Someone hand-writes an MCP tools/list response that describes a subset of that doc.
  3. The agent calls the tool list it was given, not the spec.

You now have two artifacts that both claim to describe the same API. They will drift. The only question is how loudly.

Drift shows up in three flavors:

Renamed or removed parameters. The server stopped accepting customer_id, the MCP tool still lists it. The agent sends the old name, gets a 400, and improvises.

New endpoints that never make it into the tool list. The team ships POST /invoices/{id}/refund in the spec. The hand-written tools.json was never updated. The agent cannot reach the new capability even though it is one operationId away.

Schemas that lie about types. A field moved from string to number in the spec. The MCP tool still says string. The agent sends a quoted number, the parser coerces it, and you spend an afternoon debugging a validation error that should not exist.

Every one of these is a version of the same bug: the agent is being given a stale description of the system it is allowed to call.

What actually works

We stopped hand-writing the tool list. The gateway now reads the OpenAPI doc, derives the tool surface from it, and serves that. When the spec changes, we re-upload the doc — the tool list on the next client refresh reflects the new parameter names, the new operations, the new types.

The hand-maintained tools.json does not exist anymore. There is one artifact of truth, and it lives in the repo the service team already owns.

A few specifics from that setup:

  • The gateway parses the spec on upload. It lists every operation by operationId, method, path, and tag. Per-operation exposure is a toggle, not a hand-written allowlist — so when a new operation ships, it is off by default and we choose whether to turn it on.
  • Parameter names, required/optional flags, and types come straight from the spec. We do not copy them into a second file.
  • When the spec changes in a breaking way (a parameter removed, a type changed), the gateway shows the diff between the last uploaded revision and this one. We see the drift before the agent does.
  • Client snippets for Cursor, Claude, and MCP Inspector are regenerated from the same parsed doc, so the example in the doc matches the tool list in the client.

The mental model

The OpenAPI spec is not a document you generate once and forget. It is the contract between your service and whatever code calls it — including agents. The MCP tool list should be a view of that contract, not a separate document you maintain.

Every hand-maintained description you ship next to the spec is a place for drift to hide. The agent trusts the description it was given. It cannot tell that the description is three weeks out of date. That gap is not a prompt-engineering problem. It is an artifact-hygiene problem.

If your MCP server has a tools.json that someone on your team edits by hand, ask one question: when was it last reconciled with the spec? If you cannot answer from memory, you already know the answer.

This is the default behavior in Powerduck: upload the spec, the tool list is derived from it, and breaking changes between revisions show up as a diff before you ship.