{"record":{"id":"f6660e7d6e210c6f","repo":"windmill-labs/windmill","slug":"the-tool-list-for-path-changed-while-it-was-loa","errorCode":null,"errorMessage":"The tool list for ${path} changed while it was loading. Try again.","messagePattern":"The tool list for (.+?) changed while it was loading\\. Try again\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"frontend/src/lib/components/copilot/chat/global/mcpTools.ts","lineNumber":61,"sourceCode":"\trevision?: string\n): Promise<McpToolDef[]> {\n\tconst key = `${workspace}:${path}:${revision ?? ''}`\n\tconst cached = toolsCache[key]\n\tif (cached && Date.now() - cached.at < TOOLS_CACHE_TTL_MS) {\n\t\treturn cached.tools\n\t}\n\t// A clear while this is in flight means the path may now name a different\n\t// server, and `readOnlyHint` decides whether a call needs confirmation — so the\n\t// answer is thrown away and asked again rather than cached or returned.\n\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\tconst generation = cacheGeneration\n\t\tconst tools = await ResourceService.getMcpTools({ workspace, path })\n\t\tif (generation === cacheGeneration) {\n\t\t\ttoolsCache[key] = { tools, at: Date.now() }\n\t\t\treturn tools\n\t\t}\n\t}\n\tthrow new Error(`The tool list for ${path} changed while it was loading. Try again.`)\n}\n\nexport function clearMcpToolsCache() {\n\tcacheGeneration++\n\ttoolsCache = {}\n}\n\n/**\n * Drop one server's listing after the backend refused the read-only assertion it\n * produced. Without this the write call the model is being sent to would be\n * rejected by the same stale `readOnlyHint`, leaving it with nowhere to go until\n * the entry expires.\n */\nfunction forgetServerTools(workspace: string, path: string) {\n\tcacheGeneration++\n\tconst prefix = `${workspace}:${path}:`\n\tfor (const key of Object.keys(toolsCache)) {\n\t\tif (key.startsWith(prefix)) delete toolsCache[key]","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/global/mcpTools.ts#L43-L79","documentation":"Thrown by loadServerTools in mcpTools.ts, used by the AI chat's global mode to fetch an MCP server's tool list with a generation-based cache. While the ResourceService.getMcpTools request is in flight, a cache invalidation (cacheGeneration bump via clearMcpToolsCache) makes the fetched list stale; the code loops/retries until the fetch lands in the current generation, and if the generation keeps changing it gives up with this error telling the caller to retry. It exists to prevent returning a tool list that no longer matches the server's current configuration.","triggerScenarios":"Calling the MCP tool-loading path (e.g. the chat's tools() flow) while something repeatedly invalidates the cache — editing/reconfiguring the MCP server, rapid re-connections, or concurrent clearMcpToolsCache() calls — so generation !== cacheGeneration on every completed fetch.","commonSituations":"A user edits the MCP server resource in another tab while the chat is loading tools; an agent session reconnects the MCP server mid-load; multiple chat messages in parallel each triggering cache clears; flapping MCP server connections causing repeated invalidations.","solutions":["Retry the operation — the error message says to try again once the MCP server configuration settles.","Wait for any in-flight edit/reconnect of the MCP server to finish, then reload the chat tools.","If it recurs constantly, find what is repeatedly calling clearMcpToolsCache (e.g. a reconnect loop) and fix that loop."],"exampleFix":"// before\nconst tools = await loadServerTools(workspace, path) // throws while cache keeps invalidating\n// after: retry with backoff\nlet tools\nfor (let i = 0; i < 3; i++) {\n  try { tools = await loadServerTools(workspace, path); break }\n  catch (e) { if (!/changed while it was loading/.test(e.message)) throw e; await sleep(500 * (i + 1)) }\n}","handlingStrategy":"retry","validationCode":"import { clearMcpToolsCache } from '$lib/components/copilot/chat/global/mcpTools'\n// don't clear the MCP tools cache while a load is in flight\nclearMcpToolsCache()\nawait loadServerTools(workspace, path)","typeGuard":null,"tryCatchPattern":"try {\n  tools = await loadServerTools(workspace, path)\n} catch (e) {\n  if (/changed while it was loading/.test(e.message)) {\n    await sleep(500)\n    tools = await loadServerTools(workspace, path) // retry after invalidations settle\n  } else throw e\n}","preventionTips":["Avoid editing the MCP server resource while the chat is loading its tools.","Serialize clearMcpToolsCache calls instead of firing them concurrently.","Add retry-with-backoff around tool loading in agent loops."],"tags":["mcp","cache","race-condition","retry"],"backgroundTag":"cache-invalidated-during-load","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}