ultraworkers/claw-code · critical
mcp registry lock poisoned
Error message
mcp registry lock poisoned
What it means
Panic from McpToolBridge/McpRegistry::register_server (mcp_tool_bridge.rs:100): inserting a server's state (status, tools, resources, server_info) takes the registry Mutex, and .expect("mcp registry lock poisoned") unwinds when that Mutex was poisoned by an earlier panic on a thread holding it. Registration happens during MCP server connection setup, so one poisoned lock breaks every subsequent server connection.
Source
Thrown at rust/crates/runtime/src/mcp_tool_bridge.rs:100
Self::default()
}
pub fn set_manager(
&self,
manager: Arc<Mutex<McpServerManager>>,
) -> Result<(), Arc<Mutex<McpServerManager>>> {
self.manager.set(manager)
}
pub fn register_server(
&self,
server_name: &str,
status: McpConnectionStatus,
tools: Vec<McpToolInfo>,
resources: Vec<McpResourceInfo>,
server_info: Option<String>,
) {
let mut inner = self.inner.lock().expect("mcp registry lock poisoned");
inner.insert(
server_name.to_owned(),
McpServerState {
server_name: server_name.to_owned(),
status,
tools,
resources,
server_info,
error_message: None,
},
);
}
pub fn get_server(&self, server_name: &str) -> Option<McpServerState> {
let inner = self.inner.lock().expect("mcp registry lock poisoned");
inner.get(server_name).cloned()
}
View on GitHub (pinned to 08106b0c37)
Solutions
- RUST_BACKTRACE=1 and fix the first panic under the lock — commonly an unwrap on malformed MCP server responses; convert to error propagation
- Restart the process; the poisoned Mutex never heals
- Maintainer fix: self.inner.lock().unwrap_or_else(|poison| poison.into_inner()) — a HashMap insert is safe even after a foreign panic
- Keep MCP server I/O and deserialization outside the registry critical section so protocol bugs can't poison the lock
Example fix
// before (runtime/src/mcp_tool_bridge.rs:100)
let mut inner = self.inner.lock().expect("mcp registry lock poisoned");
// after
let mut inner = self
.inner
.lock()
.unwrap_or_else(|poison| poison.into_inner()); Defensive patterns
Strategy: try-catch
Try / catch
let registered = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
bridge.register_server(name, status, tools, resources, info);
}));
if registered.is_err() {
// poisoned registry: rebuild bridge before reconnecting servers
bridge = McpToolBridge::new();
} Prevention
- Parse and validate MCP initialize responses before taking the registry lock; protocol bugs must not poison registration
- One poisoned lock breaks all subsequent server connections — monitor for the first panic, not the cascade
- Keep connection retry loops from hammering register_server after a panic; rebuild the registry instead
When it happens
Trigger: Calling register_server after the MCP connect/lifecycle path panicked earlier while holding this registry's lock — e.g. a JSON-RPC handshake unwrap, serde parse panic, or the "MCP tool call thread panicked" path seen at mcp_tool_bridge.rs:238-246 which catches a tool-call panic but can occur while lock discipline was already broken elsewhere.
Common situations: An MCP server returns malformed JSON on initialize and a parse unwrap panics with the lock held; later reconnect attempts call register_server and panic in a loop; multi-server setups where one bad server poisons registration for all others.
Related errors
- lsp registry lock poisoned
- registry lock poisoned
- team registry lock poisoned
- cron registry lock poisoned
- worker registry lock poisoned
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/8aaffb66792db47d.
Report an issue: GitHub.