{"slug": "tool-descriptions-are-the-contract", "title": "Tool Descriptions Are the Contract", "summary": "A developer's MCP-vs-REST sample repo shows that tool descriptions in MCP servers function as the API contract, with prose drift already appearing across two hand-written descriptions of the same restaurant search tool. The writeup argues that provenance sentences — telling an agent where a value like a restaurant ID comes from — are the highest-value part of a tool description and cannot be expressed by a JSON schema, while verbose ceremony should be cut to save tokens on every model call.", "body_md": "This series started with a claim: MCP and REST are two doors into the same kitchen. Three posts later, the comments have pushed the argument down to its foundation. One reader on dev.to said the useful test is whether both doors preserve the same auth, error, and idempotency behavior. Another warned about granularity drift and asked how anyone validates tool descriptions. Both are pointing at the same uncomfortable fact: in an MCP server, prose is load-bearing. The description decides whether the agent picks your tool. The parameter text decides whether it calls the tool correctly. The error message decides whether it recovers. That prose is the API contract now, and almost nobody treats it like one.\n\nHere is what I did not expect to find in my own [sample repo](https://github.com/steefjan1/mcp-vs-api-azure-functions). The restaurant backend describes its behavior in prose three times. The Functions MCP door carries descriptions in C# attributes:\n\n```\n[McpToolTrigger(\"search_restaurants\",\n    \"Searches the restaurant directory. Both filters are optional; call it without arguments to list every restaurant.\")]\n```\n\nThe APIM REST door carries operation descriptions in Bicep. And the third pattern from the [APIM post](https://dev.to/steefjan_wiggers_34a415b), where the gateway manufactures an MCP server from the REST API, carries its own tool descriptions in Bicep too:\n\n```\ndescription: 'Searches the restaurant directory. Both cuisine and city filters are optional; call without arguments to list every restaurant.'\n```\n\nRead those two closely. They are not the same sentence. I wrote them two days apart, and they drifted: \"Both filters\" became \"Both cuisine and city filters\", \"call it without\" became \"call without\". Harmless here. But this is a three-tool sample maintained by one person for one week, and the copies already disagree. Scale that to fifty tools, three teams, and a year of changes, and the gateway's MCP door describes a backend that no longer behaves the way its C# door says it does. That is the granularity drift the commenter meant, in miniature: the contract forks the moment it exists in two files, and nothing in the toolchain notices.\n\nThe REST door gets away with prose drift because its real contract is elsewhere: routes, status codes, an OpenAPI document that tooling can diff. The MCP doors have no elsewhere. The description is the interface.\n\nThe [measurement post](https://dev.to/steefjan_wiggers_34a415b/what-the-agent-pays-for-discovery-221a) put numbers on the cost side: a realistic description runs about 111 tokens per tool, and a verbose one 208, paid on every model call. So the budget question is what each sentence buys. The sample's descriptions follow three rules, and each rule earns its tokens differently.\n\nSay what the tool does, in one sentence, in terms that distinguish it from its neighbors. Say when to call it, which is mostly about optionality: \"both filters are optional; call it without arguments to list every restaurant\" prevents the agent from inventing a filter just to have one. And say where values come from: \"the restaurant id, as returned by search_restaurants (for example 'r1')\". That last rule is the one a JSON schema cannot express. A schema says restaurantId is a required string. Only prose says the string must come from a previous search_restaurants call and must not be guessed. Provenance is the highest-value sentence in the whole contract, and it is the difference between an agent that chains tools and one that hallucinates identifiers.\n\nThe verbose tier from the measurement post doubles the bill mostly with ceremony the model would infer anyway. Spend on disambiguation and provenance, cut the rest.\n\nThe commenter's test, applied to the sample: auth and idempotency live in the kitchen, in the shared service that both doors call, so neither door can drift on what is allowed or what happens on a retry. Error rendering is the one thing the doors deliberately do differently. The REST door returns machine-shaped errors, a 4xx status with a small JSON body, because its caller is code and code branches on status. The MCP door returns instructions:\n\n```\nreturn menu is null\n    ? $\"Restaurant '{restaurantId}' was not found. Use search_restaurants first to get a valid id.\"\n    : JsonSerializer.Serialize(menu, JsonOptions);\n```\n\nSame fact, two renderings. That passes the parity test rather than failing it: parity means the doors agree on the decision, not that they speak the same dialect. The kitchen decides the order is invalid; each door reports it in the shape its caller can act on.\n\nThose recovery sentences do a second job that I have not seen anyone talk about. They are constants, so every time one is returned, you know exactly which misunderstanding just happened. \"Use search_restaurants first to get a valid id\" fires when an agent invented a restaurant id, which means the provenance sentence in get_menu's description did not land. Add one log line where each recovery string is returned and Application Insights turns into a description quality dashboard: a KQL query grouping by sentinel string, per tool, per client. A rising count on one string is not an outage. It is a failing sentence in your contract, located precisely. I know of no cheaper contract test than instrumenting the errors you already wrote.\n\nWhich is the honest answer to the commenter's validation question: start by treating descriptions as artifacts that CI can check without any LLM. Lint that every identifier parameter states its provenance. Lint that every tool description contains a when-to-call clause. And budget-gate the whole array: the token harness from the measurement post lives in the repo under measure/, and pointing it at your tools array in CI turns \"our context bill crept up\" into a failing build with a number in it. Drift between copies is checkable the same way; if the gateway's Bicep descriptions and the code's attribute descriptions are both machine-readable, a test can diff them.\n\nWhat CI cannot check is whether the model picks the right tool, and I will not pretend otherwise. That needs the deferred experiment: scripted tasks with known correct tool sequences, run repeatedly against description variants. A commenter on the measurement post sharpened how to score it: as a two-stage system. Stage one is recall, whether the correct tool made it into the shortlist that deferred loading or retrieval produced. Stage two is selection, whether the model chose it from that shortlist and called it correctly. The stages fail differently: a recall miss is a retrieval or granularity problem, a selection miss is a description problem. And recall misses need their own headline number, because they fail silently; a cheap context with the right tool absent is the worst outcome, and the model will improvise something plausible instead of erroring. The measurement harness was structured so that this eval slots in on top, and it is the next piece of this series with numbers in it.\n\nThe description is the interface. So give it the treatment interfaces get: one authoritative copy where you can manage it, review on every change, a token budget enforced in CI, provenance sentences on every identifier, and telemetry on the error strings that reveal where the contract fails. The doors were never the hard part. The words were.\n\nThe sample, the APIM patterns, and the token harness are all in the [repo](https://github.com/steefjan1/mcp-vs-api-azure-functions).", "url": "https://wpnews.pro/news/tool-descriptions-are-the-contract", "canonical_source": "https://dev.to/steefjan_wiggers_34a415b/tool-descriptions-are-the-contract-5488", "published_at": "2026-09-17 07:37:43+00:00", "updated_at": "2026-09-17 07:53:39.163778+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "developer-tools"], "entities": ["MCP", "REST", "Azure Functions", "APIM", "OpenAPI", "dev.to", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/tool-descriptions-are-the-contract", "markdown": "https://wpnews.pro/news/tool-descriptions-are-the-contract.md", "text": "https://wpnews.pro/news/tool-descriptions-are-the-contract.txt", "jsonld": "https://wpnews.pro/news/tool-descriptions-are-the-contract.jsonld"}}