Bridge camelCase ToolAnnotations reads (#4597)

🤖 Generated with Codex
This commit is contained in:
nate nowack 2026-07-22 13:55:41 -05:00 committed by GitHub
commit 8efa405833
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 23 additions and 3 deletions

View file

@ -6,7 +6,7 @@ This is the complete register of user-facing changes from the MCP Python SDK v2
Each entry is tagged **Absorbed** (public surface unchanged), **Bridged** (shim keeps old code working, usually warning), **Breaking** (user code must change), or **Deprecated** (works, warns, slated for removal). See the [overview](/development/v4-notes/index) for what each disposition means.
**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures are the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction. Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 25 `_ALIASES` bridge entries warn correctly with actionable messages.
**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures are the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction. Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 29 `_ALIASES` bridge entries warn correctly with actionable messages.
## Environment
@ -68,7 +68,7 @@ async def read_schema():
return tools[0].inputSchema # works, warns; prefer .input_schema
```
The bridged fields are exactly those users read, data-driven from an `_ALIASES` table: `inputSchema`/`outputSchema` (Tool); `mimeType` (Resource, ResourceTemplate, TextResourceContents, BlobResourceContents, ImageContent, AudioContent) and `uriTemplate` (ResourceTemplate); `isError`/`structuredContent` (CallToolResult); `hasMore` (Completion); `serverInfo`/`protocolVersion` (InitializeResult); `nextCursor`/`resourceTemplates` (List\*Result); `systemPrompt`/`maxTokens`/`stopSequences`/`modelPreferences`/`toolChoice` (CreateMessageRequestParams); `requestedSchema` (ElicitRequestFormParams). WS2 verified all 25 alias entries warn correctly with actionable messages.
The bridged fields are exactly those users read, data-driven from an `_ALIASES` table: `inputSchema`/`outputSchema` (Tool); `readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint` (ToolAnnotations); `mimeType` (Resource, ResourceTemplate, TextResourceContents, BlobResourceContents, ImageContent, AudioContent) and `uriTemplate` (ResourceTemplate); `isError`/`structuredContent` (CallToolResult); `hasMore` (Completion); `serverInfo`/`protocolVersion` (InitializeResult); `nextCursor`/`resourceTemplates` (List\*Result); `systemPrompt`/`maxTokens`/`stopSequences`/`modelPreferences`/`toolChoice` (CreateMessageRequestParams); `requestedSchema` (ElicitRequestFormParams). WS2 verified all 29 alias entries warn correctly with actionable messages.
*Verify:* `fastmcp_slim/fastmcp/_compat.py` (the `_ALIASES` table and `install()`).

View file

@ -31,7 +31,7 @@ async with Client("my_mcp_server.py") as client:
schema = tools[0].inputSchema # still works, warns once
```
Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools, `mimeType` on resources and content, `isError`/`structuredContent` on tool results, `nextCursor` on paginated results, `serverInfo`/`protocolVersion` on the initialize result, the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`), and `requestedSchema` on elicitation parameters.
Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools; `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` on tool annotations; `mimeType` on resources and content; `isError`/`structuredContent` on tool results; `nextCursor` on paginated results; `serverInfo`/`protocolVersion` on the initialize result; the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`); and `requestedSchema` on elicitation parameters.
The bridge is controlled by the `mcp_camelcase_compat` setting, which defaults to on. Set it to `False` (or the environment variable `FASTMCP_MCP_CAMELCASE_COMPAT=false`) to turn the shims off, in which case only the snake_case names resolve:

View file

@ -42,6 +42,12 @@ _ALIASES: dict[type, dict[str, str]] = {
"inputSchema": "input_schema",
"outputSchema": "output_schema",
},
mcp_types.ToolAnnotations: {
"readOnlyHint": "read_only_hint",
"destructiveHint": "destructive_hint",
"idempotentHint": "idempotent_hint",
"openWorldHint": "open_world_hint",
},
mcp_types.Resource: {
"mimeType": "mime_type",
},

View file

@ -51,6 +51,20 @@ class TestCamelCaseBridge:
with pytest.warns(FastMCPDeprecationWarning):
assert tool.outputSchema == {"type": "string"} # ty: ignore[unresolved-attribute]
@pytest.mark.parametrize(
("camel", "snake", "value"),
[
("readOnlyHint", "read_only_hint", True),
("destructiveHint", "destructive_hint", False),
("idempotentHint", "idempotent_hint", True),
("openWorldHint", "open_world_hint", False),
],
)
def test_tool_annotations_bridged(self, camel, snake, value):
annotations = mcp_types.ToolAnnotations(**{snake: value})
with pytest.warns(FastMCPDeprecationWarning):
assert getattr(annotations, camel) is value
def test_call_tool_result_is_error_bridged(self):
result = mcp_types.CallToolResult(content=[], is_error=True)
with pytest.warns(FastMCPDeprecationWarning):