Skip to content

Commit d3fdcca

Browse files
committed
fix: run sync MCP handlers in thread pool, prevent mermaid validation hang
- Wrap synchronous tool handlers in asyncio.to_thread() to avoid blocking the event loop (analyze_repo excluded — Tree-sitter C extensions are not thread-safe) - Disable mermaid-py validation by default (set MERMAID_VALIDATE=1 to enable), add 15s timeout to prevent indefinite hangs
1 parent 5db73ac commit d3fdcca

2 files changed

Lines changed: 27 additions & 7 deletions

File tree

codewiki/mcp/server.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -415,21 +415,26 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
415415
"""Route tool calls to the appropriate handler."""
416416
try:
417417
# --- Fine-grained tools (no LLM config needed) ---
418+
# Synchronous handlers run via asyncio.to_thread() so they never
419+
# block the event loop (which would hang the MCP stdio server).
418420
if name == "analyze_repo":
419421
from codewiki.mcp.tools.analysis import handle_analyze_repo
422+
# NOTE: Tree-sitter C extensions are not thread-safe, so this
423+
# must run on the main thread (blocking the event loop is
424+
# acceptable for this one-time heavy operation).
420425
return [_text(handle_analyze_repo(arguments, _store))]
421426

422427
elif name == "read_code_components":
423428
from codewiki.mcp.tools.code_reader import handle_read_code_components
424-
return [_text(handle_read_code_components(arguments, _store))]
429+
return [_text(await asyncio.to_thread(handle_read_code_components, arguments, _store))]
425430

426431
elif name == "list_components":
427432
from codewiki.mcp.tools.analysis import handle_list_components
428-
return [_text(handle_list_components(arguments, _store))]
433+
return [_text(await asyncio.to_thread(handle_list_components, arguments, _store))]
429434

430435
elif name == "view_repo_file":
431436
from codewiki.mcp.tools.code_reader import handle_view_repo_file
432-
return [_text(handle_view_repo_file(arguments, _store))]
437+
return [_text(await asyncio.to_thread(handle_view_repo_file, arguments, _store))]
433438

434439
elif name == "write_doc_file":
435440
from codewiki.mcp.tools.doc_writer import handle_write_doc_file
@@ -443,15 +448,15 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
443448

444449
elif name == "save_module_tree":
445450
from codewiki.mcp.tools.module_tree import handle_save_module_tree
446-
return [_text(handle_save_module_tree(arguments, _store))]
451+
return [_text(await asyncio.to_thread(handle_save_module_tree, arguments, _store))]
447452

448453
elif name == "get_processing_order":
449454
from codewiki.mcp.tools.module_tree import handle_get_processing_order
450-
return [_text(handle_get_processing_order(arguments, _store))]
455+
return [_text(await asyncio.to_thread(handle_get_processing_order, arguments, _store))]
451456

452457
elif name == "get_prompt":
453458
from codewiki.mcp.tools.prompt_server import handle_get_prompt
454-
return [_text(handle_get_prompt(arguments, _store))]
459+
return [_text(await asyncio.to_thread(handle_get_prompt, arguments, _store))]
455460

456461
elif name == "close_session":
457462
sid = arguments["session_id"]

codewiki/src/be/utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import os
23
import re
34
import sys
45
import threading
@@ -150,6 +151,12 @@ def extract_mermaid_blocks(content: str) -> List[Tuple[int, str]]:
150151
# Skip it proactively so SpiderMonkey is never loaded into the process.
151152
_PYTHONMONKEY_BROKEN = sys.version_info >= (3, 12)
152153

154+
# mermaid-py spawns a Node.js subprocess that can hang indefinitely (e.g. when
155+
# Node.js is missing or the mermaid CLI is misconfigured). Default to
156+
# disabled; set MERMAID_VALIDATE=1 to enable.
157+
_MERMAID_PY_BROKEN = os.environ.get("MERMAID_VALIDATE", "0") != "1"
158+
_MERMAID_PY_PROBED = True # Skip probing — rely on env var
159+
153160

154161
async def _try_pythonmonkey_parse(diagram_content: str) -> str | None:
155162
"""Attempt to parse via PythonMonkey/mermaid-parser-py.
@@ -232,8 +239,16 @@ async def validate_single_diagram(diagram_content: str, diagram_num: int, line_s
232239
"""
233240
core_error = await _try_pythonmonkey_parse(diagram_content)
234241
if core_error is None:
242+
# Both PythonMonkey (3.12+) and mermaid-py (env disabled) are unavailable
243+
if _MERMAID_PY_BROKEN:
244+
return "" # Skip validation gracefully
235245
try:
236-
core_error = _parse_via_mermaid_py(diagram_content)
246+
core_error = await asyncio.wait_for(
247+
asyncio.to_thread(_parse_via_mermaid_py, diagram_content),
248+
timeout=15.0,
249+
)
250+
except asyncio.TimeoutError:
251+
return "" # Graceful skip on timeout
237252
except Exception as e:
238253
return f" Diagram {diagram_num}: Exception during validation - {str(e)}"
239254

0 commit comments

Comments
 (0)