Skip to content

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/search endpoint — runs BM25 scoring, dense retrieval, and RRF fusion
  • /ir/index endpoint — 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 compute
  • work_buddy/ir/dense.py_IN_SERVICE flag
  • work_buddy/mcp_server/context_wrappers.py — gateway-callable wrappers following the correct pattern