Files
wehub-resource-sync 60e0ffc959
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Waiting to run
Run static analysis / static_analysis (push) Waiting to run
Tests / Tests: Python 3.10 on ubuntu-latest (push) Waiting to run
Tests / Tests: Python 3.13 on ubuntu-latest (push) Waiting to run
Tests / Tests: Python 3.10 on windows-latest (push) Waiting to run
Tests / Tests with lowest-direct dependencies (push) Waiting to run
Tests / Package install smoke (push) Waiting to run
Upgrade checks / Static analysis (push) Waiting to run
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Waiting to run
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Waiting to run
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Waiting to run
Upgrade checks / Integration tests (push) Waiting to run
Upgrade checks / Notify on failure (push) Blocked by required conditions
Upgrade checks / Close issue on success (push) Blocked by required conditions
Update MCPServerConfig Schema / update-config-schema (push) Waiting to run
Update SDK Documentation / update-sdk-docs (push) Waiting to run
Tests / MCP conformance tests (push) Waiting to run
Tests / Integration tests (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

244 lines
10 KiB
Python

"""Tests for the MCP SDK v2 camelCase compatibility bridge (fastmcp._compat)."""
import datetime
import warnings
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
from fastmcp.exceptions import FastMCPDeprecationWarning, MCPError, McpError
def _reset_warn_once() -> None:
"""Reinstall the shims so every property's warn-once flag starts fresh.
Each bridged property closes over its own `warned` flag which persists for
the life of the process. Rebuilding the properties gives tests a clean slate
without leaking warning state across tests.
"""
for cls, mapping in _compat._ALIASES.items():
for camel in mapping:
attr = cls.__dict__.get(camel)
if isinstance(attr, property):
delattr(cls, camel)
_compat._installed = False
_compat.install()
@pytest.fixture(autouse=True)
def fresh_shims():
_reset_warn_once()
yield
class TestCamelCaseBridge:
def test_tool_input_schema_bridged(self):
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
with pytest.warns(FastMCPDeprecationWarning):
assert tool.inputSchema == {"type": "object"} # ty: ignore[unresolved-attribute]
def test_tool_output_schema_bridged(self):
tool = mcp_types.Tool(
name="t",
input_schema={"type": "object"},
output_schema={"type": "string"},
)
with pytest.warns(FastMCPDeprecationWarning):
assert tool.outputSchema == {"type": "string"} # ty: ignore[unresolved-attribute]
def test_call_tool_result_is_error_bridged(self):
result = mcp_types.CallToolResult(content=[], is_error=True)
with pytest.warns(FastMCPDeprecationWarning):
assert result.isError is True # ty: ignore[unresolved-attribute]
def test_call_tool_result_structured_content_bridged(self):
result = mcp_types.CallToolResult(content=[], structured_content={"a": 1})
with pytest.warns(FastMCPDeprecationWarning):
assert result.structuredContent == {"a": 1} # ty: ignore[unresolved-attribute]
def test_resource_mime_type_bridged(self):
resource = mcp_types.Resource(name="r", uri="file:///x", mime_type="text/plain")
with pytest.warns(FastMCPDeprecationWarning):
assert resource.mimeType == "text/plain" # ty: ignore[unresolved-attribute]
def test_resource_template_uri_template_bridged(self):
template = mcp_types.ResourceTemplate(name="rt", uri_template="file:///{id}")
with pytest.warns(FastMCPDeprecationWarning):
assert template.uriTemplate == "file:///{id}" # ty: ignore[unresolved-attribute]
def test_completion_has_more_bridged(self):
completion = mcp_types.Completion(values=["a"], has_more=True)
with pytest.warns(FastMCPDeprecationWarning):
assert completion.hasMore is True # ty: ignore[unresolved-attribute]
def test_list_tools_result_next_cursor_bridged(self):
result = mcp_types.ListToolsResult(tools=[], next_cursor="abc")
with pytest.warns(FastMCPDeprecationWarning):
assert result.nextCursor == "abc" # ty: ignore[unresolved-attribute]
def test_sampling_params_max_tokens_bridged(self):
params = mcp_types.CreateMessageRequestParams(messages=[], max_tokens=100)
with pytest.warns(FastMCPDeprecationWarning):
assert params.maxTokens == 100 # ty: ignore[unresolved-attribute]
def test_elicit_form_params_requested_schema_bridged(self):
params = mcp_types.ElicitRequestFormParams(
message="hi", requested_schema={"type": "object", "properties": {}}
)
with pytest.warns(FastMCPDeprecationWarning):
assert params.requestedSchema == {"type": "object", "properties": {}} # ty: ignore[unresolved-attribute]
class TestWarnOnce:
def test_warns_exactly_once_per_class_name(self):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
t1 = mcp_types.Tool(name="a", input_schema={"type": "object"})
t2 = mcp_types.Tool(name="b", input_schema={"type": "string"})
_ = t1.inputSchema # ty: ignore[unresolved-attribute]
_ = t2.inputSchema # ty: ignore[unresolved-attribute]
_ = t1.inputSchema # ty: ignore[unresolved-attribute]
deprecations = [
w for w in caught if issubclass(w.category, FastMCPDeprecationWarning)
]
assert len(deprecations) == 1
def test_distinct_names_warn_independently(self):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
tool = mcp_types.Tool(
name="a",
input_schema={"type": "object"},
output_schema={"type": "string"},
)
_ = tool.inputSchema # ty: ignore[unresolved-attribute]
_ = tool.outputSchema # ty: ignore[unresolved-attribute]
deprecations = [
w for w in caught if issubclass(w.category, FastMCPDeprecationWarning)
]
assert len(deprecations) == 2
class TestModelRoundTrip:
def test_survives_model_copy(self):
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
copied = tool.model_copy()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
assert copied.inputSchema == {"type": "object"} # ty: ignore[unresolved-attribute]
def test_survives_model_validate(self):
tool = mcp_types.Tool.model_validate(
{"name": "t", "inputSchema": {"type": "object"}}
)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
assert tool.inputSchema == {"type": "object"} # ty: ignore[unresolved-attribute]
assert tool.input_schema == {"type": "object"}
class TestGuards:
def test_does_not_shadow_existing_snake_field(self):
# The real snake_case field must always resolve without warning.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
assert tool.input_schema == {"type": "object"}
assert not caught
def test_install_is_idempotent(self):
_compat.install()
_compat.install()
tool = mcp_types.Tool(name="t", input_schema={"type": "object"})
with pytest.warns(FastMCPDeprecationWarning):
assert tool.inputSchema == {"type": "object"} # ty: ignore[unresolved-attribute]
class TestSettingOff:
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:
def test_mcp_error_is_alias(self):
assert McpError is MCPError
assert McpError is SDKMCPError
def test_except_mcp_error_catches_sdk_raised(self):
with pytest.raises(McpError):
raise SDKMCPError(code=-32000, message="boom")
class TestClientBehaviorCompat:
"""Behavior compat checklist (design decision D)."""
@pytest.fixture
def server(self):
srv = FastMCP("BehaviorServer")
@srv.tool
def echo(x: str) -> str:
return x
return srv
@pytest.mark.parametrize(
"timeout",
[5, 5.0, datetime.timedelta(seconds=5), None],
)
async def test_client_accepts_timedelta_or_float_timeout(self, server, timeout):
client = Client(transport=FastMCPTransport(server), timeout=timeout)
async with client:
result = await client.call_tool("echo", {"x": "hi"})
assert result.data == "hi"
async def test_ping_returns_bool(self, server):
client = Client(transport=FastMCPTransport(server))
async with client:
result = await client.ping()
assert result is True
async def test_session_id_none_safe(self, server):
# In-memory transport has no HTTP session id; must return None, not raise.
client = Client(transport=FastMCPTransport(server))
async with client:
assert client.transport.get_session_id() is None