Skip to content

Commit 399b743

Browse files
authored
fix(mcp): restore compatibility with mcp 2.x (kyegomez#2128)
requirements.txt now allows mcp 2.x (kyegomez#2108), which renamed several attributes. Three sites still read the 1.x spellings, and because they all used getattr with a default, two of them failed silently rather than raising. - CallToolResult.isError -> is_error. The old name now always fell through to the default, so every failed MCP tool call was reported to the agent as a success. - CallToolResult.structuredContent -> structured_content, same silent-fallthrough shape. - OAuthClientProvider dropped its timeout kwarg in 2.x, so building an OAuth provider raised TypeError. The wait stays bounded because callback_handler already closes over oauth.callback_timeout. _mcp_tool_to_openai already handled inputSchema -> input_schema this way; these sites were missed. The test fixture server imported mcp.server.fastmcp, removed in 2.x, so it could not start at all -- 28 tests errored in setup. FastMCP is now MCPServer, behind a shim that still works on 1.x. test_api_key_server_rejects_wrong_key asserted "401" in the error. Rejection still works, but 2.x collapses every non-404 non-2xx response into ErrorData(INTERNAL_ERROR, "Server returned an error response") before it reaches us, so the status is unrecoverable. The assertion is now version-gated rather than dropped: 1.x still requires the status. tests/tools/test_mcp_manager.py: 85 passed (was 2 failed, 28 errors). The 6 remaining failures in tests/tools/ are in test_base_tool.py and test_parse_tools.py, and fail identically on unmodified master.
1 parent 9f37b18 commit 399b743

3 files changed

Lines changed: 52 additions & 13 deletions

File tree

swarms/tools/mcp_manager.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,12 @@ async def _acall_tool(
10841084
"tool": name,
10851085
"server": self.label(connection),
10861086
"arguments": arguments,
1087-
"is_error": bool(getattr(result, "isError", False)),
1087+
# mcp 2.x renamed `isError` to `is_error`; reading only the
1088+
# old name reports every failed call as a success.
1089+
"is_error": bool(
1090+
getattr(result, "isError", None)
1091+
or getattr(result, "is_error", None)
1092+
),
10881093
"result": self._extract_result(result),
10891094
}
10901095

@@ -1144,7 +1149,10 @@ def _extract_result(result: Any) -> Any:
11441149
if texts or others:
11451150
return {"text": "\n".join(texts), "content": others}
11461151

1147-
structured = getattr(result, "structuredContent", None)
1152+
# mcp 2.x renamed `structuredContent` to `structured_content`.
1153+
structured = getattr(
1154+
result, "structuredContent", None
1155+
) or getattr(result, "structured_content", None)
11481156
if structured:
11491157
return structured
11501158

@@ -1408,14 +1416,18 @@ async def callback_handler() -> Tuple[str, Optional[str]]:
14081416
callback_server.wait, float(oauth.callback_timeout)
14091417
)
14101418

1411-
provider = OAuthClientProvider(
1412-
server_url=_server_origin(connection.url or ""),
1413-
client_metadata=client_metadata,
1414-
storage=storage,
1415-
redirect_handler=redirect_handler,
1416-
callback_handler=callback_handler,
1417-
timeout=float(oauth.callback_timeout),
1418-
)
1419+
provider_kwargs: Dict[str, Any] = {
1420+
"server_url": _server_origin(connection.url or ""),
1421+
"client_metadata": client_metadata,
1422+
"storage": storage,
1423+
"redirect_handler": redirect_handler,
1424+
"callback_handler": callback_handler,
1425+
}
1426+
# 2.x dropped the kwarg; callback_handler already bounds the wait.
1427+
if not MCP_IS_V2:
1428+
provider_kwargs["timeout"] = float(oauth.callback_timeout)
1429+
1430+
provider = OAuthClientProvider(**provider_kwargs)
14191431

14201432
self._oauth_providers[key] = provider
14211433
return provider

tests/tools/mcp_test_server.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,32 @@
1616

1717
import sys
1818

19-
from mcp.server.fastmcp import FastMCP
2019
from starlette.responses import JSONResponse
2120

21+
try:
22+
# mcp 1.x
23+
from mcp.server.fastmcp import FastMCP
24+
25+
def _build_server(port: int):
26+
return FastMCP(
27+
"swarms-test-server", host="127.0.0.1", port=port
28+
)
29+
30+
except ImportError:
31+
# 2.x renamed FastMCP to MCPServer and moved the transport settings
32+
# off the constructor; uvicorn binds the port here either way.
33+
from mcp.server.mcpserver import MCPServer
34+
35+
def _build_server(port: int):
36+
return MCPServer("swarms-test-server")
37+
38+
2239
API_KEY = "test-key-123"
2340
BEARER_TOKEN = "test-token-abc"
2441

2542

2643
def build_app(profile: str, port: int):
27-
mcp = FastMCP("swarms-test-server", host="127.0.0.1", port=port)
44+
mcp = _build_server(port)
2845

2946
@mcp.tool()
3047
def add(a: int, b: int) -> int:

tests/tools/test_mcp_manager.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from swarms.schemas.agent_mcp_errors import AgentMCPConnectionError
3131
from swarms.schemas.mcp_schemas import MCPConnection, MCPOAuthConfig
3232
from swarms.tools.mcp_manager import (
33+
MCP_IS_V2,
3334
MCPFileTokenStorage,
3435
MCPInMemoryTokenStorage,
3536
MCPManager,
@@ -527,7 +528,16 @@ def test_api_key_server_rejects_wrong_key(self, apikey_server):
527528
)
528529
with pytest.raises(AgentMCPConnectionError) as excinfo:
529530
manager.get_tools()
530-
assert "401" in str(excinfo.value)
531+
message = str(excinfo.value)
532+
assert apikey_server.url in message
533+
if MCP_IS_V2:
534+
# 2.x collapses every non-404 non-2xx response into
535+
# ErrorData(INTERNAL_ERROR, "Server returned an error
536+
# response") (mcp/client/streamable_http.py), so the status
537+
# is gone before it reaches us. Only the rejection survives.
538+
assert "error response" in message
539+
else:
540+
assert "401" in message
531541

532542
def test_bearer_server_accepts_authorization_token(
533543
self, bearer_server

0 commit comments

Comments
 (0)