Skip to content

Commit ea2ffd0

Browse files
committed
fix(mcpserver): omit pydantic auto-derived titles on tool input schemas
Tool inputSchema was paying Title Case restatements of every property key on every tools/list. Use ToolInputJsonSchema so auto titles are dropped while explicit Field(title=...) remains. Output schemas unchanged (#3391).
1 parent 9972c21 commit ea2ffd0

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

src/mcp/server/mcpserver/tools/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
returns_input_required,
2222
)
2323
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
24-
from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, func_metadata
24+
from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, ToolInputJsonSchema, func_metadata
2525
from mcp.shared._callable_inspection import is_async_callable
2626
from mcp.shared.exceptions import MCPError
2727
from mcp.shared.tool_name_validation import validate_and_warn_tool_name
@@ -103,7 +103,10 @@ def from_function(
103103
skip_names=skip_names,
104104
structured_output=structured_output,
105105
)
106-
parameters = func_arg_metadata.arg_model.model_json_schema(by_alias=True)
106+
parameters = func_arg_metadata.arg_model.model_json_schema(
107+
by_alias=True,
108+
schema_generator=ToolInputJsonSchema,
109+
)
107110

108111
# Match `model_dump_one_level`'s kwarg keys (alias when present, else field name)
109112
# so a by-name resolver param resolves to a key that exists at call time.

src/mcp/server/mcpserver/utilities/func_metadata.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
7474
raise ValueError(f"JSON schema warning: {kind} - {detail}")
7575

7676

77+
class ToolInputJsonSchema(StrictJsonSchema):
78+
"""JSON schema for tool *input* parameters without pydantic auto-titles.
79+
80+
Auto-derived titles (Title Case of the field name) restate the property key and
81+
waste context on every ``tools/list``. Explicit ``Field(title=...)`` / WithJsonSchema
82+
titles still appear on the wire. See #3391.
83+
"""
84+
85+
def field_title_should_be_set(self, schema) -> bool: # type: ignore[no-untyped-def]
86+
return False
87+
88+
7789
_LOCAL_DEFS_PREFIX = "#/$defs/"
7890

7991

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Regression: tool input schemas omit pydantic auto-derived property titles (#3391)."""
2+
3+
from pydantic import Field
4+
5+
from mcp.server.mcpserver.tools.base import Tool
6+
7+
8+
def test_tool_input_schema_omits_auto_derived_property_titles():
9+
def log_set(exercise_id: str, reps: int) -> str:
10+
"""Log a set."""
11+
return "ok"
12+
13+
tool = Tool.from_function(log_set)
14+
props = tool.parameters["properties"]
15+
assert "title" not in props["exercise_id"]
16+
assert "title" not in props["reps"]
17+
assert props["exercise_id"]["type"] == "string"
18+
assert props["reps"]["type"] == "integer"
19+
20+
21+
def test_tool_input_schema_keeps_explicit_field_title():
22+
def greet(name: str = Field(title="Preferred Name")) -> str:
23+
"""Greet someone."""
24+
return f"hi {name}"
25+
26+
tool = Tool.from_function(greet)
27+
props = tool.parameters["properties"]
28+
assert props["name"]["title"] == "Preferred Name"

0 commit comments

Comments
 (0)