Drop pydantic's auto-derived field titles from tool input schemas - #3392
Drop pydantic's auto-derived field titles from tool input schemas#3392wromansky wants to merge 1 commit into
Conversation
Pydantic titles every field by title-casing its name, so a parameter named exercise_id gains "title": "Exercise Id" - a restatement of the key it already sits under. Tool schemas go to the model on every request, so that repetition is paid for in context on every turn and carries nothing the field name does not. Measured against a real server: the wger MCP server's tools/list is 43,710 bytes across 49 tools, of which 8,333 bytes - 19% - is 297 auto-derived title keys. For an agent granted 43 of those tools on a 32k-context local model, dropping them returns about 2,000 tokens per request. NoAutoTitleJsonSchema suppresses only the automatic titles. A title set explicitly through Field(title=...) is the author's choice and is left alone, as are descriptions and every constraint. Scoped to tool input schemas. Output schemas, prompts and resource templates generate titles the same way and are deliberately left for a separate change.
|
This PR has been closed automatically. It's still a draft, but we close those early so you don't put in more time only to have it closed the moment you mark it ready. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3391. If a maintainer assigns you to #3391, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Fixes #3391
Drops pydantic's auto-derived
titlefrom every property of a tool'sinputSchema. A parameter namedexercise_idno longer carries"title": "Exercise Id".Draft on purpose: #3391 asks two open questions — default vs opt-in, and whether to cover output schemas, prompts and resource templates in the same change. This branch is the smallest version that answers "yes to the default, tools only", so there is something concrete to react to. Say the word and I will reshape it.
Motivation and Context
Tool schemas are re-sent to the model on every request, so anything in them is paid for in context on every turn. Pydantic titles each field by title-casing its name, which restates the key it already sits under and gives a model nothing to act on.
Measured against a live server — the wger MCP server, whose
tools/listis 43,710 bytes across 49 tools:titlekeys (297)anyOfnull-wrappingThe agent that prompted this is granted 43 of those tools and runs on a 32k-context local model: ~11,100 tokens of schema, 42% of the window gone before the first message. This returns about 2,000 of those tokens per request.
GenerateJsonSchema.field_title_should_be_setis the supported hook, andfunc_metadata.pyalready defines a custom generator (StrictJsonSchema) for the output path, so the pattern is established.An explicit
Field(title=...)is preserved. That is the line this draws: an explicit title is the author's choice; an auto-derived one is a default nobody asked for. Descriptions and constraints are untouched.How Has This Been Tested?
5805 passed, 8 skipped, 1 xfailed.uv run pyright— 0 errors.uv run ruff check .— clean.ruff formatapplied.test_tool_input_schema_omits_auto_derived_titlesasserts all three cases in one place: an auto title is gone, an explicitField(title=...)survives, anddescription/default/typeare unchanged.snapshot(...)intests/docs_src/, refreshed with--inline-snapshot=fix, plus two hand-written dicts intests/client/test_client.pyandtests/docs_src/test_progress.py. That churn is the honest cost of changing the default and is the main thing to weigh.Breaking Changes
The wire format changes:
inputSchemaproperties no longer carry auto-derived titles. No user code needs updating, and nothing in the MCP spec requirestitle. A client that displaystitleas a form label would fall back to the property name — that is the risk worth a maintainer's judgement, and the reason for the default-vs-opt-in question in #3391.Types of changes
Checklist
help wanted, or I'm a maintainer)Additional context
Not assigned to #3391 yet — I understand this stays closed until a maintainer picks it up, and I will push updates as new commits rather than force-pushing so it can reopen.
Deliberately left out, all measurable and all better as separate decisions:
anyOf: [{"type": "X"}, {"type": "null"}]spelling of optionals — a further 4%, but that is pydantic's output rather than a hook this SDK controls.reps_unitis stored as 60 repetitions rather than 60 seconds, unrecoverably. That is the schema earning its tokens.Disclosure: I used an AI agent to take the measurements and draft this change. The problem is one I hit in my own deployment, and I have read and can explain the result.