MCP Server Import Discipline¶
Critical safety constraint: why heavy library imports in capability callables deadlock the MCP server, and the correct pattern to avoid it
Details¶
Rule¶
The MCP server process must never import heavy compute libraries in capability callables. This includes numpy, rank_bm25, sentence-transformers, and sqlite3 (via ir.store).
All heavy compute goes through the embedding service HTTP API (localhost:5124).
Why: the deadlock mechanism¶
The MCP server uses asyncio.to_thread() to dispatch capability callables to a thread pool. If a callable does a deferred import of a heavy module (e.g., from work_buddy.ir.engine import search), the import triggers Python's per-module import lock. The main thread (running the asyncio event loop) may also need import locks for its own operations. Result: permanent deadlock.
Step-by-step¶
1. Claude calls `wb_run("context_search", ...)`
2. Gateway submits callable to thread pool via asyncio.to_thread()
3. Worker thread starts executing the callable
4. Callable hits: from work_buddy.ir.engine import search
5. This triggers loading numpy, rank_bm25, sqlite3 — heavy C extensions
6. Python import system acquires per-module locks for each module in the chain
7. Main thread's event loop needs one of those locks (for internal lazy imports)
8. DEADLOCK: worker holds locks, waits for event loop; event loop waits for worker
This was discovered and fixed on April 6, 2026. The original symptom was context_search hanging for 30+ seconds on first request — debug checkpoints confirmed execution reached the function body but never completed the ir.engine import.
The correct pattern¶
All heavy compute runs in the embedding service (work_buddy/embedding/service.py), which runs in its own process and already imports numpy/rank_bm25/sentence-transformers:
/ir/searchendpoint — runs BM25 scoring, dense retrieval, and RRF fusion/ir/indexendpoint — builds/checks the search index
The MCP server's _ir_search_dispatch and _ir_index_dispatch call client.ir_search() and client.ir_index() — lightweight HTTP requests via urllib, no heavy imports.
The _IN_SERVICE flag¶
The _IN_SERVICE flag in ir/dense.py lets the embedding service call models directly (avoiding HTTP self-calls) while external callers still use the HTTP API.
Safe vs unsafe imports in capability callables¶
| Safe | Unsafe |
|---|---|
urllib, json, pathlib |
numpy, rank_bm25 |
work_buddy.config, work_buddy.paths |
work_buddy.ir.store, work_buddy.ir.engine |
| HTTP calls to embedding service | sqlite3 (loaded by ir.store) |
work_buddy.obsidian.bridge |
sentence_transformers |
Key files¶
work_buddy/mcp_server/registry.py— capability registration (deadlock warnings in_build_registry()and_context_capabilities())work_buddy/embedding/service.py— the correct home for heavy computework_buddy/ir/dense.py—_IN_SERVICEflagwork_buddy/mcp_server/context_wrappers.py— gateway-callable wrappers following the correct pattern