From 2dfd5b8772a4d06a316d8735a92140094d23db7f Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Thu, 15 Oct 2026 19:16:37 +0000 Subject: [PATCH 01/11] Introduce the `Tool` interface --- README.md | 4 +- index.bs | 313 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 208 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index e28d86f..0cebbf9 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ Any document in the tree matching these origins (and allowed to use `tools` perm Once tools are registered, in-page agents can discover and invoke them using `getTools()` and `executeTool()`. -Calling `document.modelContext.getTools()` returns a promise that resolves with an array of `RegisteredTool` dictionary objects. Each object contains the tool's `name`, `description`, `inputSchema`, `origin`, and owner `window`. By default, `getTools()` only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the `fromOrigins` option. This array only supports secure origins. +Calling `document.modelContext.getTools()` returns a promise that resolves with an array of [`Tool`](https://webmachinelearning.github.io/webmcp/#tool) objects. Each `Tool` exposes the tool's `name`, `title`, `description`, `inputSchema`, `annotations`, `origin`, and owner `window`. By default, `getTools()` only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the `fromOrigins` option. This array only supports secure origins. ```js // Discover tools exposed by same-origin frames in the tree (default) @@ -372,7 +372,7 @@ const crossOriginTools = await document.modelContext.getTools({ }); ``` -An agent executes a discovered `RegisteredTool` by passing the tool dictionary and input arguments along to `document.modelContext.executeTool()`. The browser securely mediates the execution, ensuring the [`exposedTo`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextregistertooloptions-exposedto) and [`fromOrigins`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextgettooloptions-fromorigins) agree, and the tool runs in the tool owner's execution context: +An agent executes a discovered `Tool` by passing it and input arguments along to `document.modelContext.executeTool()`. The browser securely mediates the execution, ensuring the [`exposedTo`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextregistertooloptions-exposedto) and [`fromOrigins`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextgettooloptions-fromorigins) agree, and the tool runs in the tool owner's execution context. `Tool` objects can only be obtained from `getTools()`, and each one refers to the specific tool registration it was returned for: if that tool is unregistered, executing the `Tool` fails, even if a new tool is later registered under the same name. ```js const tools = await document.modelContext.getTools(); diff --git a/index.bs b/index.bs index bd81b6d..beb01d6 100644 --- a/index.bs +++ b/index.bs @@ -153,8 +153,12 @@ A model context is a [=struct=] with the following [=struct/items=]:
: tool map - :: a [=map=] whose [=map/keys=] are [=strings=] and whose [=map/values=] are [=tool definition=] - [=structs=]. + :: a [=map=] whose [=map/keys=] are [=unique internal values=] and whose [=map/values=] are [=tool + definition=] [=structs=]. + + Note: Each [=map/key=] identifies a specific registration of a tool. Unlike the tool's [=tool + definition/name=], it is never reused when a tool is unregistered and another tool is + registered under the same name. : local pending tool executions map :: a [=map=] whose [=map/keys=] are [=unique internal values=] and whose [=map/values=] are @@ -173,7 +177,7 @@ A tool definition is a [=struct=] with the following [=struct/items=]
: name :: a [=string=] uniquely identifying a tool registered within a [=model context=]'s [=model - context/tool map=]; it is the same as the [=map/key=] identifying this object. + context/tool map=]. The [=tool definition/name=]'s [=string/length=] must be between 1 and 128, inclusive, and only consist of [=ASCII alphanumeric=] [=code points=], U+005F LOW LINE (_), @@ -466,10 +470,10 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin
-The tool execute steps, given a [=string=] |toolName|, a {{Document}} |targetDocument|, a -[=string=] |inputArguments|, an algorithm |completionSteps|, and a [=unique internal value=] |uuid|, -are as follows. The |completionSteps| algorithm takes a [=string=]-or-null result -and a [=tool execution error=]-or-null error. +The tool execute steps, given a [=unique internal value=] |toolId|, a {{Document}} +|targetDocument|, a [=string=] |inputArguments|, an algorithm |completionSteps|, and a [=unique +internal value=] |uuid|, are as follows. The |completionSteps| algorithm takes a [=string=]-or-null +result and a [=tool execution error=]-or-null error. 1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=]. @@ -477,18 +481,18 @@ and a [=tool execution error=]-or-null error. 1. Let |toolMap| be |targetDocument|'s [=Document/associated ModelContext|associated ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. -1. If |toolMap|[|toolName|] does not [=map/exist=], then run |completionSteps| given null and +1. If |toolMap|[|toolId|] does not [=map/exist=], then run |completionSteps| given null and [=tool execution error/tool not found=], and abort these steps.
-

This protects us against a race between tool unregistration and execution. While tool - existence is protected from this race, tool unregistration followed by a quick - re-registration of a tool with the same |toolName| but input schema is not protected - against.

+

This protects against a race between tool unregistration and execution: the tool might have + been unregistered after {{ModelContext/executeTool()}} checked for its existence [=in + parallel=], but before these steps run.

-

This might result in |inputArguments| for an old tool being applied to the [=tool - definition/input schema=] of a newer tool, and causing whatever error that might cause, when issue #92 is resolved.

+

Because tools are looked up by |toolId| rather than by name, this also protects against the + tool being unregistered and quickly re-registered under the same name. The re-registered tool + has a different |toolId|, so a caller holding a {{Tool}} for the old tool fails to execute, + instead of silently executing the new tool with |inputArguments| intended for the old one.

@@ -498,22 +502,24 @@ and a [=tool execution error=]-or-null <var ignore>error</var>. const ac = new AbortController(); document.modelContext.registerTool({..., inputSchema: oldInputSchema}, {signal: ac.signal}); + // -- Executing document. -- + const [oldTool] = await document.modelContext.getTools(); + + // -- Tool owner document. -- // Unregister, and quickly re-register with an updated input schema. ac.abort(); document.modelContext.registerTool({..., inputSchema: newInputSchema}); - // -- Executing document. -- // - // This could target either the "old" tool, or the "new" one above, - // and the execution might encounter any requisite errors due to the mismatch. - const [tool] = await document.modelContext.getTools(); - document.modelContext.executeTool(tool, {a: 10}); + // `oldTool` refers to the old tool, so this rejects with a "NotFoundError" + // DOMException rather than executing the new tool. + document.modelContext.executeTool(oldTool, {a: 10});
-1. Let |tool| be |toolMap|[|toolName|]. +1. Let |tool| be |toolMap|[|toolId|]. 1. Run |tool|'s [=tool definition/execute steps=] given |targetDocument|, |inputArguments|, |completionSteps|, and |uuid|. @@ -598,7 +604,7 @@ internal value=] |uuid|, run these steps:
To unregister a tool given a {{ModelContext}} |modelContext| and a -[=string=] |tool name|, run these steps: +[=unique internal value=] |tool id|, run these steps: 1. [=Assert=]: these steps are running on |modelContext|'s [=relevant agent=]'s [=agent/event loop=]. @@ -606,11 +612,16 @@ To unregister a tool given a {{ModelContext}} |mo 1. Let |tool map| be |modelContext|'s [=ModelContext/internal context=]'s [=model context/tool map=]. -1. If |tool map|[|tool name|] does not [=map/exist=], then return. +1. [=Assert=]: |tool map|[|tool id|] [=map/exists=]. + + Note: These steps only run as the abort steps of the {{ModelContextRegisterToolOptions/signal}} + passed to the {{ModelContext/registerTool()}} call that registered the tool identified by + |tool id|. An {{AbortSignal}} runs its abort steps at most once, and nothing else removes + entries from |tool map|, so the tool is still registered. -1. Let |exposed origins| be |tool map|[|tool name|]'s [=tool definition/exposed origins=]. +1. Let |exposed origins| be |tool map|[|tool id|]'s [=tool definition/exposed origins=]. -1. [=map/Remove=] |tool map|[|tool name|]. +1. [=map/Remove=] |tool map|[|tool id|]. Note: Unregistering a tool does not cancel an execution that has already invoked its {{ToolExecuteCallback}}, including when the callback itself performs the unregistration. @@ -669,8 +680,8 @@ The {{ModelContext}} interface provides methods for web applications to register [Exposed=Window, SecureContext] interface ModelContext : EventTarget { Promise registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {}); - Promise> getTools(optional ModelContextGetToolOptions options = {}); - Promise executeTool(RegisteredTool tool, optional object inputObject, optional ModelContextExecuteToolOptions options = {}); + Promise> getTools(optional ModelContextGetToolOptions options = {}); + Promise executeTool(Tool tool, optional object inputObject, optional ModelContextExecuteToolOptions options = {}); attribute EventHandler ontoolchange; attribute EventHandler ontoolactivated; @@ -704,11 +715,13 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}.

Executes a tool on the document it was registered on. Returns a promise that resolves to the stringified result of the tool's execution.

-

tool is a {{RegisteredTool}}, often retrieved by - {{ModelContext/getTools()}}. Callers pass a JavaScript object in as the +

tool is a {{Tool}}, retrieved by {{ModelContext/getTools()}}. It + identifies the exact tool registration that {{ModelContext/getTools()}} returned, so if that + tool has since been unregistered, execution fails, even if a different tool has since been + registered under the same name. Callers pass a JavaScript object in as the {{ModelContext/executeTool(tool)/inputObject}} argument for the tool; it's expected to match the - schema described by the tool's {{RegisteredTool/inputSchema}}. If the argument is omitted, the - tool receives an empty object.

+ schema described by the tool's {{Tool/inputSchema}}. If the argument is omitted, the tool + receives an empty object.

@@ -745,7 +758,8 @@ The registerTool(tool, optionsregisterTool(tool, options. -1. If |tool map|[|tool name|] [=map/exists=], then return [=a promise rejected with=] an +1. If |tool map|'s [=map/values=] [=list/contains=] a [=tool definition=] whose [=tool + definition/name=] is |tool name|, then return [=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}. Note: Similarly, script run while serializing |tool|'s {{ModelContextTool/inputSchema}} can @@ -810,6 +825,8 @@ The registerTool(tool, optionsregisterTool(tool, optionsregisterTool(tool, optionsgetTools(options) method steps ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. - 1. [=map/For each=] tool name → |tool definition| of |targetToolMap|: + 1. [=map/For each=] |tool id| → |tool definition| of |targetToolMap|: 1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool definition/exposed origins=], and |callerOrigin| returns false, then [=iteration/continue=]. - 1. [=list/Append=] (|tool definition|, |targetDocument|) to |discoveredTools|. + 1. [=list/Append=] (|tool id|, |tool definition|, |targetDocument|) to |discoveredTools|. 1. [=Queue a global task=] on the [=webmcp task source=] given |global| to run the following steps: - 1. Let |tools| be an empty [=list=] of {{RegisteredTool}} dictionaries. + 1. Let |tools| be an empty [=list=] of {{Tool}} objects. - 1. [=list/For each=] (|tool definition|, |targetDocument|) of |discoveredTools|: + 1. [=list/For each=] (|tool id|, |tool definition|, |targetDocument|) of |discoveredTools|: - 1. Let |registeredTool| be a new {{RegisteredTool}} dictionary, with the following fields: + 1. Let |newTool| be a [=new=] {{Tool}} object created in [=this=]'s [=relevant realm=], + whose members are set as follows: - : {{RegisteredTool/name}} + : [=Tool/name=] :: |tool definition|'s [=tool definition/name=] - : {{RegisteredTool/title}} + : [=Tool/title=] :: |tool definition|'s [=tool definition/title=] if it is non-null; otherwise the empty string. - Issue(#224): Consider not defaulting to the empty string, and just excluding this - member, which will result in {{undefined}}. + Issue(#224): Consider not defaulting to the empty string. - : {{RegisteredTool/description}} + : [=Tool/description=] :: |tool definition|'s [=tool definition/description=] - : {{RegisteredTool/inputSchema}} + : [=Tool/input schema=] :: the result of [=parse a JSON string to a JavaScript value=] given |tool definition|'s [=tool definition/input schema=], if |tool definition|'s - [=tool definition/input schema=] is not the empty string; otherwise undefined. + [=tool definition/input schema=] is not the empty string; otherwise null. Note: This will never throw an exception, because the string stored in the tool definition is always a valid JSON string. - : {{RegisteredTool/window}} - :: |targetDocument|'s [=relevant global object=] - - : {{RegisteredTool/origin}} - :: [=serialization of an origin|serialized=] |targetDocument|'s [=Document/origin=]. - - : {{RegisteredTool/annotations}} - :: if |tool definition|'s [=tool definition/annotations=] is not null, a + : [=Tool/annotations=] + :: null if |tool definition|'s [=tool definition/annotations=] is null. Otherwise, the + result of [=converted to a JavaScript value|converting=] to a JavaScript value a {{ToolAnnotations}} dictionary whose {{ToolAnnotations/readOnlyHint}} is |tool definition|'s [=tool definition/annotations=]'s [=annotations/read-only hint=], {{ToolAnnotations/untrustedContentHint}} is |tool definition|'s [=tool @@ -985,10 +997,19 @@ The getTools(options) method steps {{ToolAnnotations/debugging}} is |tool definition|'s [=tool definition/annotations=]'s [=annotations/debugging=]. - 1. [=list/Append=] |registeredTool| to |tools|. + : [=Tool/window=] + :: |targetDocument|'s [=relevant global object=] + + : [=Tool/origin=] + :: |targetDocument|'s [=Document/origin=] + + : [=Tool/tool id=] + :: |tool id| - 1. [=list/Sort in ascending order=] |tools|, with |a| being less than |b| if - |a|["{{RegisteredTool/name}}"] is [=code unit less than=] |b|["{{RegisteredTool/name}}"]. + 1. [=list/Append=] |newTool| to |tools|. + + 1. [=list/Sort in ascending order=] |tools|, with |a| being less than |b| if |a|'s [=Tool/name=] + is [=code unit less than=] |b|'s [=Tool/name=]. 1. [=Resolve=] |promise| with |tools|. @@ -1009,16 +1030,13 @@ The executeTool(tool, inputObjectexecuteTool(tool, inputObjectDocument=]. @@ -1078,24 +1096,28 @@ The executeTool(tool, inputObjectModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. - 1. Let |toolName| be |tool|'s {{RegisteredTool/name}}. + 1. Let |toolId| be |tool|'s [=Tool/tool id=]. - 1. If |targetToolMap|[|toolName|] does not [=map/exist=], then [=queue a global task=] on the + 1. If |targetToolMap|[|toolId|] does not [=map/exist=], then [=queue a global task=] on the [=webmcp task source=] given |callerDocument|'s [=relevant global object=] to [=reject=] |promise| with a "{{NotFoundError}}" {{DOMException}}, and abort these steps. - 1. Let |tool definition| be |targetToolMap|[|toolName|]. + 1. Let |tool definition| be |targetToolMap|[|toolId|]. + + 1. [=Assert=]: |tool definition|'s [=tool definition/name=] is |tool|'s [=Tool/name=]. 1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool definition/exposed origins=], and |callerOrigin| returns false, then [=queue a global task=] on the [=webmcp task source=] given |callerDocument|'s [=relevant global object=] to [=reject=] |promise| with a "{{NotFoundError}}" {{DOMException}}, and abort these steps. - Note: Rejecting with "{{NotFoundError}}" across all three checks above—when |targetOrigin| does - not match |expectedTargetOrigin|, when |toolName| does not exist, and when |tool definition| - is not exposed to |callerOrigin|—prevents a cross-origin caller from using fabricated - {{RegisteredTool}} dictionaries to probe |targetDocument|'s current origin or enumerate - private tools not exposed to |callerOrigin|. + Note: Callers cannot fabricate {{Tool}} objects; they only get them from + {{ModelContext/getTools()}}. But |targetDocument| can change between + {{ModelContext/getTools()}} resolving and this method being called: for example, the tool can + be unregistered, or unregistered and re-registered. Rejecting with "{{NotFoundError}}" across + all three checks above—when |targetOrigin| does not match |expectedTargetOrigin|, when the + tool identified by |toolId| does not exist, and when |tool definition| is not exposed to + |callerOrigin|—avoids revealing to the caller which of these happened. 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=tool execution error=]-or-null |error|, and runs the following steps [=in parallel=]: @@ -1139,7 +1161,7 @@ The executeTool(tool, inputObjectexecuteTool(tool, inputObject -

RegisteredTool Dictionary

+

Tool Interface

-The {{RegisteredTool}} dictionary represents a tool that has been registered and is available for -execution. +The {{Tool}} interface represents a tool that has been registered and is available for execution, +as returned by {{ModelContext/getTools()}}. A {{Tool}} is a snapshot of the tool at the time +{{ModelContext/getTools()}} discovered it, and identifies that specific registration of the tool. -dictionary RegisteredTool { - required DOMString name; +[Exposed=Window, SecureContext] +interface Tool { + readonly attribute DOMString name; // `title` can be exposed as a `DOMString` since it was taken in by a // `USVString`, meaning all unmatched surrogate processing has already been // done, and there's no need to do it again on tool exposure. - DOMString title; - required DOMString description; - object inputSchema; - required Window window; - required USVString origin; - ToolAnnotations annotations; + readonly attribute DOMString title; + readonly attribute DOMString description; + readonly attribute object? inputSchema; + readonly attribute Window? window; + readonly attribute USVString origin; + readonly attribute object? annotations; }; +Each {{Tool}} object has the following associated members: + +
+ : name + :: a [=string=]. + + : title + :: a [=string=]. + + : description + :: a [=string=]. + + : input schema + :: an {{object}} or null. + + : annotations + :: an {{object}} or null. + + Note: This is an {{object}} rather than a {{ToolAnnotations}} dictionary, because Web IDL + attributes cannot have dictionary types. + + : window + :: a {{Window}}. + + : origin + :: an [=origin=]. + + : tool id + :: a [=unique internal value=] identifying the registered tool that this {{Tool}} represents, in + its [=Tool/window=]'s [=associated Document|associated Document=]'s + [=Document/associated ModelContext|associated ModelContext=]'s [=ModelContext/internal + context=]'s [=model context/tool map=]. + + Note: This is not exposed to script. {{ModelContext/executeTool()}} uses it to execute exactly + the tool that {{ModelContext/getTools()}} returned. +
+ +
+The name getter steps are to return [=this=]'s [=Tool/name=]. +
+ +
+The title getter steps are to return [=this=]'s [=Tool/title=]. +
+ +
+The description getter steps are to return [=this=]'s +[=Tool/description=]. +
+ +
+The inputSchema getter steps are to return [=this=]'s [=Tool/input +schema=]. +
+ +
+The window getter steps are: + +1. If [=this=]'s [=Tool/window=]'s [=Window/browsing context=] is null, then return null. + +1. Return [=this=]'s [=Tool/window=]. + +
+ +
+The origin getter steps are to return the [=serialization of an +origin|serialization=] of [=this=]'s [=Tool/origin=]. +
+ +
+The annotations getter steps are to return [=this=]'s +[=Tool/annotations=]. +
+
- : tool["{{RegisteredTool/name}}"] + : tool.{{Tool/name}} :: A unique identifier for the tool. It is the same value provided at tool registration, via {{ModelContextTool/name}}. - : tool["{{RegisteredTool/title}}"] + : tool.{{Tool/title}} :: A human-readable label for the tool. It is the same value provided at tool registration, via {{ModelContextTool/title}}. - : tool["{{RegisteredTool/description}}"] + : tool.{{Tool/description}} :: A natural language description of the tool's functionality. It is the same value provided at tool registration, via {{ModelContextTool/description}}. - : tool["{{RegisteredTool/inputSchema}}"] + : tool.{{Tool/inputSchema}} :: A JSON Schema object describing the expected input parameters for the tool - [[!JSON-SCHEMA]]. It is a deep copy of the schema provided at tool registration, via - {{ModelContextTool/inputSchema}}. - - : tool["{{RegisteredTool/window}}"] - :: The {{Window}} of the document that registered the tool. - - : tool["{{RegisteredTool/origin}}"] - :: The origin of the document that registered the tool. This member is only meaningful when the - tool is cross-origin, and the consumer of a tool cannot otherwise get the tool's origin from - its {{RegisteredTool/window}}. For same-origin tools, this is the same as the tool's - {{RegisteredTool/window}}'s {{WindowOrWorkerGlobalScope/origin}}, and the caller's own + [[!JSON-SCHEMA]], or null if the tool was registered without one. It is a deep copy of the + schema provided at tool registration, via {{ModelContextTool/inputSchema}}. + + : tool.{{Tool/window}} + :: The {{Window}} of the document that registered the tool, or null if that document has since + been destroyed (e.g., because its <{iframe}> was removed). + + : tool.{{Tool/origin}} + :: The origin of the document that registered the tool. This is only meaningful when the tool is + cross-origin, and the consumer of a tool cannot otherwise get the tool's origin from its + {{Tool/window}}. For same-origin tools, this is the same as the tool's {{Tool/window}}'s + {{WindowOrWorkerGlobalScope/origin}}, and the caller's own {{Window}}.{{WindowOrWorkerGlobalScope/origin}}. - : tool["{{RegisteredTool/annotations}}"] - :: Optional annotations providing metadata about the tool. It matches - {{ModelContextTool/annotations}}. + : tool.{{Tool/annotations}} + :: Annotations providing metadata about the tool, matching {{ModelContextTool/annotations}}, or + null if the tool was registered without any.
From f8c3b0a46fa54383f7d98387c7bd619b1829d03d Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 16 Oct 2026 02:11:27 +0000 Subject: [PATCH 02/11] Add tool id to definition --- index.bs | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/index.bs b/index.bs index beb01d6..4a55920 100644 --- a/index.bs +++ b/index.bs @@ -156,10 +156,6 @@ A model context is a [=struct=] with the following [=struct/items=]: :: a [=map=] whose [=map/keys=] are [=unique internal values=] and whose [=map/values=] are [=tool definition=] [=structs=]. - Note: Each [=map/key=] identifies a specific registration of a tool. Unlike the tool's [=tool - definition/name=], it is never reused when a tool is unregistered and another tool is - registered under the same name. - : local pending tool executions map :: a [=map=] whose [=map/keys=] are [=unique internal values=] and whose [=map/values=] are [=local pending tool execution=] [=structs=]. It is initially empty. @@ -175,6 +171,12 @@ A model context is a [=struct=] with the following [=struct/items=]: A tool definition is a [=struct=] with the following [=struct/items=]:
+ : tool id + :: a [=unique internal value=]. + + Note: This identifies a specific registration of a tool. Unlike the tool's [=tool + definition/name=], it is not reused when a same-named tool is re-registered later. + : name :: a [=string=] uniquely identifying a tool registered within a [=model context=]'s [=model context/tool map=]. @@ -841,6 +843,9 @@ The registerTool(tool, optionsgetTools(options) method steps ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. - 1. [=map/For each=] |tool id| → |tool definition| of |targetToolMap|: + 1. [=list/For each=] |tool definition| of |targetToolMap|'s [=map/values=]: 1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool definition/exposed origins=], and |callerOrigin| returns false, then [=iteration/continue=]. - 1. [=list/Append=] (|tool id|, |tool definition|, |targetDocument|) to |discoveredTools|. + 1. [=list/Append=] (|tool definition|, |targetDocument|) to |discoveredTools|. 1. [=Queue a global task=] on the [=webmcp task source=] given |global| to run the following steps: 1. Let |tools| be an empty [=list=] of {{Tool}} objects. - 1. [=list/For each=] (|tool id|, |tool definition|, |targetDocument|) of |discoveredTools|: + 1. [=list/For each=] (|tool definition|, |targetDocument|) of |discoveredTools|: 1. Let |newTool| be a [=new=] {{Tool}} object created in [=this=]'s [=relevant realm=], whose members are set as follows: + : [=Tool/tool id=] + :: |tool definition|'s [=tool definition/tool id=] + : [=Tool/name=] :: |tool definition|'s [=tool definition/name=] @@ -1003,9 +1011,6 @@ The getTools(options) method steps : [=Tool/origin=] :: |targetDocument|'s [=Document/origin=] - : [=Tool/tool id=] - :: |tool id| - 1. [=list/Append=] |newTool| to |tools|. 1. [=list/Sort in ascending order=] |tools|, with |a| being less than |b| if |a|'s [=Tool/name=] @@ -1357,6 +1362,13 @@ interface Tool { Each {{Tool}} object has the following associated members:
+ : tool id + :: a [=unique internal value=]. + + Note: This is the [=tool definition/tool id=] of the [=tool definition=] that this {{Tool}} + represents. It is not exposed to script; {{ModelContext/executeTool()}} uses it to execute + exactly the tool that {{ModelContext/getTools()}} returned. + : name :: a [=string=]. @@ -1380,15 +1392,6 @@ Each {{Tool}} object has the following associated members: : origin :: an [=origin=]. - - : tool id - :: a [=unique internal value=] identifying the registered tool that this {{Tool}} represents, in - its [=Tool/window=]'s [=associated Document|associated Document=]'s - [=Document/associated ModelContext|associated ModelContext=]'s [=ModelContext/internal - context=]'s [=model context/tool map=]. - - Note: This is not exposed to script. {{ModelContext/executeTool()}} uses it to execute exactly - the tool that {{ModelContext/getTools()}} returned.
From fd29af45658fa49fe50f01a88530f2555be1ae91 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 16 Oct 2026 02:23:13 +0000 Subject: [PATCH 03/11] Wordsmithing --- index.bs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/index.bs b/index.bs index 4a55920..b19ee05 100644 --- a/index.bs +++ b/index.bs @@ -1117,12 +1117,13 @@ The executeTool(tool, inputObject Date: Fri, 16 Oct 2026 12:56:24 +0000 Subject: [PATCH 04/11] Reordering and small wording changes --- index.bs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/index.bs b/index.bs index b19ee05..beb2174 100644 --- a/index.bs +++ b/index.bs @@ -618,8 +618,8 @@ To unregister a tool given a {{ModelContext}} |mo Note: These steps only run as the abort steps of the {{ModelContextRegisterToolOptions/signal}} passed to the {{ModelContext/registerTool()}} call that registered the tool identified by - |tool id|. An {{AbortSignal}} runs its abort steps at most once, and nothing else removes - entries from |tool map|, so the tool is still registered. + |tool id|. Since {{AbortSignal}} only abort at most once, and nothing else removes entries from + |tool map|, we can guarantee that the tool we're targeting is still registered. 1. Let |exposed origins| be |tool map|[|tool id|]'s [=tool definition/exposed origins=]. @@ -993,6 +993,12 @@ The getTools(options) method steps Note: This will never throw an exception, because the string stored in the tool definition is always a valid JSON string. + : [=Tool/window=] + :: |targetDocument|'s [=relevant global object=] + + : [=Tool/origin=] + :: |targetDocument|'s [=Document/origin=] + : [=Tool/annotations=] :: null if |tool definition|'s [=tool definition/annotations=] is null. Otherwise, the result of [=converted to a JavaScript value|converting=] to a JavaScript value a @@ -1005,12 +1011,6 @@ The getTools(options) method steps {{ToolAnnotations/debugging}} is |tool definition|'s [=tool definition/annotations=]'s [=annotations/debugging=]. - : [=Tool/window=] - :: |targetDocument|'s [=relevant global object=] - - : [=Tool/origin=] - :: |targetDocument|'s [=Document/origin=] - 1. [=list/Append=] |newTool| to |tools|. 1. [=list/Sort in ascending order=] |tools|, with |a| being less than |b| if |a|'s [=Tool/name=] @@ -1382,17 +1382,17 @@ Each {{Tool}} object has the following associated members: : input schema :: an {{object}} or null. - : annotations - :: an {{object}} or null. - - Note: This is an {{object}} rather than a {{ToolAnnotations}} dictionary, because Web IDL - attributes cannot have dictionary types. - : window :: a {{Window}}. : origin :: an [=origin=]. + + : annotations + :: an {{object}} or null. + + Note: This is an {{object}} rather than a {{ToolAnnotations}} dictionary, because Web IDL + attributes cannot have dictionary types.
From 6422b9d93023de5e4d183068beb2db6b032387f6 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 16 Oct 2026 13:19:16 +0000 Subject: [PATCH 05/11] Reference opaque origin follow-up --- index.bs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.bs b/index.bs index beb2174..9c5ad41 100644 --- a/index.bs +++ b/index.bs @@ -1040,8 +1040,7 @@ The executeTool(tool, inputObject Date: Fri, 16 Oct 2026 13:23:09 +0000 Subject: [PATCH 06/11] Some tool documentation --- index.bs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 9c5ad41..c885f35 100644 --- a/index.bs +++ b/index.bs @@ -1339,8 +1339,9 @@ dictionary ModelContextExecuteToolOptions {

Tool Interface

-The {{Tool}} interface represents a tool that has been registered and is available for execution, -as returned by {{ModelContext/getTools()}}. A {{Tool}} is a snapshot of the tool at the time +The {{Tool}} interface represents a tool that has been registered and exposed to a document for +execution. It is obtained by {{ModelContext/getTools()}}, and executed with +{{ModelContext/executeTool()}}. A {{Tool}} is a snapshot of the tool at the time {{ModelContext/getTools()}} discovered it, and identifies that specific registration of the tool. From eb2e130e2ddbb1460f79a49546727255cb49a4ce Mon Sep 17 00:00:00 2001 From: Dominic Farolino <dom@chromium.org> Date: Fri, 16 Oct 2026 13:24:54 +0000 Subject: [PATCH 07/11] or-null and remove note --- index.bs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/index.bs b/index.bs index c885f35..63fb45c 100644 --- a/index.bs +++ b/index.bs @@ -1380,7 +1380,7 @@ Each {{Tool}} object has the following associated members: :: a [=string=]. : <dfn>input schema</dfn> - :: an {{object}} or null. + :: an {{object}}-or-null. : <dfn>window</dfn> :: a {{Window}}. @@ -1389,10 +1389,7 @@ Each {{Tool}} object has the following associated members: :: an [=origin=]. : <dfn>annotations</dfn> - :: an {{object}} or null. - - Note: This is an {{object}} rather than a {{ToolAnnotations}} dictionary, because Web IDL - attributes cannot have dictionary types. + :: an {{object}}-or-null. </dl> <div algorithm> From 35eda34731857cedefc1d57c03d387566d5bafea Mon Sep 17 00:00:00 2001 From: Dominic Farolino <dom@chromium.org> Date: Fri, 16 Oct 2026 13:34:56 +0000 Subject: [PATCH 08/11] Tiny nits --- index.bs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.bs b/index.bs index 63fb45c..1c415b4 100644 --- a/index.bs +++ b/index.bs @@ -618,8 +618,8 @@ To <dfn for="model context">unregister a tool</dfn> given a {{ModelContext}} |mo Note: These steps only run as the abort steps of the {{ModelContextRegisterToolOptions/signal}} passed to the {{ModelContext/registerTool()}} call that registered the tool identified by - |tool id|. Since {{AbortSignal}} only abort at most once, and nothing else removes entries from - |tool map|, we can guarantee that the tool we're targeting is still registered. + |tool id|. Since {{AbortSignal}}s only abort at most once, and nothing else removes tools from + the |tool map|, we can guarantee that the tool we're targeting is still registered. 1. Let |exposed origins| be |tool map|[|tool id|]'s [=tool definition/exposed origins=]. @@ -1001,7 +1001,7 @@ The <dfn method for=ModelContext>getTools(<var>options</var>)</dfn> method steps : [=Tool/annotations=] :: null if |tool definition|'s [=tool definition/annotations=] is null. Otherwise, the - result of [=converted to a JavaScript value|converting=] to a JavaScript value a + result of [=converted to a JavaScript value|converting to a JavaScript value=] a {{ToolAnnotations}} dictionary whose {{ToolAnnotations/readOnlyHint}} is |tool definition|'s [=tool definition/annotations=]'s [=annotations/read-only hint=], {{ToolAnnotations/untrustedContentHint}} is |tool definition|'s [=tool From 1f14499664d889a0f2cf1e1b3d49b1bf2a35f2f4 Mon Sep 17 00:00:00 2001 From: Dominic Farolino <dom@chromium.org> Date: Wed, 21 Oct 2026 01:09:59 +0000 Subject: [PATCH 09/11] Clean note --- index.bs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.bs b/index.bs index 1c415b4..41845b9 100644 --- a/index.bs +++ b/index.bs @@ -625,11 +625,11 @@ To <dfn for="model context">unregister a tool</dfn> given a {{ModelContext}} |mo 1. [=map/Remove=] |tool map|[|tool id|]. - Note: Unregistering a tool does not cancel an execution that has already invoked its - {{ToolExecuteCallback}}, including when the callback itself performs the unregistration. - The registration-time {{ModelContextRegisterToolOptions/signal}} only controls tool - availability, while the execution-time {{ModelContextExecuteToolOptions/signal}} controls the - lifetime of an specific invocation. + Note: Unregistering a tool does not cancel an execution that has already reached the [=invoke the + execute callback=] steps, even when the {{ToolExecuteCallback}} itself performs the + unregistration. The registration-time {{ModelContextRegisterToolOptions/signal}} only controls + tool availability, while the execution-time {{ModelContextExecuteToolOptions/signal}} controls + the lifetime of an specific invocation. 1. Let |targetDocument| be |modelContext|'s [=relevant global object=]'s [=associated Document|associated <code>Document</code>=]. From 2cd2621e4d4ed3a9a29e33c2b155cb79355acee1 Mon Sep 17 00:00:00 2001 From: Dominic Farolino <dom@chromium.org> Date: Wed, 21 Oct 2026 01:12:45 +0000 Subject: [PATCH 10/11] Split into two notes --- index.bs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 41845b9..9b32ba2 100644 --- a/index.bs +++ b/index.bs @@ -1119,10 +1119,12 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputObject</ {{ModelContext/getTools()}}. But the tools registered in |targetDocument| can change after {{ModelContext/getTools()}} resolves: for example, the tool can be unregistered, or unregistered and replaced by a new tool with the same name but a different [=tool - definition/tool id=]. Rejecting with "{{NotFoundError}}" across all three checks above—when - |targetOrigin| does not match |expectedTargetOrigin|, when the tool identified by |toolId| - does not exist, and when |tool definition| is not exposed to |callerOrigin|—avoids revealing - to the caller which of these happened. + definition/tool id=]. + + Note: Rejecting with "{{NotFoundError}}" across all three checks above—when |targetOrigin| + does not match |expectedTargetOrigin|, when the tool identified by |toolId| does not exist, + and when |tool definition| is not exposed to |callerOrigin|—avoids revealing to the caller + which of these happened. 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=tool execution error=]-or-null |error|, and runs the following steps [=in parallel=]: From 8020f5864f0e04d9432ade346bc30d7a78d7617b Mon Sep 17 00:00:00 2001 From: Dominic Farolino <dom@chromium.org> Date: Wed, 21 Oct 2026 01:15:00 +0000 Subject: [PATCH 11/11] Address domintro --- index.bs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 9b32ba2..c0fe9fe 100644 --- a/index.bs +++ b/index.bs @@ -1222,8 +1222,7 @@ callback ToolExecuteCallback = Promise<any> (object inputObject, ToolExecuteCall <dl class="domintro"> <dt><code><var ignore>tool</var>["{{ModelContextTool/name}}"]</code></dt> <dd> - <p>A unique identifier for the tool. This is used by [=agents=] to reference the tool when - making tool calls. + <p>A name for the tool. This is used by [=agents=] to reference the tool when making tool calls. </dd> <dt><code><var ignore>tool</var>["{{ModelContextTool/title}}"]</code></dt>