From 177347ce8fbe4493a225816425e481585756353b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Mon, 31 Aug 2026 14:04:44 -0300 Subject: [PATCH 1/6] add helpers for compatibility with mcp 2.x --- src/sap_cloud_sdk/agentgateway/_compat.py | 27 +++++++ src/sap_cloud_sdk/agentgateway/_customer.py | 17 ++-- src/sap_cloud_sdk/agentgateway/_lob.py | 17 ++-- tests/agentgateway/unit/test_compat.py | 89 +++++++++++++++++++++ 4 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 src/sap_cloud_sdk/agentgateway/_compat.py create mode 100644 tests/agentgateway/unit/test_compat.py diff --git a/src/sap_cloud_sdk/agentgateway/_compat.py b/src/sap_cloud_sdk/agentgateway/_compat.py new file mode 100644 index 00000000..68d137c7 --- /dev/null +++ b/src/sap_cloud_sdk/agentgateway/_compat.py @@ -0,0 +1,27 @@ +"""Compatibility shims for reading MCP result objects across mcp 1.x and 2.x.""" + +from typing import Any + + +def mcp_server_name(init_result: Any) -> str | None: + """Return ``serverInfo.name`` / ``server_info.name`` if present, else None.""" + info = getattr(init_result, "server_info", None) + if info is None: + info = getattr(init_result, "serverInfo", None) + return getattr(info, "name", None) if info is not None else None + + +def mcp_input_schema(tool: Any) -> dict: + """Return the tool's input schema across mcp 1.x/2.x, defaulting to {}.""" + schema = getattr(tool, "input_schema", None) + if schema is None: + schema = getattr(tool, "inputSchema", None) + return schema or {} + + +def mcp_is_error(result: Any) -> bool: + """Return the tool-call error flag across mcp 1.x/2.x, defaulting to False.""" + flag = getattr(result, "is_error", None) + if flag is None: + flag = getattr(result, "isError", None) + return bool(flag) diff --git a/src/sap_cloud_sdk/agentgateway/_customer.py b/src/sap_cloud_sdk/agentgateway/_customer.py index 83f40ca1..e6d17920 100644 --- a/src/sap_cloud_sdk/agentgateway/_customer.py +++ b/src/sap_cloud_sdk/agentgateway/_customer.py @@ -24,6 +24,11 @@ from mcp import ClientSession from mcp.client.streamable_http import streamable_http_client +from sap_cloud_sdk.agentgateway._compat import ( + mcp_input_schema, + mcp_is_error, + mcp_server_name, +) from sap_cloud_sdk.agentgateway._dependencies_resolver import ( EnvironmentDependenciesResolver, IntegrationDependenciesResolver, @@ -648,17 +653,13 @@ async def _list_server_tools( async with ClientSession(read, write) as session: init_result = await session.initialize() - if not ( - init_result - and init_result.serverInfo - and init_result.serverInfo.name - ): + server_name = mcp_server_name(init_result) + if not server_name: raise AgentGatewaySDKError( f"MCP server at '{url}' did not provide serverInfo.name. " "This is required by the MCP protocol." ) - server_name = init_result.serverInfo.name result = await session.list_tools() return [ @@ -666,7 +667,7 @@ async def _list_server_tools( name=t.name, server_name=server_name, description=t.description or "", - input_schema=t.inputSchema or {}, + input_schema=mcp_input_schema(t), url=url, ) for t in result.tools @@ -802,7 +803,7 @@ async def call_mcp_tool_customer( first = result.content[0] text = str(getattr(first, "text", "")) - if result.isError: + if mcp_is_error(result): logger.error("Tool '%s' returned an error: %s", tool.name, text) return text diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index 6a199b82..266a7adc 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -28,6 +28,11 @@ list_mcp_fragments, list_a2a_fragments, ) +from sap_cloud_sdk.agentgateway._compat import ( + mcp_input_schema, + mcp_is_error, + mcp_server_name, +) from sap_cloud_sdk.agentgateway._models import ( Agent, AgentCard, @@ -348,20 +353,14 @@ async def list_server_tools( ): async with ClientSession(read, write) as session: init_result = await session.initialize() - server_name = ( - init_result.serverInfo.name - if init_result - and init_result.serverInfo - and init_result.serverInfo.name - else fragment_name - ) + server_name = mcp_server_name(init_result) or fragment_name result = await session.list_tools() return [ MCPTool( name=t.name, server_name=server_name, description=t.description or "", - input_schema=t.inputSchema or {}, + input_schema=mcp_input_schema(t), url=dest_url, fragment_name=fragment_name, ) @@ -487,7 +486,7 @@ async def call_mcp_tool_lob( first = result.content[0] text = str(getattr(first, "text", "")) - if result.isError: + if mcp_is_error(result): logger.error("Tool '%s' returned an error: %s", tool.name, text) return text diff --git a/tests/agentgateway/unit/test_compat.py b/tests/agentgateway/unit/test_compat.py new file mode 100644 index 00000000..aa3830b8 --- /dev/null +++ b/tests/agentgateway/unit/test_compat.py @@ -0,0 +1,89 @@ +"""Unit tests for the mcp 1.x/2.x compatibility shims.""" + +from types import SimpleNamespace + +from sap_cloud_sdk.agentgateway._compat import ( + mcp_input_schema, + mcp_is_error, + mcp_server_name, +) + + +class TestMcpServerName: + """Tests for mcp_server_name across both mcp majors.""" + + def test_reads_snake_case_server_info_mcp_2x(self): + """mcp 2.x exposes ``server_info`` (snake_case).""" + init = SimpleNamespace(server_info=SimpleNamespace(name="srv-2x")) + assert mcp_server_name(init) == "srv-2x" + + def test_reads_camel_case_server_info_mcp_1x(self): + """mcp 1.x exposes ``serverInfo`` (camelCase).""" + init = SimpleNamespace(serverInfo=SimpleNamespace(name="srv-1x")) + assert mcp_server_name(init) == "srv-1x" + + def test_returns_none_when_server_info_missing(self): + """No server info field on either name -> None.""" + assert mcp_server_name(SimpleNamespace()) is None + + def test_returns_none_when_name_missing(self): + """server_info present but without a ``name`` -> None.""" + init = SimpleNamespace(server_info=SimpleNamespace()) + assert mcp_server_name(init) is None + + def test_returns_none_when_init_result_is_none(self): + """A falsy init_result must not raise -> None.""" + assert mcp_server_name(None) is None + + def test_works_against_real_installed_mcp_types(self): + """Prove it works against the actually-installed mcp library.""" + from mcp.types import Implementation, InitializeResult + + init = InitializeResult( + protocolVersion="2025-06-18", + capabilities={}, + serverInfo=Implementation(name="real-srv", version="1.0.0"), + ) + assert mcp_server_name(init) == "real-srv" + + +class TestMcpInputSchema: + """Tests for mcp_input_schema across both mcp majors.""" + + def test_reads_snake_case_input_schema_mcp_2x(self): + """mcp 2.x exposes ``input_schema`` (snake_case).""" + tool = SimpleNamespace(input_schema={"type": "object"}) + assert mcp_input_schema(tool) == {"type": "object"} + + def test_reads_camel_case_input_schema_mcp_1x(self): + """mcp 1.x exposes ``inputSchema`` (camelCase).""" + tool = SimpleNamespace(inputSchema={"type": "string"}) + assert mcp_input_schema(tool) == {"type": "string"} + + def test_defaults_to_empty_dict_when_missing(self): + """No schema field on either name -> {}.""" + assert mcp_input_schema(SimpleNamespace()) == {} + + def test_defaults_to_empty_dict_when_none(self): + """Schema explicitly None -> {}.""" + assert mcp_input_schema(SimpleNamespace(input_schema=None)) == {} + + +class TestMcpIsError: + """Tests for mcp_is_error across both mcp majors.""" + + def test_reads_snake_case_is_error_mcp_2x(self): + """mcp 2.x exposes ``is_error`` (snake_case).""" + assert mcp_is_error(SimpleNamespace(is_error=True)) is True + + def test_reads_camel_case_is_error_mcp_1x(self): + """mcp 1.x exposes ``isError`` (camelCase).""" + assert mcp_is_error(SimpleNamespace(isError=True)) is True + + def test_false_when_flag_false(self): + """An explicit False flag stays False.""" + assert mcp_is_error(SimpleNamespace(is_error=False)) is False + + def test_defaults_to_false_when_missing(self): + """No error field on either name -> False.""" + assert mcp_is_error(SimpleNamespace()) is False From d55bf2cb1d43fa53073eee4bf5c0b6180773b7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Mon, 31 Aug 2026 14:49:52 -0300 Subject: [PATCH 2/6] change comments to reflect new variable names --- src/sap_cloud_sdk/agentgateway/_customer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/_customer.py b/src/sap_cloud_sdk/agentgateway/_customer.py index e6d17920..378b0dd7 100644 --- a/src/sap_cloud_sdk/agentgateway/_customer.py +++ b/src/sap_cloud_sdk/agentgateway/_customer.py @@ -636,7 +636,8 @@ async def _list_server_tools( List of MCPTool objects from this server. Raises: - AgentGatewaySDKError: If server does not provide serverInfo.name. + AgentGatewaySDKError: If server does not provide a server name + (serverInfo/server_info). """ async with httpx.AsyncClient( headers={ @@ -656,8 +657,8 @@ async def _list_server_tools( server_name = mcp_server_name(init_result) if not server_name: raise AgentGatewaySDKError( - f"MCP server at '{url}' did not provide serverInfo.name. " - "This is required by the MCP protocol." + f"MCP server at '{url}' did not provide its server name " + "(serverInfo/server_info). This is required by the MCP protocol." ) result = await session.list_tools() From 6dee986f27ae59ad2962438440ffa07bed18ec5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Mon, 31 Aug 2026 16:16:24 -0300 Subject: [PATCH 3/6] chore(release): bump version to v0.46.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 419e1792..78f34fa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.46.1" +version = "0.46.2" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" From f16ccdf18ad85049f57eed75733d5e96720e3d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Tue, 1 Sep 2026 14:42:23 -0300 Subject: [PATCH 4/6] refactor(agentgateway): tighten mcp_input_schema return type to dict[str, Any] --- src/sap_cloud_sdk/agentgateway/_compat.py | 2 +- uv.lock | 24 +++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/_compat.py b/src/sap_cloud_sdk/agentgateway/_compat.py index 68d137c7..ab23e832 100644 --- a/src/sap_cloud_sdk/agentgateway/_compat.py +++ b/src/sap_cloud_sdk/agentgateway/_compat.py @@ -11,7 +11,7 @@ def mcp_server_name(init_result: Any) -> str | None: return getattr(info, "name", None) if info is not None else None -def mcp_input_schema(tool: Any) -> dict: +def mcp_input_schema(tool: Any) -> dict[str, Any]: """Return the tool's input schema across mcp 1.x/2.x, defaulting to {}.""" schema = getattr(tool, "input_schema", None) if schema is None: diff --git a/uv.lock b/uv.lock index c55004dc..34a2f022 100644 --- a/uv.lock +++ b/uv.lock @@ -161,9 +161,9 @@ name = "aiologic" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "wrapt", marker = "python_full_version < '3.13'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } wheels = [ @@ -615,8 +615,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "aiologic" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -665,9 +665,9 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "asgiref", marker = "python_full_version < '3.12'" }, - { name = "sqlparse", marker = "python_full_version < '3.12'" }, - { name = "tzdata", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" } wheels = [ @@ -685,9 +685,9 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "asgiref", marker = "python_full_version >= '3.12'" }, - { name = "sqlparse", marker = "python_full_version >= '3.12'" }, - { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/55/664f24ff81c9ea19cb7dfc851afeae1f3c2390c7aee01d4ded68b5c1580d/django-6.0.7.tar.gz", hash = "sha256:2998503fc083124fb58037084bfa00de323c7c743f05f1b4284e77bff0ab8890", size = 10921299, upload-time = "2026-07-07T13:51:26.485Z" } wheels = [ @@ -3925,7 +3925,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.46.1" +version = "0.46.2" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 1ed4f4665481e78cbb8311bd141cc2e827bf1af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Tue, 1 Sep 2026 14:45:13 -0300 Subject: [PATCH 5/6] chore(release): bump version to v0.48.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 78f34fa9..1bb95676 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.46.2" +version = "0.48.2" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" From b04708085f6cf962851e71ab8ac407cc0265fee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Tue, 1 Sep 2026 14:47:01 -0300 Subject: [PATCH 6/6] chore: update uv.lock for v0.48.2 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 34a2f022..c6a54ef2 100644 --- a/uv.lock +++ b/uv.lock @@ -3925,7 +3925,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.46.2" +version = "0.48.2" source = { editable = "." } dependencies = [ { name = "cryptography" },