diff --git a/navi/api/deps.py b/navi/api/deps.py index 57bf3b4..02df568 100644 --- a/navi/api/deps.py +++ b/navi/api/deps.py @@ -99,8 +99,25 @@ async def register_mcp_tools(registry: ToolRegistry, manager: McpManager) -> None: - """(kept for backward compat; MCP tools are already registered at startup).""" - pass + """Register all currently connected MCP tools into the tool registry.""" + from navi.mcp.tools import McpTool + + for name, client in manager.clients.items(): + if not client.connected: + continue + try: + tools = await client.list_tools() + for tool in tools: + mcp_tool = McpTool( + server_name=name, + tool_name=tool.name, + description=tool.description or "", + parameters=tool.inputSchema, + manager=manager, + ) + registry.register_external(mcp_tool) + except Exception as exc: + log.warning("Failed to register MCP tools for %r: %s", name, exc) # Backward-compat re-exports so nothing outside breaks diff --git a/navi/main.py b/navi/main.py index 16cfd13..7bd3ac1 100644 --- a/navi/main.py +++ b/navi/main.py @@ -25,6 +25,9 @@ ), ) +# Suppress noisy MCP SDK health-check chatter. +logging.getLogger("mcp.server.lowlevel.server").setLevel(logging.WARNING) + _base = Path(__file__).parent.parent diff --git a/navi/mcp/client.py b/navi/mcp/client.py index 9807441..7d10f30 100644 --- a/navi/mcp/client.py +++ b/navi/mcp/client.py @@ -58,6 +58,10 @@ if self._connected: return + # Ensure any stale transport/session from a prior disconnect or + # mark_disconnected() is fully torn down before we open fresh ones. + await self._cleanup() + try: if self.config.is_stdio: if not self.config.command: @@ -134,6 +138,7 @@ Used by the health-check loop when a server drops silently. """ self._connected = False + self._session = None async def _cleanup(self) -> None: try: diff --git a/navi/mcp/manager.py b/navi/mcp/manager.py index 65cff7f..60ba80f 100644 --- a/navi/mcp/manager.py +++ b/navi/mcp/manager.py @@ -35,6 +35,10 @@ self._health_check_task: asyncio.Task | None = None self._health_check_interval: float = 30.0 + # Last known connected status per server — used to suppress duplicate + # "connected" toast notifications on every health-check poll. + self._connected_status: dict[str, bool] = {} + @property def clients(self) -> dict[str, McpClient]: return self._clients @@ -84,12 +88,14 @@ try: await client.connect() self._clients[name] = client + self._connected_status[name] = True if self._on_server_connected: await self._on_server_connected(name) except Exception as exc: logger.warning("MCP server %r failed to connect: %s", name, exc) # Keep the client in the pool so health-check can retry later self._clients[name] = client + self._connected_status[name] = False async def reload_all(self) -> None: """Re-read the config file and reconnect every server.""" @@ -109,6 +115,7 @@ except Exception as exc: logger.warning("MCP server %r disconnect error: %s", name, exc) self._clients.clear() + self._connected_status.clear() async def get_all_tools(self) -> list[tuple[str, Any]]: """Return ``(server_name, mcp_tool)`` for every tool on every server. @@ -200,24 +207,31 @@ try: await client.connect() logger.info("MCP server %r reconnected by health check", name) + self._connected_status[name] = True if self._on_server_connected: await self._on_server_connected(name) except Exception as exc: logger.debug("MCP health-check reconnect failed for %r: %s", name, exc) continue - else: - # Live server — make sure it still responds - try: - await client.list_tools() - except Exception: - logger.warning("MCP server %r dropped during health check", name) - client.mark_disconnected() - await get_event_bus().publish( - McpStatusUpdate(server_name=name, status="disconnected") - ) - continue + continue - # If we get here the server is connected (either newly or verified) + # Live server — make sure it still responds + try: + await client.list_tools() + except Exception: + logger.warning("MCP server %r dropped during health check", name) + client.mark_disconnected() + self._connected_status[name] = False + await get_event_bus().publish( + McpStatusUpdate(server_name=name, status="disconnected") + ) + continue + + # Server is still connected — only notify if it was previously + # known as disconnected (recovery), not on every routine poll. + if self._connected_status.get(name): + continue + self._connected_status[name] = True try: tools = await client.list_tools() await get_event_bus().publish(