diff --git a/nanobot/agent/tools/mcp.py b/nanobot/agent/tools/mcp.py index ea93422e..f5a41e10 100644 --- a/nanobot/agent/tools/mcp.py +++ b/nanobot/agent/tools/mcp.py @@ -166,12 +166,28 @@ async def _probe_http_url(url: str, timeout: float = 3.0) -> bool: return False +def _redact_url(url: str) -> str: + """Strip credentials and query/fragment before logging an MCP URL. + + Server URLs may embed secrets (``https://user:token@host/sse`` or a + ``?token=`` query); only scheme, host, port, and path are safe to log. + """ + try: + parts = urllib.parse.urlsplit(url) + netloc = parts.hostname or "" + if parts.port: + netloc = f"{netloc}:{parts.port}" + return urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, "", "")) + except Exception: + return "" + + async def _validate_mcp_request_url(request: httpx.Request) -> None: """Validate each outgoing MCP HTTP request, including redirect targets.""" ok, error = validate_url_target(str(request.url)) if not ok: raise httpx.RequestError( - f"Blocked unsafe MCP URL {request.url} ({error})", + f"Blocked unsafe MCP URL {_redact_url(str(request.url))} ({error})", request=request, ) @@ -774,7 +790,7 @@ async def connect_mcp_servers( logger.warning( "MCP server '{}': blocked unsafe URL {} ({})", name, - cfg.url, + _redact_url(cfg.url), error, ) await server_stack.aclose() @@ -795,7 +811,7 @@ async def connect_mcp_servers( read, write = await server_stack.enter_async_context(stdio_client(params)) elif transport_type == "sse": if not await _probe_http_url(cfg.url): - logger.warning("MCP server '{}': {} unreachable, skipping", name, cfg.url) + logger.warning("MCP server '{}': {} unreachable, skipping", name, _redact_url(cfg.url)) await server_stack.aclose() return name, None @@ -822,7 +838,7 @@ async def connect_mcp_servers( ) elif transport_type == "streamableHttp": if not await _probe_http_url(cfg.url): - logger.warning("MCP server '{}': {} unreachable, skipping", name, cfg.url) + logger.warning("MCP server '{}': {} unreachable, skipping", name, _redact_url(cfg.url)) await server_stack.aclose() return name, None diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index a5a8d585..c2eb81e5 100644 --- a/tests/tools/test_mcp_tool.py +++ b/tests/tools/test_mcp_tool.py @@ -1240,3 +1240,15 @@ async def test_connect_mcp_servers_enabled_tools_matches_sanitized_name( await stack.aclose() assert registry.tool_names == ["mcp_test_My_Tool"] + + +@pytest.mark.parametrize( + "url, expected", + [ + ("https://user:secret@host.example/sse", "https://host.example/sse"), + ("https://host.example:8443/mcp?token=abc#frag", "https://host.example:8443/mcp"), + ("https://host.example/sse", "https://host.example/sse"), + ], +) +def test_redact_url_strips_credentials_and_query(url: str, expected: str) -> None: + assert mcp_mod._redact_url(url) == expected