Description
Every tool's inputSchema carries a title on every property, derived by pydantic from the field name. A parameter named exercise_id gets "title": "Exercise Id" — a restatement of the key it already sits under. Tool schemas are re-sent to the model on every request, so this is paid for in context on every turn.
I ran into this running a local 27B model against the wger MCP server, where context is genuinely scarce. Measuring its live tools/list:
|
bytes |
share |
| whole payload, 49 tools |
43,710 |
100% |
| tool descriptions (prose) |
10,904 |
25% |
auto-derived title keys (297 of them) |
8,333 |
19% |
anyOf null-wrapping on optionals |
1,869 |
4% |
My agent is granted 43 of those tools, which is ~11,100 tokens of schema against a 32k window — 42% of the context gone before the first message, once the system prompt is counted. Roughly 2,000 of those tokens are titles.
Reproduction — any tool at all:
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("demo")
@mcp.tool()
def log_set(exercise_id: str, reps: int) -> str:
"""Log a set."""
return "ok"
inputSchema.properties is:
{
"exercise_id": {"title": "Exercise Id", "type": "string"},
"reps": {"title": "Reps", "type": "integer"}
}
I'd expect the titles not to be there, since they add nothing a model can act on that the property name doesn't already say.
GenerateJsonSchema has a hook for exactly this — field_title_should_be_set — and Tool.from_function already passes a custom generator elsewhere in the file, so it's a small change. Suppressing the automatic titles leaves an explicit Field(title=...) intact, which seems like the right line to draw: an explicit title is the author's choice, an auto-derived one is a default nobody asked for.
Two things I'd want a maintainer's call on before this is worth doing:
- Default or opt-in. Changing the default updates 12 test expectations in this repo (mostly
snapshot(...) in tests/docs_src/), so it's visible. An opt-in flag on MCPServer(...) avoids that but adds public API.
- Scope. Output schemas, prompts and resource templates generate titles the same way. Output schemas alone are another 61 titles in the payload I measured. Worth doing together, or separately?
Happy to open a PR if it's useful — I have the change and the test updates working locally against main, full suite green. Equally happy to leave it if you'd rather write it yourselves.
Disclosure: I used an AI agent to take the measurements and draft the change. The problem is one I actually hit, and I've read and can explain the result.
References
Description
Every tool's
inputSchemacarries atitleon every property, derived by pydantic from the field name. A parameter namedexercise_idgets"title": "Exercise Id"— a restatement of the key it already sits under. Tool schemas are re-sent to the model on every request, so this is paid for in context on every turn.I ran into this running a local 27B model against the wger MCP server, where context is genuinely scarce. Measuring its live
tools/list:titlekeys (297 of them)anyOfnull-wrapping on optionalsMy agent is granted 43 of those tools, which is ~11,100 tokens of schema against a 32k window — 42% of the context gone before the first message, once the system prompt is counted. Roughly 2,000 of those tokens are titles.
Reproduction — any tool at all:
inputSchema.propertiesis:{ "exercise_id": {"title": "Exercise Id", "type": "string"}, "reps": {"title": "Reps", "type": "integer"} }I'd expect the titles not to be there, since they add nothing a model can act on that the property name doesn't already say.
GenerateJsonSchemahas a hook for exactly this —field_title_should_be_set— andTool.from_functionalready passes a custom generator elsewhere in the file, so it's a small change. Suppressing the automatic titles leaves an explicitField(title=...)intact, which seems like the right line to draw: an explicit title is the author's choice, an auto-derived one is a default nobody asked for.Two things I'd want a maintainer's call on before this is worth doing:
snapshot(...)intests/docs_src/), so it's visible. An opt-in flag onMCPServer(...)avoids that but adds public API.Happy to open a PR if it's useful — I have the change and the test updates working locally against
main, full suite green. Equally happy to leave it if you'd rather write it yourselves.Disclosure: I used an AI agent to take the measurements and draft the change. The problem is one I actually hit, and I've read and can explain the result.
References
field_title_should_be_set— https://docs.pydantic.dev/latest/api/json_schema/#pydantic.json_schema.GenerateJsonSchema.field_title_should_be_set