Skip to content

Drop pydantic's auto-derived field titles from tool input schemas - #3392

Closed
wromansky wants to merge 1 commit into
modelcontextprotocol:mainfrom
wromansky:compact-tool-input-schemas
Closed

Drop pydantic's auto-derived field titles from tool input schemas#3392
wromansky wants to merge 1 commit into
modelcontextprotocol:mainfrom
wromansky:compact-tool-input-schemas

Conversation

@wromansky

Copy link
Copy Markdown

Fixes #3391

Drops pydantic's auto-derived title from every property of a tool's inputSchema. A parameter named exercise_id no 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/list is 43,710 bytes across 49 tools:

bytes share
tool descriptions (prose) 10,904 25%
auto-derived title keys (297) 8,333 19%
anyOf null-wrapping 1,869 4%

The 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_set is the supported hook, and func_metadata.py already 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?

  • Full suite green: 5805 passed, 8 skipped, 1 xfailed.
  • uv run pyright — 0 errors. uv run ruff check . — clean. ruff format applied.
  • New regression test test_tool_input_schema_omits_auto_derived_titles asserts all three cases in one place: an auto title is gone, an explicit Field(title=...) survives, and description / default / type are unchanged.
  • 12 existing expectations needed updating — mostly snapshot(...) in tests/docs_src/, refreshed with --inline-snapshot=fix, plus two hand-written dicts in tests/client/test_client.py and tests/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: inputSchema properties no longer carry auto-derived titles. No user code needs updating, and nothing in the MCP spec requires title. A client that displays title as 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I am assigned to the linked issue (or it is labeled help wanted, or I'm a maintainer)
  • I have disclosed any AI assistance and can explain the change in my own words
  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

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:

  • Output schemas (another 61 titles in the payload above), prompts, and resource templates, which generate titles the same way.
  • The anyOf: [{"type": "X"}, {"type": "null"}] spelling of optionals — a further 4%, but that is pydantic's output rather than a hook this SDK controls.
  • Tool descriptions are 25% of the payload and are not a target. In the server I measured they explain things a model genuinely cannot infer — one warns that a plank logged without reps_unit is 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.

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.
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

@github-actions github-actions Bot closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool input schemas carry a pydantic-derived title on every property

1 participant