Fix MCP tool launch timeout on macos / unable to connect

This commit is contained in:
Mo Chahadeh
2026-05-29 14:10:22 +03:00
parent d2ea43d061
commit 36820d6897
3 changed files with 74 additions and 41 deletions

50
package-lock.json generated
View File

@@ -102,9 +102,9 @@
} }
}, },
"node_modules/@eslint/config-array/node_modules/brace-expansion": { "node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "5.0.5", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -650,9 +650,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.5", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1246,9 +1246,9 @@
} }
}, },
"node_modules/eslint/node_modules/brace-expansion": { "node_modules/eslint/node_modules/brace-expansion": {
"version": "5.0.5", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1432,12 +1432,12 @@
} }
}, },
"node_modules/express-rate-limit": { "node_modules/express-rate-limit": {
"version": "8.3.2", "version": "8.5.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ip-address": "10.1.0" "ip-address": "^10.2.0"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -1470,9 +1470,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.0", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -1705,9 +1705,9 @@
} }
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "4.12.16", "version": "4.12.23",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==", "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
@@ -1783,9 +1783,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "10.1.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 12" "node": ">= 12"
@@ -2267,9 +2267,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.1", "version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"

View File

@@ -8,6 +8,7 @@ and providing search functionality for component selection.
import logging import logging
import os import os
import re import re
import threading
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
@@ -54,22 +55,37 @@ class SymbolLibraryManager:
self.project_path = project_path self.project_path = project_path
self.libraries: Dict[str, str] = {} # nickname -> path mapping self.libraries: Dict[str, str] = {} # nickname -> path mapping
self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo] self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo]
self._cache_lock = threading.Lock()
self._load_libraries() self._load_libraries()
self._warm_cache() self._warm_cache()
def _warm_cache(self) -> None: 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 Without warming, the first ``search_symbols`` call parses every
.kicad_sym file on demand, which can take 30-120 s across .kicad_sym file on demand, which can take tens of seconds across
200+ libraries. By populating the cache here (during startup, 200+ libraries. Warming pays that cost ahead of time.
before the READY handshake) the cost is paid once.
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: def _warm() -> None:
self.list_symbols(nickname) for nickname in list(self.libraries.keys()):
except Exception: try:
logger.debug("Skipping unparseable library: %s", nickname) 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: def _load_libraries(self) -> None:
"""Load libraries from sym-lib-table files""" """Load libraries from sym-lib-table files"""
@@ -287,13 +303,27 @@ class SymbolLibraryManager:
# Find the start position of this symbol # Find the start position of this symbol
start_pos = match.start() 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 depth = 0
i = start_pos i = start_pos
end_pos = start_pos end_pos = start_pos
in_string = False
while i < len(content): while i < len(content):
ch = content[i] 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 depth += 1
elif ch == ")": elif ch == ")":
depth -= 1 depth -= 1
@@ -373,8 +403,9 @@ class SymbolLibraryManager:
List of SymbolInfo objects List of SymbolInfo objects
""" """
# Check cache first # Check cache first
if library_nickname in self.symbol_cache: cached = self.symbol_cache.get(library_nickname)
return self.symbol_cache[library_nickname] if cached is not None:
return cached
library_path = self.libraries.get(library_nickname) library_path = self.libraries.get(library_nickname)
if not library_path: if not library_path:
@@ -384,8 +415,10 @@ class SymbolLibraryManager:
# Parse the library file # Parse the library file
symbols = self._parse_kicad_sym_file(library_path, library_nickname) symbols = self._parse_kicad_sym_file(library_path, library_nickname)
# Cache the results # Cache the results. Guarded so an on-demand parse and the background
self.symbol_cache[library_nickname] = symbols # warm-up thread can't corrupt the dict mid-update.
with self._cache_lock:
self.symbol_cache[library_nickname] = symbols
return symbols return symbols

0
setup-macos.sh Normal file → Executable file
View File