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.39.0"
version = "0.39.1"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
40 changes: 31 additions & 9 deletions src/sap_cloud_sdk/agentgateway/_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ async def _list_server_tools(
async with streamable_http_client(url, http_client=http_client) as (
read,
write,
_,
*_,
):
async with ClientSession(read, write) as session:
init_result = await session.initialize()
Expand Down Expand Up @@ -672,6 +672,25 @@ async def _list_server_tools(
]


def _log_mcp_server_error(ord_id: str, exc: BaseException) -> None:
# Unwrap ExceptionGroup from anyio to surface the real HTTP error body
if isinstance(exc, BaseExceptionGroup):
for inner in exc.exceptions:
_log_mcp_server_error(ord_id, inner)
return
if isinstance(exc, httpx.HTTPStatusError):
logger.error(
"Failed to load tools from %s (HTTP %d): %s",
ord_id,
exc.response.status_code,
exc.response.text[:500],
)
else:
logger.exception(
"Failed to load tools from %s — skipping", ord_id, exc_info=exc
)


async def get_mcp_tools_customer(
credentials: CustomerCredentials,
system_token: str,
Expand All @@ -689,16 +708,14 @@ async def get_mcp_tools_customer(

Returns:
List of MCPTool objects from all servers.

Raises:
AgentGatewaySDKError: If integrationDependencies is empty.
"""
dependencies = credentials.integration_dependencies

if not dependencies:
raise AgentGatewaySDKError(
logger.warning(
"integrationDependencies is empty in credentials — no MCP servers configured."
)
return []

logger.info("Discovering tools from %d MCP server(s)", len(dependencies))

Expand All @@ -717,8 +734,8 @@ async def get_mcp_tools_customer(
server_tools = await _list_server_tools(url, system_token, timeout)
tools.extend(server_tools)
logger.debug("Loaded %d tool(s) from %s", len(server_tools), dep.ord_id)
except Exception:
logger.exception("Failed to load tools from %s — skipping", dep.ord_id)
except Exception as exc:
_log_mcp_server_error(dep.ord_id, exc)

logger.info(
"Loaded %d MCP tool(s) from %d server(s)", len(tools), len(dependencies)
Expand Down Expand Up @@ -758,7 +775,7 @@ async def call_mcp_tool_customer(
async with streamable_http_client(tool.url, http_client=http_client) as (
read,
write,
_,
*_,
):
async with ClientSession(read, write) as session:
await session.initialize()
Expand All @@ -769,4 +786,9 @@ async def call_mcp_tool_customer(
return ""

first = result.content[0]
return str(getattr(first, "text", ""))
text = str(getattr(first, "text", ""))

if result.isError:
logger.error("Tool '%s' returned an error: %s", tool.name, text)

return text
38 changes: 30 additions & 8 deletions src/sap_cloud_sdk/agentgateway/_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,26 @@ def _fetch_user_auth_sync():
return token, gateway_url


def _log_mcp_server_error(fragment_name: str, exc: BaseException) -> None:
if isinstance(exc, BaseExceptionGroup):
for inner in exc.exceptions:
_log_mcp_server_error(fragment_name, inner)
return
if isinstance(exc, httpx.HTTPStatusError):
logger.error(
"Failed to load tools from fragment '%s' (HTTP %d): %s",
fragment_name,
exc.response.status_code,
exc.response.text[:500],
)
else:
logger.exception(
"Failed to load tools from fragment '%s' — skipping",
fragment_name,
exc_info=exc,
)


async def list_server_tools(
dest_url: str, auth_token: str, fragment_name: str, timeout: float
) -> list[MCPTool]:
Expand All @@ -313,7 +333,7 @@ async def list_server_tools(
async with streamable_http_client(dest_url, http_client=http_client) as (
read,
write,
_,
*_,
):
async with ClientSession(read, write) as session:
init_result = await session.initialize()
Expand Down Expand Up @@ -388,11 +408,8 @@ async def get_mcp_tools_lob(
len(server_tools),
fragment_name,
)
except Exception:
logger.exception(
"Failed to load tools from fragment '%s' — skipping",
fragment_name,
)
except Exception as exc:
_log_mcp_server_error(fragment_name, exc)

logger.info("Loaded %d MCP tool(s) from %d fragment(s)", len(tools), len(fragments))
return tools
Expand Down Expand Up @@ -427,7 +444,7 @@ async def call_mcp_tool_lob(
async with streamable_http_client(tool.url, http_client=http_client) as (
read,
write,
_,
*_,
):
async with ClientSession(read, write) as session:
await session.initialize()
Expand All @@ -436,7 +453,12 @@ async def call_mcp_tool_lob(
logger.warning("Tool '%s' returned empty content", tool.name)
return ""
first = result.content[0]
return str(getattr(first, "text", ""))
text = str(getattr(first, "text", ""))

if result.isError:
logger.error("Tool '%s' returned an error: %s", tool.name, text)

return text


async def _fetch_agent_card(
Expand Down
10 changes: 4 additions & 6 deletions tests/agentgateway/unit/test_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,8 @@ def credentials(self):
)

@pytest.mark.asyncio
async def test_raises_when_empty_dependencies(self):
"""Raise error when integrationDependencies is empty."""
async def test_returns_empty_when_empty_dependencies(self):
"""Return empty list when integrationDependencies is empty."""
credentials = CustomerCredentials(
token_service_url="https://ias.example.com/oauth2/token",
client_id="test-client",
Expand All @@ -474,10 +474,8 @@ async def test_raises_when_empty_dependencies(self):
gateway_url="https://agw.example.com",
integration_dependencies=[],
)
with pytest.raises(
AgentGatewaySDKError, match="integrationDependencies is empty"
):
await get_mcp_tools_customer(credentials, "system-token", 60.0)
result = await get_mcp_tools_customer(credentials, "system-token", 60.0)
assert result == []

@pytest.mark.asyncio
async def test_discovers_tools_from_credentials(self, credentials):
Expand Down
24 changes: 12 additions & 12 deletions uv.lock

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

Loading