Skip to content
Open
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
50 changes: 39 additions & 11 deletions src/sap_cloud_sdk/agentgateway/_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,24 +618,30 @@ async def _list_server_tools(
url: str,
auth_token: str,
timeout: float,
correlation_id: str,
) -> list[MCPTool]:
"""List tools from a single MCP server.

Args:
url: MCP server endpoint URL.
auth_token: Authorization token.
dependency: Integration dependency (for metadata).
correlation_id: Outbound x-correlation-id for this request.

Returns:
List of MCPTool objects from this server.

Raises:
AgentGatewaySDKError: If server does not provide serverInfo.name.
"""
logger.debug(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correlation id is already part of traces with attribute trace id. We can enhance logs and errors messages, but let's not use correlation id.

"Listing tools from server [correlation-id=%s, path=%s]",
correlation_id,
url,
)
async with httpx.AsyncClient(
headers={
"Authorization": f"Bearer {auth_token}",
"x-correlation-id": str(uuid.uuid4()),
"x-correlation-id": correlation_id,
},
timeout=timeout,
) as http_client:
Expand All @@ -653,7 +659,8 @@ async def _list_server_tools(
and init_result.serverInfo.name
):
raise AgentGatewaySDKError(
f"MCP server at '{url}' did not provide serverInfo.name. "
f"MCP server at '{url}' did not provide serverInfo.name "
f"[correlation-id={correlation_id}]. "
"This is required by the MCP protocol."
)

Expand All @@ -672,22 +679,27 @@ async def _list_server_tools(
]


def _log_mcp_server_error(ord_id: str, exc: BaseException) -> None:
def _log_mcp_server_error(ord_id: str, exc: BaseException, correlation_id: str) -> 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)
_log_mcp_server_error(ord_id, inner, correlation_id)
return
correlation_id_tag = f" [correlation-id={correlation_id}]"
if isinstance(exc, httpx.HTTPStatusError):
logger.error(
"Failed to load tools from %s (HTTP %d): %s",
"Failed to load tools from %s (HTTP %d)%s: %s",
ord_id,
exc.response.status_code,
correlation_id_tag,
exc.response.text[:500],
)
else:
logger.exception(
"Failed to load tools from %s — skipping", ord_id, exc_info=exc
"Failed to load tools from %s%s — skipping",
ord_id,
correlation_id_tag,
exc_info=exc,
)


Expand Down Expand Up @@ -730,12 +742,15 @@ async def get_mcp_tools_customer(
dep.global_tenant_id,
)

correlation_id = str(uuid.uuid4())
try:
server_tools = await _list_server_tools(url, system_token, timeout)
server_tools = await _list_server_tools(
url, system_token, timeout, correlation_id
)
tools.extend(server_tools)
logger.debug("Loaded %d tool(s) from %s", len(server_tools), dep.ord_id)
except Exception as exc:
_log_mcp_server_error(dep.ord_id, exc)
_log_mcp_server_error(dep.ord_id, exc, correlation_id)

logger.info(
"Loaded %d MCP tool(s) from %d server(s)", len(tools), len(dependencies)
Expand Down Expand Up @@ -765,10 +780,17 @@ async def call_mcp_tool_customer(
"""
logger.info("Calling tool '%s' on server '%s'", tool.name, tool.server_name)

correlation_id = str(uuid.uuid4())
logger.debug(
"Calling tool '%s' [correlation-id=%s, path=%s]",
tool.name,
correlation_id,
tool.url,
)
async with httpx.AsyncClient(
headers={
"Authorization": f"Bearer {auth_token}",
"x-correlation-id": str(uuid.uuid4()),
"x-correlation-id": correlation_id,
},
timeout=timeout,
) as http_client:
Expand All @@ -789,6 +811,12 @@ async def call_mcp_tool_customer(
text = str(getattr(first, "text", ""))

if result.isError:
logger.error("Tool '%s' returned an error: %s", tool.name, text)
logger.error(
"Tool '%s' returned an error [correlation-id=%s, path=%s]: %s",
tool.name,
correlation_id,
tool.url,
text,
)

return text
67 changes: 52 additions & 15 deletions src/sap_cloud_sdk/agentgateway/_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,43 +290,59 @@ def _fetch_user_auth_sync():
return token, gateway_url


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


async def list_server_tools(
dest_url: str, auth_token: str, fragment_name: str, timeout: float
dest_url: str,
auth_token: str,
fragment_name: str,
timeout: float,
correlation_id: str,
) -> list[MCPTool]:
"""List tools from a single MCP server.

Args:
dest_url: MCP endpoint URL.
auth_token: Raw access token for the request.
fragment_name: Fragment name for reference.
correlation_id: Outbound x-correlation-id for this request.

Returns:
List of MCPTool objects from this server.
"""
logger.debug(
"Listing tools from fragment '%s' [correlation-id=%s, path=%s]",
fragment_name,
correlation_id,
dest_url,
)
async with httpx.AsyncClient(
headers={
"Authorization": f"Bearer {auth_token}",
"x-correlation-id": str(uuid.uuid4()),
"x-correlation-id": correlation_id,
},
timeout=timeout,
) as http_client:
Expand Down Expand Up @@ -398,9 +414,10 @@ async def get_mcp_tools_lob(
)
continue

correlation_id = str(uuid.uuid4())
try:
server_tools = await list_server_tools(
mcp_url, system_token, fragment_name, timeout
mcp_url, system_token, fragment_name, timeout, correlation_id
)
tools.extend(server_tools)
logger.debug(
Expand All @@ -409,7 +426,7 @@ async def get_mcp_tools_lob(
fragment_name,
)
except Exception as exc:
_log_mcp_server_error(fragment_name, exc)
_log_mcp_server_error(fragment_name, exc, correlation_id)

logger.info("Loaded %d MCP tool(s) from %d fragment(s)", len(tools), len(fragments))
return tools
Expand All @@ -434,10 +451,17 @@ async def call_mcp_tool_lob(
Returns:
Tool execution result as string.
"""
correlation_id = str(uuid.uuid4())
logger.debug(
"Calling tool '%s' [correlation-id=%s, path=%s]",
tool.name,
correlation_id,
tool.url,
)
async with httpx.AsyncClient(
headers={
"Authorization": f"Bearer {user_auth_token}",
"x-correlation-id": str(uuid.uuid4()),
"x-correlation-id": correlation_id,
},
timeout=timeout,
) as http_client:
Expand All @@ -456,7 +480,13 @@ async def call_mcp_tool_lob(
text = str(getattr(first, "text", ""))

if result.isError:
logger.error("Tool '%s' returned an error: %s", tool.name, text)
logger.error(
"Tool '%s' returned an error [correlation-id=%s, path=%s]: %s",
tool.name,
correlation_id,
tool.url,
text,
)

return text

Expand All @@ -482,33 +512,40 @@ async def _fetch_agent_card(
AgentGatewaySDKError: If the request fails or returns a non-200 status.
"""
url = f"{fragment_url.rstrip('/')}/.well-known/agent-card.json"
logger.debug("Fetching agent card from '%s'", url)
correlation_id = str(uuid.uuid4())
logger.debug(
"Fetching agent card [correlation-id=%s, path=%s]",
correlation_id,
url,
)

async with httpx.AsyncClient(
headers={
"Authorization": f"Bearer {auth_token}",
"x-correlation-id": str(uuid.uuid4()),
"x-correlation-id": correlation_id,
},
timeout=timeout,
) as client:
try:
response = await client.get(url)
except httpx.RequestError as e:
raise AgentGatewaySDKError(
f"Agent card request failed for '{fragment_url}': {e}"
f"Agent card request failed for '{fragment_url}' "
f"[correlation-id={correlation_id}]: {e}"
) from e

if response.status_code != 200:
raise AgentGatewaySDKError(
f"Agent card request returned status {response.status_code} "
f"for '{fragment_url}': {response.text[:200]}"
f"for '{fragment_url}' [correlation-id={correlation_id}]: {response.text[:200]}"
)

try:
payload = response.json()
except Exception as e:
raise AgentGatewaySDKError(
f"Failed to parse agent card JSON for '{fragment_url}': {e}"
f"Failed to parse agent card JSON for '{fragment_url}' "
f"[correlation-id={correlation_id}]: {e}"
) from e

return AgentCard(raw=payload)
Expand Down
Loading
Loading