Schema Drift Reaches the Tool Definition
Context layer series Everyone who sells a context layer talks about freshness. Fresh rows, streaming…
MCP tool schema design is the practice of writing a tool’s name, description, parameters, and return schema so an agent picks the right tool and calls it correctly on the first try. The schema is the one artifact the model reads on every single call, which makes it the most direct output of a context layer. A grounded schema carries real enums, disambiguated identifiers, units, scope, and guardrails, generated from your data rather than written by hand. This post shows where each piece comes from and what changes when you fix only the schema.
The evidence first. We have published three benchmarks this summer comparing our task-specific MCP servers with vendor-native ones for BigQuery, Google Ads and HubSpot. The pattern was the same each time. Fewer tokens, fewer calls, fewer turns, same or better answers.
People read those numbers and take away “task-specific beats general-purpose.” That is true. But it hides the mechanism. Task-specific servers win because the tool definitions carry the meaning the agent would otherwise have to discover by trial and error. The 39,000-token schema the HubSpot server resent on every turn was not context. It was a catalog. The agent still had to figure out which object held deals, which property was the close date, and what “closedwon” meant, and it did that with four discovery calls and 13 SQL queries.
The point of a context layer is not a knowledge graph diagram on an architecture slide. Its most direct output is a tool definition that is right. This post is about how that definition gets built, where each piece of it comes from, and what changes when you fix only the schema and nothing else.
Look at any trace where an agent calls a data tool badly and you find the failure in the tool definition before the first call.
"id": { "type": "string" }
Which id? The CRM account ID, the billing account number, and the canonical customer key are all “id” somewhere.
"status": { "type": "string" }
The real values are six enums. One is deprecated. One triggers a workflow.
"amt": 13756172
No unit, no currency, no note that it is net of discounts.
"description": "Gets deals."
The description restates the function name and tells the agent nothing about scope or when to use it.
"close_date": { "type": "string" }
No format, so the agent tries ISO 8601, then epoch, then “last quarter.”
returns first 100 rows
Pagination is not mentioned, so the agent treats the first page as all rows.
None of these are model failures. The model did what the definition allowed. The definition allowed too much.
The general advice for fixing this exists and it is fine: write rich descriptions, use enums, add constraints, keep tool count low. AWS, Anthropic and half a dozen others have written that post. We are not going to repeat it. The question we care about is different. Where does the content of a good description come from at enterprise scale, and how do you keep it right when you have hundreds of sources and nobody is going to hand write 4,000 tool schemas?
The name says what business question the tool answers, not which table it hits. The description states the purpose, the scope (time window, filters already applied), the units of anything numeric, and any consequence. Parameters use business names, carry enums drawn from real values, mark deprecated values, disambiguate identifiers, and state formats. The return schema names every field and says what it means and what unit it is in. Guardrails (read only, row limits, allowed filters) are declared, not implied.
Compare a typical general-purpose CRM tool with one of the tools from our HubSpot benchmark server.
{
"name": "search_crm_objects",
"description": "Search CRM objects by type with optional filters and properties.",
"inputSchema": {
"type": "object",
"properties": {
"objectType": { "type": "string" },
"filterGroups": { "type": "array" },
"properties": { "type": "array", "items": { "type": "string" } },
"limit": { "type": "integer" },
"after": { "type": "string" }
},
"required": ["objectType"]
}
}
{
"name": "win_loss_summary_90d",
"description": "Summarize closed deals over the trailing 90 days: count, total and average amount, and win rate, split by won and lost. Amounts are in USD, net of discounts, from the deal amount property at close. Excludes deals in open stages. Use this first for any win/loss question, then drill in with close_reason_breakdown_90d or deal_size_trend_90d.",
"inputSchema": {
"type": "object",
"properties": {
"pipeline": {
"type": "string",
"enum": ["default", "enterprise", "partner"],
"description": "HubSpot pipeline. Omit for all pipelines."
},
"as_of": {
"type": "string",
"format": "date",
"description": "End of the 90 day window, YYYY-MM-DD. Defaults to today in UTC."
}
}
},
"outputSchema": {
"type": "object",
"properties": {
"closed_deals": { "type": "integer", "description": "Deals with a close date inside the window and stage in {closedwon, closedlost}." },
"won": { "type": "integer" },
"lost": { "type": "integer" },
"win_rate": { "type": "number", "description": "won / closed_deals, 0 to 1." },
"won_amount_usd": { "type": "number" },
"lost_amount_usd": { "type": "number" },
"avg_won_amount_usd": { "type": "number" },
"avg_lost_amount_usd": { "type": "number" }
}
},
"annotations": { "readOnlyHint": true }
}
The first tool is a search API with a JSON wrapper. The second is an answer with a contract. The agent calling the second one does not discover anything. It already knows what “closed” means, what currency the amounts are in, and where to go next.
Nobody wrote it. It was assembled. Every element of a grounded tool schema has a source inside the enterprise, and a context layer’s job is to pull from each source and put the result in the right slot. This is what Helix does behind MCP Studio, and it is the part we have described least, so here it is concretely.
acct_st in {A, I, P, C} into account_status in {ACTIVE, INACTIVE, PENDING, CLOSED}.The connector reads the source schema for types and nullability. That is the easy part. The useful part comes from sampling. Sample 10,000 rows of the deals object and you learn that dealstage has exactly nine distinct values, that two of them have not appeared in eighteen months, that closedate is always midnight UTC, and that amount is never negative. The enum in the tool comes from observed values, not from the API docs, because the API docs say “string.”
Every enterprise has four customer IDs. Lineage across the connected systems shows which one the others map to. The same graph shows that arr_usd on the CRM account is a nightly copy from billing, so the tool marks it read only and points at the source of truth. Without lineage the tool definition cannot say either of those things and the agent guesses.
“Win rate” means something specific at your company. Those definitions live in a glossary, a metrics doc, a Confluence page, or a slide deck. Helix ingests them, ties each definition to the fields and tools it governs, and the definition lands in the tool description. It does not wait for someone to notice a change and hand edit forty servers.
For the long tail of SaaS sources, the OpenAPI spec is often the only documentation. It carries the parameter constraints, the pagination model, the rate limits, and sometimes the enums. It also tells you which endpoints mutate state, which is how a read-only server knows what to exclude.
This is the source most people skip. Every tool call an agent makes, with its parameters and its outcome, is data. Over a few hundred runs you learn that filtering by close_date and stage together returns in 200ms while filtering by owner alone times out, that agents keep passing the CRM ID where the canonical ID is expected, and that a particular parameter combination always returns zero rows. Each of those becomes a line in a description, a tighter enum, or a renamed parameter. The tool gets better because it was used, and the improvement is written back into the definition, not left as tribal knowledge in a Slack thread.
Who is allowed to call the tool, which rows they see, which columns are masked, whether the tool is read only, and what happens on a write. These come from the platform’s access policies and land in the tool as annotations and in the gateway as enforcement.
Put those six together and you get the second schema above. Pick any slot in the schema below and it tells you which source filled it.
Source: schema plus samples
The connector sampled the pipeline property across 10,000 deals and found exactly three values in use. The API docs say “string.” The enum comes from what is actually there, and a value that stops appearing gets marked deprecated on the next regeneration.
Source: lineage
Four systems have a customer identifier. Lineage shows the CRM account ID and the billing account number both map to cust_id_v2. The tool names that one and warns the agent away from the other two in the parameter description.
Source: business docs and glossary
The sales ops glossary defines “closed” as stage in {closedwon, closedlost} with a close date inside the window. That sentence is tied to the dealstage and closedate fields, so it lands in the description and in the output field definition. Change the glossary and the description changes.
Source: API spec
The OpenAPI spec says closedate is an ISO date, results page with a cursor, and a page holds at most 200 rows. Those become the format, the pagination note, and the row limit in guardrails. The spec also marks which endpoints write, which is how a read-only server knows what to leave out.
Source: execution logs
Across a few hundred runs, agents that called the summary first answered in two turns. Agents that started with the raw rows tool took six. That pattern becomes one sentence in the description, and the sentence is checked again the next time the logs are reviewed.
Source: policy
The platform’s access policy marks this tool read only and scopes rows by the caller’s team. The annotation tells the model. The gateway enforces it. The model cannot talk its way past a policy it can only read.
The generation runs in one direction and the feedback runs in the other. Watch the flow.
Sources
Schemas, samples, lineage, docs, specs, logs, policy
Helix
Ties each fact to a field and a tool slot
Tool definition
Name, description, params, output, guardrails, version
Agent
Reads it on every call. Its calls become the next logs.
Purple dot: generation, sources to agent. Green dot: execution history flowing back into Helix.
There is a tension here. If every tool description carries the full data dictionary, you are back to the 39,000-token schema. The description has to carry meaning, not the whole catalog.
The way we handle it is progressive disclosure. The tool description carries what the agent needs to choose the tool and call it correctly: purpose, scope, units, the enums that matter, the identifier disambiguation, the guardrails. Anything deeper (the full column list, the sample values, the lineage graph) is available through the Agentic Probe when the agent asks for it.
Same information reachable. Only the relevant part sent by default. A good test: read the description without seeing the backend and ask whether you, a human analyst, would know what you were about to get. If yes, it is grounded. If you would need to run it once to find out, it is not.
The claim in this post is that the schema carries the win, not the backend. So we tested it that way. We took [TASK FROM THE HUBSPOT OR GOOGLE ADS BENCHMARK] and held the backend fixed. Same connector, same query macros, same data. The only variable was the tool definitions. In one run the agent saw the bare definitions: minimal descriptions, no enums, no units, no output schema. In the other it saw the grounded ones generated as described above.
| Metric | Bare schema | Grounded schema |
|---|---|---|
| Tool calls | fill in | fill in |
| Agent turns | fill in | fill in |
| Total tokens | fill in | fill in |
| Correct answer | fill in | fill in |
| Clarifying questions asked | fill in | fill in |
| Wrong parameter attempts | fill in | fill in |
[Two to three sentences on the result. Expected direction: fewer calls, fewer wrong-parameter attempts, fewer clarifying questions, same or better accuracy, on the same backend. Report honestly if any metric did not move.]
The backend did not get smarter. The agent did not get smarter. The tool told the truth about itself and the agent stopped guessing.
Generating definitions from sources beats hand writing them. It also fails in predictable ways. We have made every one of these.
1
Enums from the docs instead of the data
The API doc lists eleven status values. Nine of them have never appeared in your tenant. Ship all eleven and the agent spends turns filtering on values that return nothing. Build enums from observed values, keep the doc list as a fallback for new values.
2
Over describing
If the description is the data dictionary, you are back to the 39,000-token schema, just formatted nicely. A description is for choosing the tool and calling it correctly. Put the rest behind the probe.
3
Generating once
A definition generated in March and never touched is a hand written definition with extra steps. Regeneration has to be tied to source change, which means schema fingerprints, glossary hooks, and a version on every tool. That is the next post.
4
Business names that nobody uses
Renaming acct_st to account_status is right. Renaming it to customer_lifecycle_state_indicator because the glossary says so is wrong. Use the name people say out loud in meetings. Execution logs tell you which names agents and users actually reach for.
5
No output schema
Teams spend all their effort on parameters and return an untyped blob. The agent then has to guess what won_amount_usd means, which is the same failure in the other direction. Every output field gets a name, a type, a unit, and one sentence.
Everything above is about read tools, because that is what the benchmarks measured. The same six sources produce write tools, with two additions: application validation rules become preconditions, and consequence metadata (what downstream systems react to this field) becomes part of the description and the dry run response. The first post in this series, Read Context Is Solved. Write Context Is Not., walks through one write tool built both ways. The short version: a write tool whose schema does not carry the enum, the precondition, and the consequence is a bug waiting for a retry.
You do not need a context layer to start. You need an afternoon and your tool list.
Most teams that do this find the same thing we did. The fix was never the model. It was a JSON schema that told the truth.
Copy this. Fill it from your sources, not from your head.
{
"name": "<business_question_in_snake_case>",
"description": "<What question this answers>. <Scope: window, filters, exclusions>. <Units and currency for anything numeric>. <Consequence if it writes>. <When to use it versus its siblings>.",
"inputSchema": {
"type": "object",
"properties": {
"<param>": {
"type": "<type>",
"enum": ["<observed values only>"],
"format": "<date | date-time | uuid | ...>",
"description": "<Business meaning. Which identifier. Deprecated values. Default.>"
}
},
"required": ["<only what is truly required>"]
},
"outputSchema": {
"type": "object",
"properties": {
"<field>": { "type": "<type>", "description": "<Meaning. Unit. How computed.>" }
}
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true
},
"x-context": {
"source_of_truth": "<system.object.field>",
"canonical_ids": { "<param>": "<id system>" },
"definitions": ["<glossary term ids>"],
"limits": { "max_rows": 200, "pagination": "cursor" },
"generated_from": ["schema", "samples", "lineage", "glossary", "openapi", "execution_logs"],
"version": "<semver>"
}
}
The x-context block is not part of the MCP spec. It is where we record provenance for the schema itself: what fed it and which version it is. That matters later, when the source changes and the tool has to change with it. That is the subject of the next post in this series.
Number ten is the one that separates a hand-written schema from a generated one. If you cannot say where a sentence came from, you cannot keep it true.

What is MCP tool schema design?
It is the practice of writing a tool’s name, description, input parameters, and output schema so an agent selects the right tool and calls it correctly. Good schemas carry business names, real enums, identifier disambiguation, units, formats, and declared guardrails.
What makes a good MCP tool description?
The first two sentences state what question the tool answers, its scope, and the units of anything numeric. Then it says when to use this tool versus its siblings. If a human analyst can read it and know what they are about to get, it is grounded.
Should MCP tool parameters use enums?
Yes, wherever the set of valid values is finite. Build the enum from observed values in your data, not from the API documentation, and mark deprecated values. A parameter typed as string invites the agent to guess.
How many tokens should a tool schema use?
Enough to choose the tool and call it correctly, no more. In our HubSpot benchmark the task-specific server sent about 1,200 schema tokens against roughly 39,000 for the native server, and reached the same answer in a third of the calls. Deeper detail belongs behind on-demand lookup, not in every description.
Can MCP tool schemas be generated automatically?
Yes. Every element has a source: types and enums from schema plus samples, canonical identifiers and derived fields from lineage, definitions from the business glossary, limits from API specs, usage guidance from execution logs, and guardrails from policy. That is what Helix does behind MCP Studio, and it is what keeps 4,000 tool definitions true.
What is progressive disclosure in MCP?
The description carries only what the agent needs to choose and call the tool. Anything deeper, like the full column list or sample values, stays available through a probe the agent queries on demand. Same information reachable, only the relevant part sent by default.
The benchmarks showed that task-specific servers win. This is why. The tool definition is where enterprise semantics meet the model, and it is the one artifact the model reads on every single call. A context layer that cannot produce a correct tool definition from your schemas, samples, lineage, docs, specs and history is a context layer that leaves the last mile to the model. That mile is where the errors live.
Request early access. Bring the tool that your agent keeps calling wrong.
Read next in this series: Read Context Is Solved. Write Context Is Not., Schema Drift Reaches the Tool Definition, How to Evaluate a Context Layer, and Whose Identity Is Calling? Evidence: the HubSpot benchmark and task-specific vs system-specific MCP servers.
Context layer series Everyone who sells a context layer talks about freshness. Fresh rows, streaming…
Context layer series Two MCP servers sit in front of the same warehouse. You ask…
Context layer series Ask “how many active customers do we have in EMEA” in Claude…