You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Proposal to let tool definitions respond to state — so agents see tight, accurate schemas and up-to-date availability without pages re-pushing entire tool sets.
registerTool() assumes a tool's inputSchema and callability are fixed at registration. In state-driven applications, they aren't. This proposal lets inputSchema be resolved lazily at enumeration time, and introduces a disabled flag toggleable via updateTool() — preserving tool identity across state changes.
Strawman API
navigator.modelContext.registerTool({name: "play_track",description: "Play a track from the user's library.",inputSchema: ()=>({type: "object",properties: {id: {type: "string",enum: library.trackIds()}},required: ["id"],}),asyncexecute({ id }){player.play(id);/* ... */},});navigator.modelContext.registerTool({name: "remove_from_queue",description: "Remove a track from the playback queue.",disabled: true,// queue empty at bootinputSchema: {/* static */},execute: async({ position })=>{/* ... */},});queue.on("change",(q)=>{navigator.modelContext.updateTool("remove_from_queue",{disabled: q.length===0});});
inputSchema as a function is invoked by the UA every time it materializes the tool for an agent (e.g. each model turn). Must be synchronous. Disabled tools stay in the registry — visible to introspection, hidden from the model, rejected on execute. updateTool() mutates disabled, description, or inputSchema; not execute, annotations, or name.
Problem
A music app registers playTrack, addToPlaylist, removeFromQueue. removeFromQueue is meaningless when the queue is empty. playTrack's id should enum over tracks actually in the library; addToPlaylist's playlistId should enum over playlists the user currently has. Hard-coding type: "string" invites hallucinated IDs and burns tokens on retries.
The existing workaround is to re-call provideContext() on every state change. This churns every tool's definition for a local change, races the agent, and — since the cost scales with (tools × state changes) — pushes developers toward over-broad schemas to avoid the overhead.
Design Considerations
Push vs. pull. Availability needs prompt reflection in agent UIs, so it's pushed (updateTool). Schema only matters at prompt-assembly time, so it's pulled (UA invokes the callback). This split matches how each piece of information is actually consumed.
Sync callbacks. Tool enumeration runs per model turn. An async resolver introduces a failure mode — pending resolution when the LLM is about to be prompted — worse than asking pages to keep derivation fast. Pages needing async (e.g. server-fetched enums) cache into state and push via updateTool, which reduces to the existing model exactly where push makes sense.
Staleness. Pull doesn't eliminate the window between UA reads schema and model emits call; execute still validates defensively. It does make the window a function of enumeration cadence rather than of the page's re-registration choreography.
Open Questions
Does a grant to call a tool persist across a disabled: true/false toggle? (I think yes — disabled is availability, not identity.)
What does the UA do when a lazy inputSchema callback throws — silently treat the tool as unavailable, or surface the error (DevTools, event)?
Should toolchange (currently only on navigator.modelContextTesting) be promoted so agent UIs can reflect availability without polling?
Prior Art
MCP's notifications/tools/list_changed — the push counterpart; no lazy schema equivalent.
Proposal to let tool definitions respond to state — so agents see tight, accurate schemas and up-to-date availability without pages re-pushing entire tool sets.
registerTool()assumes a tool'sinputSchemaand callability are fixed at registration. In state-driven applications, they aren't. This proposal letsinputSchemabe resolved lazily at enumeration time, and introduces adisabledflag toggleable viaupdateTool()— preserving tool identity across state changes.Strawman API
inputSchemaas a function is invoked by the UA every time it materializes the tool for an agent (e.g. each model turn). Must be synchronous. Disabled tools stay in the registry — visible to introspection, hidden from the model, rejected on execute.updateTool()mutatesdisabled,description, orinputSchema; notexecute,annotations, orname.Problem
A music app registers
playTrack,addToPlaylist,removeFromQueue.removeFromQueueis meaningless when the queue is empty.playTrack'sidshouldenumover tracks actually in the library;addToPlaylist'splaylistIdshould enum over playlists the user currently has. Hard-codingtype: "string"invites hallucinated IDs and burns tokens on retries.The existing workaround is to re-call
provideContext()on every state change. This churns every tool's definition for a local change, races the agent, and — since the cost scales with (tools × state changes) — pushes developers toward over-broad schemas to avoid the overhead.Design Considerations
updateTool). Schema only matters at prompt-assembly time, so it's pulled (UA invokes the callback). This split matches how each piece of information is actually consumed.updateTool, which reduces to the existing model exactly where push makes sense.updateToolkeys by name and leavesexecute/annotations/nameimmutable. Changing those amounts to replacing the tool and should go throughunregisterTool+registerToolso identity is explicit — consistent with the direction set in [#101](navigator.modelContext.provideContextallows overwriting of previously registered tools in the same environment #101) / [#130](Tool unregistration design #130).executestill validates defensively. It does make the window a function of enumeration cadence rather than of the page's re-registration choreography.Open Questions
disabled: true/falsetoggle? (I think yes —disabledis availability, not identity.)inputSchemacallback throws — silently treat the tool as unavailable, or surface the error (DevTools, event)?toolchange(currently only onnavigator.modelContextTesting) be promoted so agent UIs can reflect availability without polling?Prior Art
notifications/tools/list_changed— the push counterpart; no lazy schema equivalent.list_changed. WebMCP is positioned to do better because UA and provider are co-located and a sync callback is cheap.