Connect all enabled, configured MCP servers. Returns True if at least one tool was registered. Fully guarded: any failure leaves the runtime empty and never raises.
(self)
| 66 | return asyncio.run_coroutine_threadsafe(coro, self._loop).result(timeout) |
| 67 | |
| 68 | def start(self) -> bool: |
| 69 | """Connect all enabled, configured MCP servers. Returns True if at least |
| 70 | one tool was registered. Fully guarded: any failure leaves the runtime |
| 71 | empty and never raises.""" |
| 72 | try: |
| 73 | from src.services.mcp.config import get_all_mcp_configs |
| 74 | except Exception: # noqa: BLE001 |
| 75 | logger.debug("[mcp] config module unavailable", exc_info=True) |
| 76 | return False |
| 77 | try: |
| 78 | configs = get_all_mcp_configs() |
| 79 | except Exception: # noqa: BLE001 |
| 80 | logger.debug("[mcp] reading configs failed", exc_info=True) |
| 81 | return False |
| 82 | enabled = { |
| 83 | name: scoped |
| 84 | for name, scoped in (configs or {}).items() |
| 85 | if getattr(getattr(scoped, "config", None), "enabled", True) |
| 86 | } |
| 87 | if not enabled: |
| 88 | return False |
| 89 | |
| 90 | self._loop = asyncio.new_event_loop() |
| 91 | self._thread = threading.Thread( |
| 92 | target=self._loop.run_forever, daemon=True, name="mcp-loop" |
| 93 | ) |
| 94 | self._thread.start() |
| 95 | |
| 96 | from src.services.mcp.client import McpClient |
| 97 | |
| 98 | for name, scoped in enabled.items(): |
| 99 | try: |
| 100 | client = McpClient() |
| 101 | self._run(client.connect(name, scoped), _CONNECT_TIMEOUT_S) |
| 102 | mcp_tools = self._run(client.list_tools(), _CONNECT_TIMEOUT_S) |
| 103 | self.clients[name] = client |
| 104 | self.servers[name] = [t.name for t in mcp_tools] |
| 105 | for mt in mcp_tools: |
| 106 | self.tools.append(self._wrap(name, mt, client)) |
| 107 | logger.info("[mcp] connected %s (%d tools)", name, len(mcp_tools)) |
| 108 | except Exception: # noqa: BLE001 — one bad server must not sink the rest |
| 109 | logger.exception("[mcp] connect failed: %s", name) |
| 110 | |
| 111 | if not self.tools: |
| 112 | self.shutdown() |
| 113 | return False |
| 114 | return True |
| 115 | |
| 116 | def _wrap(self, server: str, mcp_tool: Any, client: Any) -> Any: |
| 117 | """Build a loop-correct sync Tool that dispatches to the connection loop.""" |