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

  1. RUST_BACKTRACE=1 and fix the first panic under the lock — commonly an unwrap on malformed MCP server responses; convert to error propagation
  2. Restart the process; the poisoned Mutex never heals
  3. Maintainer fix: self.inner.lock().unwrap_or_else(|poison| poison.into_inner()) — a HashMap insert is safe even after a foreign panic
  4. 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

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


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/8aaffb66792db47d. Report an issue: GitHub.