-
Notifications
You must be signed in to change notification settings - Fork 11
Description
Porting this from a discussion in the MCP contributors discord
Dynamic tool registration in MCP, especially WebMCP, suffers from a race condition where tools/list_changed
notifications lag behind tools/call
results, causing agents to use stale toolsets (VSCode #261611). The idea is to add registers
, unregisters
, and updates
fields to CallToolResult
to declare expected toolset changes upon a successful tool call. Clients with a wait_for
capability can delay forwarding results until the toolset updates with the expected tools.
While it's a feature of traditional MCP, I've come to the conclusion that directly registering tools from other tools is an anti-pattern in WebMCP (after writing many WebMCP servers). WebMCP works best when tools are a function of UI state. (I'll make another issue about this)
The issue this causes is the tool list updates to come after the tool response has been sent to the model. The model then makes its next move before the tool list has been updated. For example in a multi-page app, when you call a tool which navigates to the new page, the model might choose to call a tool on the old page which no longer exists.
I've set a timeout after each tool call response in MCP-B but this forces a tradeoff between UX and reliability based on the timeout length.
This is what I have in mind for the implementation (Feedback very welcome):
interface CallToolResult {
_meta?: { [key: string]: unknown };
content: ContentBlock[];
is_error?: boolean;
structured_content?: { [key: string]: unknown };
/**
* Tools expected to register post-execution. Clients wait for these in tools/list_changed.
*/
registers?: string[]; // Default: [] (no wait)
/**
* Tools expected to unregister post-execution. Clients wait for absence in tools/list_changed.
*/
unregisters?: string[]; // Default: [] (no wait)
/**
* same as the others but for tools expected to have internal changes as a result of just executed tool
*/
updates?: string[]; // Default: [] (no wait)
}
interface ClientCapabilities {
tools: {
wait_for: boolean;
wait_for_timeout: number; // how long to wait before sending the response if we don't get the expect tool updates
};
}
Example tool call response:
{
"content": ["..."],
"registers": ["read_file", "write_file"],
"unregisters": ["temp_tool"],
"updates": ["foo_tool"]
}
sequenceDiagram
participant M as Model
participant C as Client
participant S as WebMCP Server
participant UI as UI State
Note over M,C: Initial tools: [unlock_files]
M->>C: Call unlock_files
C->>S: tools/call unlock_files {args}
S->>UI: Execute, update state
S->>C: tools/call result {registers: ['read_file', 'write_file'], unregisters: [], updates: []}
Note over C: Wait for tools/list_changed matching registers/unregisters/updates (re-fetch list)
UI->>S: Register read_file
S->>C: tools/list_changed [unlock_files, read_file]
Note over C: Partial match continue waiting
UI->>S: Register write_file
S->>C: tools/list_changed [unlock_files, read_file, write_file]
alt Matches expectations
C->>S: tools/list request
S->>C: tools/list response [unlock_files, read_file, write_file]
C->>M: Forward result (read_file, write_file available)
else Timeout (e.g., after 5s)
C->>M: Forward result (may be stale log warning)
end
Note over M,C: Next turn: read_file, write_file ready
@bwalderman @khushalsagar have you run into this issue in the chromium implementation/testing?