{"record":{"id":"b21264f53b4e4af0","repo":"Hmbown/CodeWhale","slug":"mcp-catalog-changed-after-tool-resolution-retry-t","errorCode":null,"errorMessage":"MCP catalog changed after tool resolution; retry the call","messagePattern":"MCP catalog changed after tool resolution; retry the call","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/tui/src/mcp.rs","lineNumber":3314,"sourceCode":"            let name = arguments\n                .get(\"name\")\n                .and_then(|v| v.as_str())\n                .context(\"Missing 'name' argument\")?;\n            let args = arguments\n                .get(\"arguments\")\n                .cloned()\n                .unwrap_or(serde_json::json!({}));\n            return self.get_prompt(server_name, name, args).await;\n        }\n\n        let route = self.resolve_advertised_tool(prefixed_name).await?;\n        let server_name = route.server_name.clone();\n        let tool_name = route.tool_name.clone();\n        // Copy the global timeouts to avoid borrow conflict\n        let global_timeouts = self.config.timeouts;\n        let conn = self.get_or_connect(&server_name).await?;\n        if conn.catalog_generation != route.catalog_generation {\n            anyhow::bail!(\"MCP catalog changed after tool resolution; retry the call\");\n        }\n        if conn\n            .config()\n            .reviewed_plugin\n            .as_ref()\n            .map(|source| &source.authority)\n            != route.plugin_authority.as_ref()\n            || !conn.config().is_tool_enabled(&tool_name)\n            || !conn.tools().iter().any(|tool| tool.name == tool_name)\n        {\n            anyhow::bail!(\"MCP tool '{tool_name}' is disabled for server '{server_name}'\");\n        }\n        let timeout = conn.config().effective_execute_timeout(&global_timeouts);\n        match conn.call_tool(&tool_name, arguments.clone(), timeout).await {\n            Ok(result) => Ok(result),\n            Err(err) if is_mcp_stale_session_error(&err) => {\n                tracing::debug!(\n                    target: \"mcp\",","sourceCodeStart":3296,"sourceCodeEnd":3332,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L3296-L3332","documentation":"Every MCP connection carries a catalog_generation counter that the manager bumps whenever dynamic servers are registered/removed or a connection is refreshed/dropped (crates/tui/src/mcp.rs:2567,2618,3400,3410). call_tool resolves an advertised tool against a snapshot of that generation; if the live connection's generation differs by the time the call is about to execute, the resolved route may name a tool or authority that no longer exists, so the call is aborted and the caller must re-resolve before retrying.","triggerScenarios":"Calling manager.call_tool while another task runs add_runtime_server_config/remove_runtime_server_config, refreshes a server catalog, or triggers drop_connection between resolve_advertised_tool and get_or_connect.","commonSituations":"An agent turn executing tools while the user adds/removes an MCP server in another pane; a background reconnect racing an in-flight tool call; a flaky server that keeps being dropped and re-added mid-call.","solutions":["Retry the call from the top (re-resolve the tool name) with a small bounded number of attempts","Serialize dynamic server registration/removal with tool invocation (single writer for catalog mutations)","If it persists, trace catalog_generation bumps to find which code path keeps mutating the catalog mid-call","Confirm the tool still appears in the server's refreshed tools/list before retrying"],"exampleFix":"// before\nlet result = manager.call_tool(prefixed_name, args).await?;\n\n// after\nlet mut attempt = 0;\nlet result = loop {\n    attempt += 1;\n    match manager.call_tool(prefixed_name, args.clone()).await {\n        Ok(out) => break Ok(out),\n        Err(e) if e.to_string().contains(\"catalog changed after tool resolution\") && attempt < 3 => continue,\n        Err(e) => break Err(e),\n    }\n}?;","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nloop {\n    attempts += 1;\n    match manager.call_tool(name, args.clone()).await {\n        Ok(out) => return Ok(out),\n        Err(e) if e.to_string().contains(\"catalog changed after tool resolution\") && attempts < 3 => continue,\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Serialize dynamic MCP server add/remove with tool calls instead of mutating the catalog concurrently","Treat this error as transient: always re-resolve the tool before retrying rather than resending blindly","Bound retries (2-3 attempts) so a churn loop fails fast instead of spinning"],"tags":["mcp","concurrency","retry","race-condition"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}