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.53.0"
version = "0.53.1"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
16 changes: 10 additions & 6 deletions src/sap_cloud_sdk/agentgateway/_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@
MCPToolFilter,
)
from sap_cloud_sdk.agentgateway._token_cache import _TokenCache
from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError
from sap_cloud_sdk.agentgateway.exceptions import (
AgentGatewaySDKError,
AgentGatewayServerError,
)
from sap_cloud_sdk.core.secret_resolver import resolve_base_mount

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -831,6 +834,10 @@ async def call_mcp_tool_customer(
await session.initialize()
result = await session.call_tool(tool.name, kwargs)

if result is None:
raise AgentGatewayServerError(
f"Tool '{tool.name}' on '{tool.url}' returned None"
)
if not result.content:
logger.warning(
"Tool '%s' on '%s' returned empty content", tool.name, tool.url
Expand All @@ -841,11 +848,8 @@ async def call_mcp_tool_customer(
text = str(getattr(first, "text", ""))

if mcp_is_error(result):
logger.error(
"Tool '%s' on '%s' returned an error: %s",
tool.name,
tool.url,
text,
raise AgentGatewayServerError(
f"Tool '{tool.name}' on '{tool.url}' returned an error: {text}"
)

return text
12 changes: 7 additions & 5 deletions src/sap_cloud_sdk/agentgateway/_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache
from sap_cloud_sdk.agentgateway.exceptions import (
AgentGatewaySDKError,
AgentGatewayServerError,
MCPServerNotFoundError,
)

Expand Down Expand Up @@ -517,6 +518,10 @@ async def call_mcp_tool_lob(
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(tool.name, kwargs)
if result is None:
raise AgentGatewayServerError(
f"Tool '{tool.name}' on '{tool.url}' returned None"
)
if not result.content:
logger.warning(
"Tool '%s' on '%s' returned empty content", tool.name, tool.url
Expand All @@ -526,11 +531,8 @@ async def call_mcp_tool_lob(
text = str(getattr(first, "text", ""))

if mcp_is_error(result):
logger.error(
"Tool '%s' on '%s' returned an error: %s",
tool.name,
tool.url,
text,
raise AgentGatewayServerError(
f"Tool '{tool.name}' on '{tool.url}' returned an error: {text}"
)

return text
Expand Down
81 changes: 78 additions & 3 deletions tests/agentgateway/unit/test_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
)
from sap_cloud_sdk.agentgateway._token_cache import _TokenCache
from sap_cloud_sdk.agentgateway.config import ClientConfig
from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError
from sap_cloud_sdk.agentgateway.exceptions import (
AgentGatewaySDKError,
AgentGatewayServerError,
)


# ============================================================
Expand Down Expand Up @@ -766,6 +769,7 @@ async def test_calls_tool_with_pre_fetched_token(self, credentials, mock_tool):
mock_content = MagicMock()
mock_content.text = "Order created successfully"
mock_result.content = [mock_content]
mock_result.is_error = False
mock_session.call_tool = AsyncMock(return_value=mock_result)
mock_session_ctx = AsyncMock()
mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_session)
Expand Down Expand Up @@ -824,9 +828,80 @@ async def test_returns_empty_string_when_no_content(self, credentials, mock_tool

assert result == ""

@pytest.mark.asyncio
async def test_raises_when_result_is_none(self, credentials, mock_tool):
"""Raise AgentGatewayServerError when call_tool returns None."""
with (
patch("httpx.AsyncClient") as mock_client_class,
patch(
"sap_cloud_sdk.agentgateway._customer.streamable_http_client"
) as mock_stream,
patch(
"sap_cloud_sdk.agentgateway._customer.ClientSession"
) as mock_session_class,
):
mock_client = AsyncMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
mock_client_class.return_value = mock_client

# ============================================================
# Test: detect_transparent_credentials
mock_stream_ctx = AsyncMock()
mock_stream_ctx.__aenter__ = AsyncMock(
return_value=(AsyncMock(), AsyncMock(), None)
)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=None)
mock_stream.return_value = mock_stream_ctx

mock_session = AsyncMock()
mock_session.initialize = AsyncMock()
mock_session.call_tool = AsyncMock(return_value=None)
mock_session_ctx = AsyncMock()
mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_session)
mock_session_ctx.__aexit__ = AsyncMock(return_value=None)
mock_session_class.return_value = mock_session_ctx

with pytest.raises(AgentGatewayServerError, match="returned None"):
await call_mcp_tool_customer(mock_tool, "auth-token", 60.0)

@pytest.mark.asyncio
async def test_raises_when_tool_returns_is_error(self, credentials, mock_tool):
"""Raise AgentGatewayServerError when call_tool result has isError=True."""
with (
patch("httpx.AsyncClient") as mock_client_class,
patch(
"sap_cloud_sdk.agentgateway._customer.streamable_http_client"
) as mock_stream,
patch(
"sap_cloud_sdk.agentgateway._customer.ClientSession"
) as mock_session_class,
):
mock_client = AsyncMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
mock_client_class.return_value = mock_client

mock_stream_ctx = AsyncMock()
mock_stream_ctx.__aenter__ = AsyncMock(
return_value=(AsyncMock(), AsyncMock(), None)
)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=None)
mock_stream.return_value = mock_stream_ctx

mock_session = AsyncMock()
mock_session.initialize = AsyncMock()
mock_result = MagicMock()
mock_content = MagicMock()
mock_content.text = "change number test_sm doesn't exist"
mock_result.content = [mock_content]
mock_result.is_error = True
mock_session.call_tool = AsyncMock(return_value=mock_result)
mock_session_ctx = AsyncMock()
mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_session)
mock_session_ctx.__aexit__ = AsyncMock(return_value=None)
mock_session_class.return_value = mock_session_ctx

with pytest.raises(AgentGatewayServerError, match="returned an error"):
await call_mcp_tool_customer(mock_tool, "auth-token", 60.0)
# ============================================================


Expand Down
73 changes: 71 additions & 2 deletions tests/agentgateway/unit/test_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from sap_cloud_sdk.destination import ConsumptionOptions, ConsumptionLevel
from sap_cloud_sdk.agentgateway.exceptions import (
AgentGatewaySDKError,
AgentGatewayServerError,
MCPServerNotFoundError,
)
from sap_cloud_sdk.destination import ConsumptionLevel
Expand Down Expand Up @@ -953,6 +954,7 @@ async def test_calls_tool_with_pre_fetched_token(self):
mock_result = MagicMock()
mock_result.content = [MagicMock()]
mock_result.content[0].text = "Tool result"
mock_result.is_error = False

with (
patch("sap_cloud_sdk.agentgateway._lob.httpx.AsyncClient") as mock_http,
Expand Down Expand Up @@ -1030,9 +1032,76 @@ async def test_returns_empty_string_when_no_content(self):

assert result == ""

@pytest.mark.asyncio
async def test_raises_when_result_is_none(self):
"""Raise AgentGatewayServerError when call_tool returns None."""
tool = MCPTool(
name="test-tool",
server_name="test-server",
description="Test tool",
input_schema={},
url="https://example.com/mcp",
fragment_name="test-fragment",
)

# ============================================================
# Test: list_a2a_fragments
with (
patch("sap_cloud_sdk.agentgateway._lob.httpx.AsyncClient") as mock_http,
patch(
"sap_cloud_sdk.agentgateway._lob.streamable_http_client"
) as mock_stream,
patch("sap_cloud_sdk.agentgateway._lob.ClientSession") as mock_session,
):
mock_http.return_value.__aenter__.return_value = AsyncMock()
mock_stream.return_value.__aenter__.return_value = (
AsyncMock(),
AsyncMock(),
None,
)
mock_session_instance = AsyncMock()
mock_session_instance.initialize = AsyncMock()
mock_session_instance.call_tool = AsyncMock(return_value=None)
mock_session.return_value.__aenter__.return_value = mock_session_instance

with pytest.raises(AgentGatewayServerError, match="returned None"):
await call_mcp_tool_lob(tool, "user-auth-token", 60.0)

@pytest.mark.asyncio
async def test_raises_when_tool_returns_is_error(self):
"""Raise AgentGatewayServerError when call_tool result has isError=True."""
tool = MCPTool(
name="test-tool",
server_name="test-server",
description="Test tool",
input_schema={},
url="https://example.com/mcp",
fragment_name="test-fragment",
)

mock_result = MagicMock()
mock_result.content = [MagicMock()]
mock_result.content[0].text = "change number test_sm doesn't exist"
mock_result.is_error = True

with (
patch("sap_cloud_sdk.agentgateway._lob.httpx.AsyncClient") as mock_http,
patch(
"sap_cloud_sdk.agentgateway._lob.streamable_http_client"
) as mock_stream,
patch("sap_cloud_sdk.agentgateway._lob.ClientSession") as mock_session,
):
mock_http.return_value.__aenter__.return_value = AsyncMock()
mock_stream.return_value.__aenter__.return_value = (
AsyncMock(),
AsyncMock(),
None,
)
mock_session_instance = AsyncMock()
mock_session_instance.initialize = AsyncMock()
mock_session_instance.call_tool = AsyncMock(return_value=mock_result)
mock_session.return_value.__aenter__.return_value = mock_session_instance

with pytest.raises(AgentGatewayServerError, match="returned an error"):
await call_mcp_tool_lob(tool, "user-auth-token", 60.0)
# ============================================================


Expand Down
Loading
Loading