Hmbown/CodeWhale · error
MCP catalog changed after tool resolution; retry the call
Error message
MCP catalog changed after tool resolution; retry the call
What it means
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.
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
Example fix
// before
let result = engine.call_route(&stale_route, &args).await?;
// after
let route = match engine.call_route(&route, &args).await {
Err(e) if e.to_string().contains("catalog changed") => {
let route = engine.resolve_route(&route.server_name, &route.tool_name).await?;
engine.call_route(&route, &args).await?
}
r => r?,
}; Defensive patterns
Strategy: retry
Validate before calling
// before calling
if conn.catalog_generation != route.catalog_generation {
route = resolve_route(&route.server_name, &route.tool_name).await?;
} Try / catch
match call_route(&route, args).await {
Err(e) if e.to_string().contains("catalog changed") => {
let route = resolve_route(&route.server_name, &route.tool_name).await?;
call_route(&route, args).await // retry once with fresh route
}
other => other,
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- {backoff.last_error}
- MCP session expired
- A managed, project or plugin connector already uses this…
- Absolute path should not warn
- active plugin registry is missing its pre-dotenv…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/b21264f53b4e4af0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp.rs:4918
let route = match self.resolve_advertised_tool(prefixed_name).await {
Ok(route) => route,
Err(error) => {
// A real tool name reached a server whose login has lapsed
// (stale catalog, or a mid-session 401). Name the one call
// that recovers it instead of leaving a dead error.
if let Some(server) = self.needs_auth_server_for_tool_name(prefixed_name) {
return Err(error.context(self.auth_required_hint(&server)));
}
return Err(error);
}
};
let server_name = route.server_name.clone();
let tool_name = route.tool_name.clone();
// Copy the global timeouts to avoid borrow conflict
let global_timeouts = self.config.timeouts;
let conn = self.get_or_connect(&server_name).await?;
if conn.catalog_generation != route.catalog_generation {
anyhow::bail!("MCP catalog changed after tool resolution; retry the call");
}
if conn
.config()
.reviewed_plugin
.as_ref()
.map(|source| &source.authority)
!= route.plugin_authority.as_ref()
|| !conn.config().is_tool_enabled(&tool_name)
|| !conn.tools().iter().any(|tool| tool.name == tool_name)
{
anyhow::bail!("MCP tool '{tool_name}' is disabled for server '{server_name}'");
}
let timeout = conn.config().effective_execute_timeout(&global_timeouts);
let result = match conn.call_tool(&tool_name, arguments.clone(), timeout).await {
Ok(result) => Ok(result),
// A rejected credential is not a stale session: reconnecting
// replays the same rejection, so it takes the auth-required
// path below instead of the transparent retry.View on GitHub (pinned to 73e0f67d83)