Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.48.1"
version = "0.48.2"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
27 changes: 27 additions & 0 deletions src/sap_cloud_sdk/agentgateway/_compat.py
Original file line number Diff line number Diff line change
@@ -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[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:
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)
24 changes: 13 additions & 11 deletions src/sap_cloud_sdk/agentgateway/_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
except ImportError:
from mcp.shared.exceptions import MCPError as McpError # type: ignore[no-redef] # ty: ignore[unresolved-import]

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,
Expand Down Expand Up @@ -637,7 +642,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={
Expand All @@ -654,25 +660,21 @@ 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."
f"MCP server at '{url}' did not provide its server name "
"(serverInfo/server_info). This is required by the MCP protocol."
)

server_name = init_result.serverInfo.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=url,
)
for t in result.tools
Expand Down Expand Up @@ -837,7 +839,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' on '%s' returned an error: %s",
tool.name,
Expand Down
17 changes: 8 additions & 9 deletions src/sap_cloud_sdk/agentgateway/_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 (
JsonRpcError,
Agent,
Expand Down Expand Up @@ -379,20 +384,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,
)
Expand Down Expand Up @@ -520,7 +519,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' on '%s' returned an error: %s",
tool.name,
Expand Down
89 changes: 89 additions & 0 deletions tests/agentgateway/unit/test_compat.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading