mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
Make mcp_camelcase_compat a runtime toggle
Install the camelCase bridge properties unconditionally and gate each getter on the live setting: warn+return when enabled, raise AttributeError when disabled. Previously the bridge installed once at import and setting mcp_camelcase_compat = False afterward had no effect, contradicting the upgrade docs.
This commit is contained in:
parent
96ca009220
commit
d9659453f4
3 changed files with 60 additions and 25 deletions
|
|
@ -30,11 +30,12 @@ if settings.log_enabled:
|
|||
)
|
||||
|
||||
# Install camelCase compatibility shims for MCP SDK v2's snake_case rename.
|
||||
# Gated by the setting; patches only mcp_types model classes, no client chain.
|
||||
if settings.mcp_camelcase_compat:
|
||||
from fastmcp import _compat
|
||||
# Installed unconditionally; each shim's getter checks the live
|
||||
# `mcp_camelcase_compat` setting at read time, so the bridge can be toggled at
|
||||
# runtime. Patches only mcp_types model classes, no client chain.
|
||||
from fastmcp import _compat
|
||||
|
||||
_compat.install()
|
||||
_compat.install()
|
||||
|
||||
try:
|
||||
__version__ = _version("fastmcp-slim")
|
||||
|
|
|
|||
|
|
@ -9,8 +9,14 @@ This module installs warn-once `@property` shims that route a small set of
|
|||
documented camelCase reads to their snake_case attributes. Only fields users
|
||||
actually read (per the docs boundary inventory) are bridged; each read emits a
|
||||
single `FastMCPDeprecationWarning` per (class, name) and returns the correct
|
||||
value. Installation is idempotent and gated by the `mcp_camelcase_compat`
|
||||
setting.
|
||||
value. Installation is idempotent.
|
||||
|
||||
The properties are installed unconditionally, but each getter checks the live
|
||||
`mcp_camelcase_compat` setting at read time: when the setting is enabled it
|
||||
warns and returns the snake_case value; when disabled it raises `AttributeError`
|
||||
exactly as if the property were never installed. This makes the setting a
|
||||
genuine runtime toggle (`fastmcp.settings.mcp_camelcase_compat = False` after
|
||||
import turns the bridge off) at negligible overhead.
|
||||
|
||||
Guards ensure we never shadow a real upstream attribute: if a class already
|
||||
defines the camelCase name in its own `__dict__` or in its pydantic
|
||||
|
|
@ -95,11 +101,21 @@ _installed = False
|
|||
|
||||
|
||||
def _make_property(cls_name: str, camel: str, snake: str) -> property:
|
||||
"""Build a warn-once property routing a camelCase read to a snake attr."""
|
||||
"""Build a warn-once property routing a camelCase read to a snake attr.
|
||||
|
||||
The getter reads the live `mcp_camelcase_compat` setting on every access: if
|
||||
the bridge is disabled it raises `AttributeError` (matching the message
|
||||
Python raises for a genuinely missing attribute) so the shim is transparent;
|
||||
if enabled it warns once and returns the snake_case value.
|
||||
"""
|
||||
warned = False
|
||||
|
||||
def getter(self: object) -> object:
|
||||
nonlocal warned
|
||||
import fastmcp
|
||||
|
||||
if not fastmcp.settings.mcp_camelcase_compat:
|
||||
raise AttributeError(f"{cls_name!r} object has no attribute {camel!r}")
|
||||
if not warned:
|
||||
warned = True
|
||||
warnings.warn(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import mcp_types
|
|||
import pytest
|
||||
from mcp import MCPError as SDKMCPError
|
||||
|
||||
import fastmcp
|
||||
import fastmcp._compat as _compat
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.client.transports import FastMCPTransport
|
||||
|
|
@ -159,24 +160,41 @@ class TestGuards:
|
|||
|
||||
|
||||
class TestSettingOff:
|
||||
def test_setting_off_no_bridge(self, monkeypatch):
|
||||
# Simulate a fresh import with the setting disabled: strip the installed
|
||||
# properties and confirm the camelCase read raises AttributeError.
|
||||
installed = {}
|
||||
for cls, mapping in _compat._ALIASES.items():
|
||||
for camel in mapping:
|
||||
attr = cls.__dict__.get(camel)
|
||||
if isinstance(attr, property):
|
||||
installed.setdefault(cls, []).append(camel)
|
||||
delattr(cls, camel)
|
||||
try:
|
||||
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
|
||||
with pytest.raises(AttributeError):
|
||||
_ = tool.inputSchema # ty: ignore[unresolved-attribute]
|
||||
finally:
|
||||
_compat._installed = False
|
||||
_compat.install()
|
||||
assert installed # sanity: something was actually removed
|
||||
def test_setting_off_raises_attribute_error(self, monkeypatch):
|
||||
# The property stays installed, but with the setting disabled the getter
|
||||
# raises AttributeError as if the camelCase name never existed.
|
||||
monkeypatch.setattr(fastmcp.settings, "mcp_camelcase_compat", False)
|
||||
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
|
||||
with pytest.raises(AttributeError):
|
||||
_ = tool.inputSchema # ty: ignore[unresolved-attribute]
|
||||
|
||||
def test_setting_off_attribute_error_message(self, monkeypatch):
|
||||
monkeypatch.setattr(fastmcp.settings, "mcp_camelcase_compat", False)
|
||||
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
|
||||
with pytest.raises(
|
||||
AttributeError,
|
||||
match=r"'Tool' object has no attribute 'inputSchema'",
|
||||
):
|
||||
_ = tool.inputSchema # ty: ignore[unresolved-attribute]
|
||||
|
||||
def test_runtime_toggle_on_off_on(self, monkeypatch):
|
||||
# The bridge honours the live setting on every read: on -> off -> on.
|
||||
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
|
||||
|
||||
# On (default): bridged read works and warns.
|
||||
with pytest.warns(FastMCPDeprecationWarning):
|
||||
assert tool.inputSchema == {"type": "object"} # ty: ignore[unresolved-attribute]
|
||||
|
||||
# Off: same attribute now raises.
|
||||
monkeypatch.setattr(fastmcp.settings, "mcp_camelcase_compat", False)
|
||||
with pytest.raises(AttributeError):
|
||||
_ = tool.inputSchema # ty: ignore[unresolved-attribute]
|
||||
|
||||
# Back on: resolves again (warn-once may have fired already, so ignore).
|
||||
monkeypatch.setattr(fastmcp.settings, "mcp_camelcase_compat", True)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
assert tool.inputSchema == {"type": "object"} # ty: ignore[unresolved-attribute]
|
||||
|
||||
|
||||
class TestExceptionAlias:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue