Skip to content

Commit 9d18924

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 256f204 + 2f40acd commit 9d18924

5 files changed

Lines changed: 159 additions & 34 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Compatibility shims for reading MCP result objects across mcp 1.x and 2.x."""
2+
3+
from typing import Any
4+
5+
6+
def mcp_server_name(init_result: Any) -> str | None:
7+
"""Return ``serverInfo.name`` / ``server_info.name`` if present, else None."""
8+
info = getattr(init_result, "server_info", None)
9+
if info is None:
10+
info = getattr(init_result, "serverInfo", None)
11+
return getattr(info, "name", None) if info is not None else None
12+
13+
14+
def mcp_input_schema(tool: Any) -> dict[str, Any]:
15+
"""Return the tool's input schema across mcp 1.x/2.x, defaulting to {}."""
16+
schema = getattr(tool, "input_schema", None)
17+
if schema is None:
18+
schema = getattr(tool, "inputSchema", None)
19+
return schema or {}
20+
21+
22+
def mcp_is_error(result: Any) -> bool:
23+
"""Return the tool-call error flag across mcp 1.x/2.x, defaulting to False."""
24+
flag = getattr(result, "is_error", None)
25+
if flag is None:
26+
flag = getattr(result, "isError", None)
27+
return bool(flag)

src/sap_cloud_sdk/agentgateway/_customer.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@
2323
import httpx
2424
from mcp import ClientSession
2525
from mcp.client.streamable_http import streamable_http_client
26-
from mcp.shared.exceptions import McpError
2726

27+
try:
28+
from mcp.shared.exceptions import McpError
29+
except ImportError:
30+
from mcp.shared.exceptions import MCPError as McpError # type: ignore[no-redef] # ty: ignore[unresolved-import]
31+
32+
from sap_cloud_sdk.agentgateway._compat import (
33+
mcp_input_schema,
34+
mcp_is_error,
35+
mcp_server_name,
36+
)
2837
from sap_cloud_sdk.agentgateway._dependencies_resolver import (
2938
EnvironmentDependenciesResolver,
3039
IntegrationDependenciesResolver,
@@ -633,7 +642,8 @@ async def _list_server_tools(
633642
List of MCPTool objects from this server.
634643
635644
Raises:
636-
AgentGatewaySDKError: If server does not provide serverInfo.name.
645+
AgentGatewaySDKError: If server does not provide a server name
646+
(serverInfo/server_info).
637647
"""
638648
async with httpx.AsyncClient(
639649
headers={
@@ -650,25 +660,21 @@ async def _list_server_tools(
650660
async with ClientSession(read, write) as session:
651661
init_result = await session.initialize()
652662

653-
if not (
654-
init_result
655-
and init_result.serverInfo
656-
and init_result.serverInfo.name
657-
):
663+
server_name = mcp_server_name(init_result)
664+
if not server_name:
658665
raise AgentGatewaySDKError(
659-
f"MCP server at '{url}' did not provide serverInfo.name. "
660-
"This is required by the MCP protocol."
666+
f"MCP server at '{url}' did not provide its server name "
667+
"(serverInfo/server_info). This is required by the MCP protocol."
661668
)
662669

663-
server_name = init_result.serverInfo.name
664670
result = await session.list_tools()
665671

666672
return [
667673
MCPTool(
668674
name=t.name,
669675
server_name=server_name,
670676
description=t.description or "",
671-
input_schema=t.inputSchema or {},
677+
input_schema=mcp_input_schema(t),
672678
url=url,
673679
)
674680
for t in result.tools
@@ -833,7 +839,7 @@ async def call_mcp_tool_customer(
833839
first = result.content[0]
834840
text = str(getattr(first, "text", ""))
835841

836-
if result.isError:
842+
if mcp_is_error(result):
837843
logger.error(
838844
"Tool '%s' on '%s' returned an error: %s",
839845
tool.name,

src/sap_cloud_sdk/agentgateway/_lob.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
import httpx
1414
from mcp import ClientSession
1515
from mcp.client.streamable_http import streamable_http_client
16-
from mcp.shared.exceptions import McpError
16+
17+
try:
18+
from mcp.shared.exceptions import McpError
19+
except ImportError:
20+
from mcp.shared.exceptions import MCPError as McpError # type: ignore[no-redef] # ty: ignore[unresolved-import]
1721
from sap_cloud_sdk.destination import (
1822
create_client as create_destination_client,
1923
ConsumptionLevel,
@@ -29,6 +33,11 @@
2933
list_mcp_fragments,
3034
list_a2a_fragments,
3135
)
36+
from sap_cloud_sdk.agentgateway._compat import (
37+
mcp_input_schema,
38+
mcp_is_error,
39+
mcp_server_name,
40+
)
3241
from sap_cloud_sdk.agentgateway._models import (
3342
JsonRpcError,
3443
Agent,
@@ -375,20 +384,14 @@ async def list_server_tools(
375384
):
376385
async with ClientSession(read, write) as session:
377386
init_result = await session.initialize()
378-
server_name = (
379-
init_result.serverInfo.name
380-
if init_result
381-
and init_result.serverInfo
382-
and init_result.serverInfo.name
383-
else fragment_name
384-
)
387+
server_name = mcp_server_name(init_result) or fragment_name
385388
result = await session.list_tools()
386389
return [
387390
MCPTool(
388391
name=t.name,
389392
server_name=server_name,
390393
description=t.description or "",
391-
input_schema=t.inputSchema or {},
394+
input_schema=mcp_input_schema(t),
392395
url=dest_url,
393396
fragment_name=fragment_name,
394397
)
@@ -516,7 +519,7 @@ async def call_mcp_tool_lob(
516519
first = result.content[0]
517520
text = str(getattr(first, "text", ""))
518521

519-
if result.isError:
522+
if mcp_is_error(result):
520523
logger.error(
521524
"Tool '%s' on '%s' returned an error: %s",
522525
tool.name,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Unit tests for the mcp 1.x/2.x compatibility shims."""
2+
3+
from types import SimpleNamespace
4+
5+
from sap_cloud_sdk.agentgateway._compat import (
6+
mcp_input_schema,
7+
mcp_is_error,
8+
mcp_server_name,
9+
)
10+
11+
12+
class TestMcpServerName:
13+
"""Tests for mcp_server_name across both mcp majors."""
14+
15+
def test_reads_snake_case_server_info_mcp_2x(self):
16+
"""mcp 2.x exposes ``server_info`` (snake_case)."""
17+
init = SimpleNamespace(server_info=SimpleNamespace(name="srv-2x"))
18+
assert mcp_server_name(init) == "srv-2x"
19+
20+
def test_reads_camel_case_server_info_mcp_1x(self):
21+
"""mcp 1.x exposes ``serverInfo`` (camelCase)."""
22+
init = SimpleNamespace(serverInfo=SimpleNamespace(name="srv-1x"))
23+
assert mcp_server_name(init) == "srv-1x"
24+
25+
def test_returns_none_when_server_info_missing(self):
26+
"""No server info field on either name -> None."""
27+
assert mcp_server_name(SimpleNamespace()) is None
28+
29+
def test_returns_none_when_name_missing(self):
30+
"""server_info present but without a ``name`` -> None."""
31+
init = SimpleNamespace(server_info=SimpleNamespace())
32+
assert mcp_server_name(init) is None
33+
34+
def test_returns_none_when_init_result_is_none(self):
35+
"""A falsy init_result must not raise -> None."""
36+
assert mcp_server_name(None) is None
37+
38+
def test_works_against_real_installed_mcp_types(self):
39+
"""Prove it works against the actually-installed mcp library."""
40+
from mcp.types import Implementation, InitializeResult
41+
42+
init = InitializeResult(
43+
protocolVersion="2025-06-18",
44+
capabilities={},
45+
serverInfo=Implementation(name="real-srv", version="1.0.0"),
46+
)
47+
assert mcp_server_name(init) == "real-srv"
48+
49+
50+
class TestMcpInputSchema:
51+
"""Tests for mcp_input_schema across both mcp majors."""
52+
53+
def test_reads_snake_case_input_schema_mcp_2x(self):
54+
"""mcp 2.x exposes ``input_schema`` (snake_case)."""
55+
tool = SimpleNamespace(input_schema={"type": "object"})
56+
assert mcp_input_schema(tool) == {"type": "object"}
57+
58+
def test_reads_camel_case_input_schema_mcp_1x(self):
59+
"""mcp 1.x exposes ``inputSchema`` (camelCase)."""
60+
tool = SimpleNamespace(inputSchema={"type": "string"})
61+
assert mcp_input_schema(tool) == {"type": "string"}
62+
63+
def test_defaults_to_empty_dict_when_missing(self):
64+
"""No schema field on either name -> {}."""
65+
assert mcp_input_schema(SimpleNamespace()) == {}
66+
67+
def test_defaults_to_empty_dict_when_none(self):
68+
"""Schema explicitly None -> {}."""
69+
assert mcp_input_schema(SimpleNamespace(input_schema=None)) == {}
70+
71+
72+
class TestMcpIsError:
73+
"""Tests for mcp_is_error across both mcp majors."""
74+
75+
def test_reads_snake_case_is_error_mcp_2x(self):
76+
"""mcp 2.x exposes ``is_error`` (snake_case)."""
77+
assert mcp_is_error(SimpleNamespace(is_error=True)) is True
78+
79+
def test_reads_camel_case_is_error_mcp_1x(self):
80+
"""mcp 1.x exposes ``isError`` (camelCase)."""
81+
assert mcp_is_error(SimpleNamespace(isError=True)) is True
82+
83+
def test_false_when_flag_false(self):
84+
"""An explicit False flag stays False."""
85+
assert mcp_is_error(SimpleNamespace(is_error=False)) is False
86+
87+
def test_defaults_to_false_when_missing(self):
88+
"""No error field on either name -> False."""
89+
assert mcp_is_error(SimpleNamespace()) is False

uv.lock

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)