Skip to content

Proposal: Dynamic Tool Definitions in WebMCP #167

@AlpineJosh

Description

@AlpineJosh

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"],
  }),
  async execute({ id }) { player.play(id); /* ... */ },
});

navigator.modelContext.registerTool({
  name: "remove_from_queue",
  description: "Remove a track from the playback queue.",
  disabled: true, // queue empty at boot
  inputSchema: { /* 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.
  • Identity preservation. updateTool keys by name and leaves execute/annotations/name immutable. Changing those amounts to replacing the tool and should go through unregisterTool + registerTool so identity is explicit — consistent with the direction set in [#101](navigator.modelContext.provideContext allows overwriting of previously registered tools in the same environment #101) / [#130](Tool unregistration design #130).
  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions