Fix MCP tool launch timeout on macos / unable to connect
This commit is contained in:
50
package-lock.json
generated
50
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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
0
setup-macos.sh
Normal file → Executable file
Reference in New Issue
Block a user