Skip to content

Commit 4e1ae25

Browse files
committed
fix(agentgateway): guard list_tools None response in LoB flow
When MCP instrumentation returns None from list_tools(), skip the fragment with a clear warning instead of raising AttributeError on result.tools.
1 parent d35af86 commit 4e1ae25

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

src/sap_cloud_sdk/agentgateway/_lob.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,15 @@ async def list_server_tools(
325325
else fragment_name
326326
)
327327
result = await session.list_tools()
328+
if result is None or result.tools is None:
329+
logger.warning(
330+
"MCP server '%s' at '%s' returned no tools from list_tools() "
331+
"(response was None — often caused by OpenTelemetry MCP "
332+
"instrumentation swallowing errors). Skipping fragment.",
333+
fragment_name,
334+
dest_url,
335+
)
336+
return []
328337
return [
329338
MCPTool(
330339
name=t.name,

tests/agentgateway/unit/test_lob.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
fetch_user_auth,
2222
get_ias_client_id_lob,
2323
get_mcp_tools_lob,
24+
list_server_tools,
2425
get_agent_cards_lob,
2526
_fetch_agent_card,
2627
call_mcp_tool_lob,
@@ -600,6 +601,91 @@ async def mock_list_tools_fn(*args, **kwargs):
600601
assert result[0].name == "tool2"
601602

602603

604+
# ============================================================
605+
# Test: list_server_tools
606+
# ============================================================
607+
608+
609+
class TestListServerTools:
610+
"""Tests for list_server_tools async function."""
611+
612+
@pytest.mark.asyncio
613+
async def test_returns_empty_when_list_tools_returns_none(self, caplog):
614+
"""Return empty list when MCP list_tools response is None (OTel instrumentation)."""
615+
import logging
616+
617+
caplog.set_level(logging.WARNING, logger="sap_cloud_sdk.agentgateway._lob")
618+
619+
with (
620+
patch("sap_cloud_sdk.agentgateway._lob.httpx.AsyncClient") as mock_http,
621+
patch(
622+
"sap_cloud_sdk.agentgateway._lob.streamable_http_client"
623+
) as mock_stream,
624+
patch("sap_cloud_sdk.agentgateway._lob.ClientSession") as mock_session,
625+
):
626+
mock_http_instance = AsyncMock()
627+
mock_http.return_value.__aenter__.return_value = mock_http_instance
628+
629+
mock_stream.return_value.__aenter__.return_value = (
630+
AsyncMock(),
631+
AsyncMock(),
632+
None,
633+
)
634+
635+
mock_session_instance = AsyncMock()
636+
mock_init = MagicMock()
637+
mock_init.serverInfo.name = "test-server"
638+
mock_session_instance.initialize = AsyncMock(return_value=mock_init)
639+
mock_session_instance.list_tools = AsyncMock(return_value=None)
640+
mock_session.return_value.__aenter__.return_value = mock_session_instance
641+
642+
result = await list_server_tools(
643+
"https://example.com/mcp", "auth-token", "frag-a", 60.0
644+
)
645+
646+
assert result == []
647+
assert "returned no tools from list_tools()" in caplog.text
648+
649+
@pytest.mark.asyncio
650+
async def test_returns_empty_when_tools_attribute_is_none(self, caplog):
651+
"""Return empty list when list_tools result has tools=None."""
652+
import logging
653+
654+
caplog.set_level(logging.WARNING, logger="sap_cloud_sdk.agentgateway._lob")
655+
656+
with (
657+
patch("sap_cloud_sdk.agentgateway._lob.httpx.AsyncClient") as mock_http,
658+
patch(
659+
"sap_cloud_sdk.agentgateway._lob.streamable_http_client"
660+
) as mock_stream,
661+
patch("sap_cloud_sdk.agentgateway._lob.ClientSession") as mock_session,
662+
):
663+
mock_http_instance = AsyncMock()
664+
mock_http.return_value.__aenter__.return_value = mock_http_instance
665+
666+
mock_stream.return_value.__aenter__.return_value = (
667+
AsyncMock(),
668+
AsyncMock(),
669+
None,
670+
)
671+
672+
mock_session_instance = AsyncMock()
673+
mock_init = MagicMock()
674+
mock_init.serverInfo.name = "test-server"
675+
mock_session_instance.initialize = AsyncMock(return_value=mock_init)
676+
mock_list_result = MagicMock()
677+
mock_list_result.tools = None
678+
mock_session_instance.list_tools = AsyncMock(return_value=mock_list_result)
679+
mock_session.return_value.__aenter__.return_value = mock_session_instance
680+
681+
result = await list_server_tools(
682+
"https://example.com/mcp", "auth-token", "frag-a", 60.0
683+
)
684+
685+
assert result == []
686+
assert "returned no tools from list_tools()" in caplog.text
687+
688+
603689
# ============================================================
604690
# Test: call_mcp_tool_lob
605691
# ============================================================

0 commit comments

Comments
 (0)