1111import aiohttp
1212from urllib .parse import urlsplit , urlunsplit
1313
14+ from agent .components .tools import MCPToolLoader
15+
1416from .checkpointer import checkpointers
1517from .mcp_tools_cache import get_mcp_tools , get_server_configs
1618from .redis_client import get_redis_client
1719
1820logger = logging .getLogger ("opey.service.status" )
1921
2022_PROBE_TIMEOUT_SEC = 2.0
23+ _MCP_TEST_CALL_TIMEOUT_SEC = 5.0 # full MCP handshake + tools/list is heavier than an HTTP ping
2124_CACHE_TTL_SEC = 15.0
2225
2326_start_time_monotonic = time .monotonic ()
@@ -70,7 +73,11 @@ def _obp_mcp_status_url() -> str | None:
7073 parts = urlsplit (cfg .url )
7174 if not parts .scheme or not parts .netloc :
7275 return None
73- return urlunsplit ((parts .scheme , parts .netloc , "/status" , "format=json" , "" ))
76+ netloc = parts .netloc
77+ # 0.0.0.0 is a bind-any address, not a valid connect target — map to loopback.
78+ if parts .hostname == "0.0.0.0" :
79+ netloc = netloc .replace ("0.0.0.0" , "127.0.0.1" , 1 )
80+ return urlunsplit ((parts .scheme , netloc , "/status" , "format=json" , "" ))
7481 return None
7582
7683
@@ -81,22 +88,51 @@ async def _probe_mcp() -> dict[str, Any]:
8188 except Exception :
8289 result = {"up" : False , "tool_count" : 0 }
8390
84- # Best-effort fetch of OBP-MCP's outbound auth mode. Failures here must not
85- # demote the overall MCP component — the tools cache is the source of truth
86- # for "up", and the auth mode is informational.
91+ # Auth-required OBP-MCP servers are never connected at startup (tools are
92+ # loaded per-request with the user's bearer token), so the tools cache says
93+ # nothing about reachability. When an OBP-MCP server is configured, probe
94+ # its /status endpoint directly: reachability drives "up", and the outbound
95+ # auth mode is included when the response parses.
8796 url = _obp_mcp_status_url ()
8897 if url :
98+ start = time .monotonic ()
99+ reachable = False
89100 try :
90101 timeout = aiohttp .ClientTimeout (total = _PROBE_TIMEOUT_SEC )
91102 async with aiohttp .ClientSession (timeout = timeout ) as session :
92103 async with session .get (url , headers = {"Accept" : "application/json" }) as resp :
104+ reachable = resp .status < 500
93105 if resp .status == 200 :
94- data = await resp .json ()
95- mode = (data .get ("auth" ) or {}).get ("outbound_auth_via" )
96- if mode :
97- result ["obp_mcp_outbound_auth_via" ] = mode
106+ try :
107+ data = await resp .json ()
108+ mode = (data .get ("auth" ) or {}).get ("outbound_auth_via" )
109+ if mode :
110+ result ["obp_mcp_outbound_auth_via" ] = mode
111+ except Exception :
112+ pass # auth mode is informational only
98113 except Exception :
99- pass
114+ reachable = False
115+ result ["up" ] = reachable
116+ result ["latency_ms" ] = int ((time .monotonic () - start ) * 1000 )
117+
118+ # Protocol-level test call: perform a real MCP handshake + tools/list over the
119+ # same loader the agent uses. MCPToolLoader is used directly (not the
120+ # create_mcp_tools_with_auth wrapper, which swallows connection errors into an
121+ # empty list). Unauthenticated — succeeds when the server's inbound auth is
122+ # disabled (e.g. consent-based OBP-MCP setups); with inbound auth enabled it
123+ # may fail, so a failure is reported but does not demote "up".
124+ configs = get_server_configs ()
125+ if configs :
126+ try :
127+ loader = MCPToolLoader (servers = configs )
128+ tools = await asyncio .wait_for (
129+ loader .load_tools (), timeout = _MCP_TEST_CALL_TIMEOUT_SEC
130+ )
131+ result ["test_call" ] = "ok"
132+ result ["tool_count" ] = len (tools )
133+ except Exception as e :
134+ logger .warning (f"MCP test call failed: { type (e ).__name__ } : { e } " )
135+ result ["test_call" ] = "failed"
100136
101137 return result
102138
@@ -172,6 +208,8 @@ def render_status_html(status: dict[str, Any]) -> str:
172208 extras .append (f"{ int (data ['latency_ms' ])} ms" )
173209 if "tool_count" in data :
174210 extras .append (f"{ int (data ['tool_count' ])} tools" )
211+ if "test_call" in data :
212+ extras .append (f"test call: { data ['test_call' ]} " )
175213 if "obp_mcp_outbound_auth_via" in data :
176214 extras .append (f"OBP-MCP auth: { data ['obp_mcp_outbound_auth_via' ]} " )
177215 extra_text = html .escape (" · " .join (extras )) if extras else ""
0 commit comments