{"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":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":4918,"sourceCode":"        let route = match self.resolve_advertised_tool(prefixed_name).await {\n            Ok(route) => route,\n            Err(error) => {\n                // A real tool name reached a server whose login has lapsed\n                // (stale catalog, or a mid-session 401). Name the one call\n                // that recovers it instead of leaving a dead error.\n                if let Some(server) = self.needs_auth_server_for_tool_name(prefixed_name) {\n                    return Err(error.context(self.auth_required_hint(&server)));\n                }\n                return Err(error);\n            }\n        };\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        let result = match conn.call_tool(&tool_name, arguments.clone(), timeout).await {\n            Ok(result) => Ok(result),\n            // A rejected credential is not a stale session: reconnecting\n            // replays the same rejection, so it takes the auth-required\n            // path below instead of the transparent retry.","sourceCodeStart":4900,"sourceCodeEnd":4936,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp.rs#L4900-L4936","documentation":"The MCP connection's tool catalog was regenerated (generation counter bumped) after a call route was resolved against an older catalog snapshot. The library bails instead of calling a tool that may no longer exist or have a different schema, so stale resolutions fail fast and the caller can re-resolve and retry.","triggerScenarios":"Calling an MCP tool through a route whose catalog_generation no longer matches the connection's current generation — i.e. the server's tool list changed (tools added/removed/updated) between route resolution and call_tool, e.g. after a reconnect or a server-initiated list-changed notification.","commonSituations":"A long-running session where the MCP server hot-reloads its tool list; a server restart or upgrade mid-session; reconnect logic refreshing tools while an old cached route is still in use.","solutions":["Re-resolve the tool route against the current catalog, then retry the call","Refresh the connection (reconnect / re-list tools) so the route and catalog generations align","Check whether the MCP server is restarting or updating tools mid-call and pin to a stable server version"],"exampleFix":"// before\nlet result = engine.call_route(&stale_route, &args).await?;\n// after\nlet route = match engine.call_route(&route, &args).await {\n    Err(e) if e.to_string().contains(\"catalog changed\") => {\n        let route = engine.resolve_route(&route.server_name, &route.tool_name).await?;\n        engine.call_route(&route, &args).await?\n    }\n    r => r?,\n};","handlingStrategy":"retry","validationCode":"// before calling\nif conn.catalog_generation != route.catalog_generation {\n    route = resolve_route(&route.server_name, &route.tool_name).await?;\n}","typeGuard":null,"tryCatchPattern":"match call_route(&route, args).await {\n    Err(e) if e.to_string().contains(\"catalog changed\") => {\n        let route = resolve_route(&route.server_name, &route.tool_name).await?;\n        call_route(&route, args).await // retry once with fresh route\n    }\n    other => other,\n}","preventionTips":["Resolve routes immediately before use instead of caching them across turns","Listen for server tool-list-changed notifications and invalidate cached routes","Avoid long-lived sessions against MCP servers that hot-reload their tool lists"],"tags":["mcp","stale-cache","retry"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}