From 36820d6897d89502719c58fd105e500051a65366 Mon Sep 17 00:00:00 2001 From: Mo Chahadeh Date: Fri, 29 May 2026 14:10:22 +0300 Subject: [PATCH] Fix MCP tool launch timeout on macos / unable to connect --- package-lock.json | 50 ++++++++++++------------ python/commands/library_symbol.py | 65 +++++++++++++++++++++++-------- setup-macos.sh | 0 3 files changed, 74 insertions(+), 41 deletions(-) mode change 100644 => 100755 setup-macos.sh diff --git a/package-lock.json b/package-lock.json index 6e716ed..2199fbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,9 +102,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -650,9 +650,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1246,9 +1246,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1432,12 +1432,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -1470,9 +1470,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -1705,9 +1705,9 @@ } }, "node_modules/hono": { - "version": "4.12.16", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz", - "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1783,9 +1783,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -2267,9 +2267,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/python/commands/library_symbol.py b/python/commands/library_symbol.py index 39bae26..32a90b2 100644 --- a/python/commands/library_symbol.py +++ b/python/commands/library_symbol.py @@ -8,6 +8,7 @@ and providing search functionality for component selection. import logging import os import re +import threading from dataclasses import asdict, dataclass from pathlib import Path from typing import Dict, List, Optional @@ -54,22 +55,37 @@ class SymbolLibraryManager: self.project_path = project_path self.libraries: Dict[str, str] = {} # nickname -> path mapping self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo] + self._cache_lock = threading.Lock() self._load_libraries() self._warm_cache() def _warm_cache(self) -> None: - """Pre-parse all symbol libraries so the first search is fast. + """Pre-parse all symbol libraries in the background so the first search + is fast. - Without this, the first ``search_symbols`` call parses every - .kicad_sym file on demand, which can take 30-120 s across - 200+ libraries. By populating the cache here (during startup, - before the READY handshake) the cost is paid once. + Without warming, the first ``search_symbols`` call parses every + .kicad_sym file on demand, which can take tens of seconds across + 200+ libraries. Warming pays that cost ahead of time. + + Crucially this runs on a daemon thread so it does NOT block + ``__init__``. The interface is constructed before the process emits + its READY handshake to the TypeScript host; warming synchronously here + delayed (and on large libraries effectively prevented) that handshake, + so the MCP server never finished starting. ``list_symbols`` is + cache-locked, so on-demand parses race-free with this background warm. """ - for nickname in list(self.libraries.keys()): - try: - self.list_symbols(nickname) - except Exception: - logger.debug("Skipping unparseable library: %s", nickname) + + def _warm() -> None: + for nickname in list(self.libraries.keys()): + try: + self.list_symbols(nickname) + except Exception: + logger.debug("Skipping unparseable library: %s", nickname) + logger.info("Symbol cache warm-up complete (%d libraries)", len(self.libraries)) + + threading.Thread( + target=_warm, name="symbol-cache-warmup", daemon=True + ).start() def _load_libraries(self) -> None: """Load libraries from sym-lib-table files""" @@ -287,13 +303,27 @@ class SymbolLibraryManager: # Find the start position of this symbol start_pos = match.start() - # Walk forward tracking parenthesis depth to find the true end of the block + # Walk forward tracking parenthesis depth to find the true end of + # the block. Parens inside quoted strings (e.g. a Description like + # "MCU (LQFP-64)") must be ignored, otherwise the depth counter + # never rebalances and the walk runs to EOF for every symbol — + # turning a multi-MB library into an O(n^2) scan (minutes per file) + # and falsely flagging well-formed symbols as malformed. depth = 0 i = start_pos end_pos = start_pos + in_string = False while i < len(content): ch = content[i] - if ch == "(": + if in_string: + if ch == "\\": + i += 2 # skip escaped char + continue + if ch == '"': + in_string = False + elif ch == '"': + in_string = True + elif ch == "(": depth += 1 elif ch == ")": depth -= 1 @@ -373,8 +403,9 @@ class SymbolLibraryManager: List of SymbolInfo objects """ # Check cache first - if library_nickname in self.symbol_cache: - return self.symbol_cache[library_nickname] + cached = self.symbol_cache.get(library_nickname) + if cached is not None: + return cached library_path = self.libraries.get(library_nickname) if not library_path: @@ -384,8 +415,10 @@ class SymbolLibraryManager: # Parse the library file symbols = self._parse_kicad_sym_file(library_path, library_nickname) - # Cache the results - self.symbol_cache[library_nickname] = symbols + # Cache the results. Guarded so an on-demand parse and the background + # warm-up thread can't corrupt the dict mid-update. + with self._cache_lock: + self.symbol_cache[library_nickname] = symbols return symbols diff --git a/setup-macos.sh b/setup-macos.sh old mode 100644 new mode 100755