diff --git a/docs/development/v4-notes/change-register.mdx b/docs/development/v4-notes/change-register.mdx index 22c0ce33b..b5ae7a77b 100644 --- a/docs/development/v4-notes/change-register.mdx +++ b/docs/development/v4-notes/change-register.mdx @@ -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()`). diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx index dfe13c7b1..a3fd233fd 100644 --- a/docs/getting-started/upgrading/from-fastmcp-3.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx @@ -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: diff --git a/fastmcp_slim/fastmcp/_compat.py b/fastmcp_slim/fastmcp/_compat.py index f5fcc7f81..402b8c238 100644 --- a/fastmcp_slim/fastmcp/_compat.py +++ b/fastmcp_slim/fastmcp/_compat.py @@ -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", }, diff --git a/tests/test_compat.py b/tests/test_compat.py index 4e2d0f6af..ff5356a64 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -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):