From 853a0aa3857856e0483d5898050f0a432055349b Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Fri, 17 Jul 2026 21:33:47 +0000
Subject: [PATCH 01/15] WebMCP: Spec executeTool() algorithm
This CL introduces the `executeTool()` method to the `ModelContext`
interface in the WebMCP specification. This aligns the spec with the C++
implementation in Blink.
Key changes:
- Adds `executeTool()` and `ExecuteToolOptions` IDL definitions.
- Specifies the `executeTool()` algorithm, detailing cross-document
tool lookup, origin visibility checks, and asynchronous invocation
in the target document`s realm.
- Clarifies the execution callback arguments and JSON serialization.
- Incorporates `AbortSignal` handling for tool cancellation,
dispatching a `toolcancel` event in the target document.
- Fires `toolactivated` event in the target document.
- Updates IDL member access syntax to use idiomatic Bikeshed style.
Small things
Fix some things
Don't serialize
Object argument enforcement
Add AbortSignal issue
Small things
---
index.bs | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 172 insertions(+), 11 deletions(-)
diff --git a/index.bs b/index.bs
index 48c44a1..c7a42b9 100644
--- a/index.bs
+++ b/index.bs
@@ -164,10 +164,9 @@ A tool definition is a [=struct=] with the following [=struct/items=]
[[!JSON-SCHEMA]]
: execute steps
- :: a set of steps to invoke the tool.
+ :: an algorithm that takes both a [=string=] and an algorithm completionSteps that itself takes a [=string=]-or-null and a [=boolean=].
- Note: For tools registered imperatively, these steps will simply invoke the supplied
- {{ToolExecuteCallback}} callback. For tools registered
+ Note: For tools registered imperatively, these steps will simply invoke the [=imperative execute steps=]. For tools registered
[declaratively](https://github.com/webmachinelearning/webmcp/pull/76), this will be a set of
"internal" steps that have not been defined yet, that describe how to fill out a <{form}> and
its [=form-associated elements=].
@@ -257,6 +256,38 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin
+
+The
imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Window}}
+|targetWindow|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows:
+
+1. [=Assert=]: these steps are running on |targetWindow|'s [=relevant agent=]'s [=agent/event
+ loop=].
+
+1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given
+ |inputArguments| and |targetWindow|'s [=relevant realm=]. If
exception was thrown, then
+ run |completionSteps| given null and false, and abort these steps.
+
+ Issue: Support more granular errors; here we should return something that prompts the caller to reject its {{Promise}} with a "{{DataError}}" {{DOMException}}.
+
+1. If |inputObject| [=Object type|is not an Object=] is false, then run |completionSteps| given null and false, and abort these steps.
+
+ Issue(#146): Specify and fire the "
toolactivated" event.
+
+1. Let |toolPromise| be the result of [=invoke|invoking=] |tool|'s {{ModelContextTool/execute}} with
+ |inputObject|.
+
+1. [=promise/React=] to |toolPromise|:
+
+ - If |toolPromise| was fulfilled with value |v|:
+
+ 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps.
+
+ 1. Run |completionSteps| given |serializedResult| and true.
+
+ - If |toolPromise| was rejected with reason
r, then run |completionSteps| given null and false.
+
+
+
To
unregister a tool given a {{ModelContext}} |modelContext| and a
[=string=] |tool name|, run these steps:
@@ -323,6 +354,7 @@ The {{ModelContext}} interface provides methods for web applications to register
interface ModelContext : EventTarget {
Promise
registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
Promise> getTools(optional ModelContextGetToolOptions options = {});
+ Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {});
attribute EventHandler ontoolchange;
};
@@ -348,6 +380,12 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}.
agents written in JavaScript, and possibly living in <{iframe}>s. The [=user agent=]'s [=browser
agent=] uses a different internal mechanism to retrieve the tools exposed to it.
+
+ document.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}
+
+ Executes a tool on the document it was registered on. Returns a promise that resolves to the
+ result of the tool's execution.
+
@@ -451,7 +489,8 @@ The registerTool(tool, optionsregisterTool(tool, optionsgetTools(options) method steps
+
+The executeTool(tool, inputArguments,
+options) method steps are:
+
+1. Let |invokingDocument| be [=this=]'s [=relevant global object=]'s [=associated
+ Document|associated Document=].
+
+1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=]
+ an "{{InvalidStateError}}" {{DOMException}}.
+
+1. If this's [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and this's
+ [=relevant settings object=]'s [=environment settings object/origin=]'s [=origin/scheme=] is not
+ "file", then return [=a promise rejected with=] a "{{SecurityError}}"
+ {{DOMException}}.
+
+1. If |invokingDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise
+ rejected with=] a "{{NotAllowedError}}" {{DOMException}}.
+
+1. Let |expectedTargetOriginURL| be the result of [=URL parser|parsing=] |tool|'s
+ {{RegisteredTool/origin}}.
+
+1. If |expectedTargetOriginURL| is failure, then return [=a promise rejected with=] a
+ "{{DataError}}" {{DOMException}}.
+
+1. Let |expectedTargetOrigin| be |expectedTargetOriginURL|'s [=url/origin=].
+
+1. [=Assert=]: |expectedTargetOrigin| is not an [=opaque origin=].
+
+1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=].
+
+1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}} if it [=map/exists=]; otherwise null.
+
+1. If |signal| is not null, then:
+
+ 1. If |signal| is [=AbortSignal/aborted=], then return [=a promise rejected with=] |signal|'s
+ [=AbortSignal/abort reason=].
+
+ 1. [=AbortSignal/add|Add the following abort steps=] to |signal|:
+
+ 1. [=Reject=] |promise| with |signal|'s [=AbortSignal/abort reason=].
+
+ Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so that tool abort is communicated all the way through.
+
+1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}.
+
+1. Let |targetDocument| be |targetWindow|'s [=associated Document|associated
+ Document=].
+
+1. Run the following steps [=in parallel=]:
+
+ 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=] is not
+ |invokingDocument|'s [=node navigable=]'s [=navigable/traversable navigable=], then [=queue a
+ global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global
+ object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these
+ steps.
+
+ Issue: TODO(domfarolino): File an issue about cross-frame-tree, same-browsing-context-group
+ tool execution.
+
+ 1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=].
+
+ 1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=].
+
+ 1. If |targetOrigin| is not [=same origin=] with |expectedTargetOrigin|, then [=queue a global
+ task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to
+ [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
+
+ Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
+
+ 1. Let |targetToolMap| be |targetDocument|'s [=Document/associated ModelContext|associated
+ ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=].
+
+ 1. Let |toolName| be |tool|'s {{RegisteredTool/name}}.
+
+ 1. If |targetToolMap|[|toolName|] does not [=map/exist=], then [=queue a global task=] on the
+ [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=]
+ |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
+
+ Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
+
+ 1. Let |tool definition| be |targetToolMap|[|toolName|].
+
+ 1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool
+ definition/exposed origins=], and |callerOrigin| is false, then [=queue a global task=] on the
+ [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=]
+ |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
+
+ Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
+
+ 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=boolean=] |success|, and runs the following steps:
+
+ 1. [=Assert=]: these steps are running [=in parallel=].
+
+ 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|.
+
+ 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}.
+
+ 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run |tool
+ definition|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|.
+
+1. Return |promise|.
+
+
+
ModelContextTool Dictionary
The {{ModelContextTool}} dictionary describes a tool that can be invoked by [=agents=].
@@ -617,19 +760,21 @@ callback ToolExecuteCallback = Promise (object input);
tool["{{ModelContextTool/name}}"]
-
-
A unique identifier for the tool. This is used by [=agents=] to reference the tool when making tool calls.
+
A unique identifier for the tool. This is used by [=agents=] to reference the tool when
+ making tool calls.
tool["{{ModelContextTool/title}}"]
-
-
A label for the tool. This is used by the user agent to reference the tool in the user interface.
-
It is recommended that this string be localized to the user's
-{{NavigatorLanguage/language}}.
+
A label for the tool. This is used by the user agent to reference the tool in the user
+ interface.
It is recommended that this string be localized to the user's
+ {{NavigatorLanguage/language}}.
tool["{{ModelContextTool/description}}"]
-
-
A natural language description of the tool's functionality. This helps [=agents=] understand when and how to use the tool.
+
A natural language description of the tool's functionality. This helps [=agents=] understand
+ when and how to use the tool.
tool["{{ModelContextTool/inputSchema}}"]
@@ -700,6 +845,22 @@ dictionary ModelContextGetToolOptions {
same-origin documents.
+
+
+The {{ExecuteToolOptions}} dictionary allows web applications to pass options to
+{{ModelContext/executeTool()}}.
+
+
+dictionary ExecuteToolOptions {
+ AbortSignal signal;
+};
+
+
+
+ : options["{{ExecuteToolOptions/signal}}"]
+ :: An {{AbortSignal}} that can be used to cancel the execution of the tool.
+
+
The {{RegisteredTool}} dictionary represents a tool that has been registered and is available for
From 90ccdfa2db714dc610d2ce87f6b7393310b7b453 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Tue, 21 Jul 2026 19:59:20 -0400
Subject: [PATCH 02/15] Remove spec issue now that #227 is filed
---
index.bs | 3 ---
1 file changed, 3 deletions(-)
diff --git a/index.bs b/index.bs
index c7a42b9..13081bf 100644
--- a/index.bs
+++ b/index.bs
@@ -685,9 +685,6 @@ The executeTool(tool, inputArgument
object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these
steps.
- Issue: TODO(domfarolino): File an issue about cross-frame-tree, same-browsing-context-group
- tool execution.
-
1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=].
1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=].
From f9ef39efa61968296d800c711dd00c24b939bf44 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 09:12:08 -0400
Subject: [PATCH 03/15] Break tool execute steps out; perform on-loop existence
check, and document race
---
index.bs | 80 +++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 71 insertions(+), 9 deletions(-)
diff --git a/index.bs b/index.bs
index 13081bf..cbdd980 100644
--- a/index.bs
+++ b/index.bs
@@ -257,15 +257,73 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin
-The
imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Window}}
-|targetWindow|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows:
+The
tool execute steps, given a [=string=] |toolName|, a {{Document}} |targetDocument|,
+a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows. The
+|completionSteps| algorithm takes a [=string=]-or-null
result and a [=boolean=]
success.
+
+1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=].
+
+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 false,
+ and abort these steps.
+
+ Issue: Support the plumbing of more granular errors back to the invoker; this should result in a
+ "{{NotFoundError}}" in the invoking document.
+
+
+
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 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.
+
+
+
+ // -- Tool owner document. --
+ const oldInputSchema = {...};
+ const newInputSchema = {...};
+ const ac = new AbortController();
+ document.modelContext.registerTool({..., inputSchema: oldInputSchema}, {signal: ac.signal});
+
+ // 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}");
+
+
+
+
+1. Let |tool| be |toolMap|[|toolName|].
+
+1. Run |tool|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|.
+
+ Note: This is the point where we branch into either the [=imperative execute steps=] or the [=declarative execute steps=].
+
+
-1. [=Assert=]: these steps are running on |targetWindow|'s [=relevant agent=]'s [=agent/event
+
+The
imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Document}} |targetDocument|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows:
+
+1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event
loop=].
1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given
- |inputArguments| and |targetWindow|'s [=relevant realm=]. If
exception was thrown, then
- run |completionSteps| given null and false, and abort these steps.
+ |inputArguments| and |targetDocument|'s [=relevant realm=]. If
exception was thrown, then run |completionSteps| given null and false, and abort
+ these steps.
Issue: Support more granular errors; here we should return something that prompts the caller to reject its {{Promise}} with a "{{DataError}}" {{DOMException}}.
@@ -489,8 +547,7 @@ The
registerTool(tool, optionsexecuteTool(tool, inputArgument
1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}.
- 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run |tool
- definition|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|.
+ 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|.
1. Return |promise|.
@@ -950,6 +1006,12 @@ The synthesize a declarative JSON Schema object algorithm, given a <{
}
+
+The declarative execute steps are as follows.
+
+domfarolino
+
+
Events
The following are the [=event handlers=] (and their corresponding [=event handler event types=])
From c968f7988cc5313f37ef6c209b13e7db4e1934a8 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 14:18:55 +0000
Subject: [PATCH 04/15] bf-cache note and cross-BCG note
---
index.bs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/index.bs b/index.bs
index cbdd980..babb083 100644
--- a/index.bs
+++ b/index.bs
@@ -96,6 +96,9 @@ spec:html; type:dfn;
text:is origin-keyed
+spec: html; urlPrefix: https://html.spec.whatwg.org/C
+ type: dfn
+ text: browsing context group
spec: SECURE-CONTEXTS; urlPrefix: https://w3c.github.io/webappsec-secure-contexts/
type: abstract-op
text: is origin potentially trustworthy?; url: is-origin-trustworthy
@@ -742,6 +745,9 @@ The executeTool(tool, inputArgument
object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these
steps.
+ Issue(#227): Consider supporting tool execution across top-level documents in the same
+ [=browsing context group=].
+
1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=].
1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=].
@@ -782,6 +788,11 @@ The executeTool(tool, inputArgument
1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|.
+ Note: Because documents only process tasks on their event loops when [=Document/fully
+ active=], if |targetDocument| is not [=Document/fully active=], this will simply queue the
+ steps to execute the tool, to run when the document finally becomes active again (i.e., when
+ it leaves the bf-cache).
+
1. Return |promise|.
From 1d6b4bdaa289f38566759cf2d2bca9e9a9a76849 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 14:27:57 +0000
Subject: [PATCH 05/15] Default webidl spec
---
index.bs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/index.bs b/index.bs
index babb083..991ad81 100644
--- a/index.bs
+++ b/index.bs
@@ -94,6 +94,8 @@ spec:html; type:dfn;
text:browsing context group set
text:unique internal value
text:is origin-keyed
+spec: webidl; type: dfn;
+ text: an exception was thrown
spec: html; urlPrefix: https://html.spec.whatwg.org/C
@@ -324,7 +326,7 @@ The imperative execute steps, given a {{ModelContextTool}} |tool|, a
loop=].
1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given
- |inputArguments| and |targetDocument|'s [=relevant realm=]. If exception was thrown, then run |completionSteps| given null and false, and abort
these steps.
From fda9bde1a7c960a3644046d66dea3000623d7abb Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 15:13:06 +0000
Subject: [PATCH 06/15] Formalize this
---
index.bs | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/index.bs b/index.bs
index 991ad81..a0124d5 100644
--- a/index.bs
+++ b/index.bs
@@ -417,7 +417,7 @@ The {{ModelContext}} interface provides methods for web applications to register
interface ModelContext : EventTarget {
Promise registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
Promise> getTools(optional ModelContextGetToolOptions options = {});
- Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {});
+ Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {});
attribute EventHandler ontoolchange;
};
@@ -701,7 +701,7 @@ The executeTool(tool, inputArgument
1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=]
an "{{InvalidStateError}}" {{DOMException}}.
-1. If this's [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and this's
+1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and [=this=]'s
[=relevant settings object=]'s [=environment settings object/origin=]'s [=origin/scheme=] is not
"file", then return [=a promise rejected with=] a "{{SecurityError}}"
{{DOMException}}.
@@ -721,9 +721,9 @@ The executeTool(tool, inputArgument
1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=].
-1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}} if it [=map/exists=]; otherwise null.
+1. If |options|'s {{ModelContextExecuteToolOptions/signal}} [=map/exists=], then:
-1. If |signal| is not null, then:
+ 1. Let |signal| be |options|'s {{ModelContextExecuteToolOptions/signal}}.
1. If |signal| is [=AbortSignal/aborted=], then return [=a promise rejected with=] |signal|'s
[=AbortSignal/abort reason=].
@@ -911,19 +911,18 @@ dictionary ModelContextGetToolOptions {
same-origin documents.
-
+ModelContextExecuteToolOptions Dictionary
-The {{ExecuteToolOptions}} dictionary allows web applications to pass options to
-{{ModelContext/executeTool()}}.
+The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to {{ModelContext/executeTool()}}.
-dictionary ExecuteToolOptions {
+dictionary ModelContextExecuteToolOptions {
AbortSignal signal;
};
- : options["{{ExecuteToolOptions/signal}}"]
+ : options["{{ModelContextExecuteToolOptions/signal}}"]
:: An {{AbortSignal}} that can be used to cancel the execution of the tool.
From e10a9f208ba9c41ff44d3912124698069c1f5779 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 15:39:44 +0000
Subject: [PATCH 07/15] Fix origin errors
---
index.bs | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/index.bs b/index.bs
index a0124d5..0c28b39 100644
--- a/index.bs
+++ b/index.bs
@@ -446,8 +446,7 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}.
document.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}
- Executes a tool on the document it was registered on. Returns a promise that resolves to the
- result of the tool's execution.
+ Executes a tool on the document it was registered on. Returns a promise that resolves to the stringified result of the tool's execution.
@@ -712,8 +711,9 @@ The executeTool(tool, inputArgument
1. Let |expectedTargetOriginURL| be the result of [=URL parser|parsing=] |tool|'s
{{RegisteredTool/origin}}.
-1. If |expectedTargetOriginURL| is failure, then return [=a promise rejected with=] a
- "{{DataError}}" {{DOMException}}.
+1. If |expectedTargetOriginURL| is failure, or |expectedTargetOriginURL|'s [=url/origin=] is an
+ [=opaque origin=], then return [=a promise rejected with=] a "{{NotSupportedError}}"
+ {{DOMException}}.
1. Let |expectedTargetOrigin| be |expectedTargetOriginURL|'s [=url/origin=].
@@ -750,6 +750,8 @@ The executeTool(tool, inputArgument
Issue(#227): Consider supporting tool execution across top-level documents in the same
[=browsing context group=].
+ Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
+
1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=].
1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=].
@@ -774,9 +776,9 @@ The executeTool(tool, inputArgument
1. Let |tool definition| be |targetToolMap|[|toolName|].
1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool
- definition/exposed origins=], and |callerOrigin| is false, then [=queue a global task=] on the
- [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=]
- |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
+ definition/exposed origins=], and |callerOrigin| returns false, then [=queue a global task=]
+ on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to
+ [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
From d6ce1bb30a59ce0c70a040c8099fa776204afa2c Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 11:57:59 -0400
Subject: [PATCH 08/15] Report a warning
---
index.bs | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/index.bs b/index.bs
index 0c28b39..3907e76 100644
--- a/index.bs
+++ b/index.bs
@@ -343,11 +343,15 @@ The imperative execute steps, given a {{ModelContextTool}} |tool|, a
- If |toolPromise| was fulfilled with value |v|:
- 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps.
+ 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps.
- 1. Run |completionSteps| given |serializedResult| and true.
+ 1. Run |completionSteps| given |serializedResult| and true.
- - If |toolPromise| was rejected with reason r, then run |completionSteps| given null and false.
+ - If |toolPromise| was rejected with reason |r|, then:
+
+ 1. Optionally [=report a warning to the console=] describing |r|.
+
+ 1. Run |completionSteps| given null and false.
From 2f3688b4e9599738059fab56c38770a02f2ad646 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 14:00:16 -0400
Subject: [PATCH 09/15] Reference toolcanceled event
---
index.bs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/index.bs b/index.bs
index 3907e76..46ca1a8 100644
--- a/index.bs
+++ b/index.bs
@@ -736,7 +736,10 @@ The executeTool(tool, inputArgument
1. [=Reject=] |promise| with |signal|'s [=AbortSignal/abort reason=].
- Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so that tool abort is communicated all the way through.
+ Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so
+ that tool abort is communicated all the way through. This should also fire the
+ "toolcanceled" event in the target document. See also issue #146.
1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}.
From 8828678d135b7cf9832f00c764f6202d344ab74f Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 27 Jul 2026 14:32:26 -0400
Subject: [PATCH 10/15] Don't let DOMString be null
---
index.bs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/index.bs b/index.bs
index 46ca1a8..12a59a4 100644
--- a/index.bs
+++ b/index.bs
@@ -421,7 +421,7 @@ The {{ModelContext}} interface provides methods for web applications to register
interface ModelContext : EventTarget {
Promise registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
Promise> getTools(optional ModelContextGetToolOptions options = {});
- Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {});
+ Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {});
attribute EventHandler ontoolchange;
};
From eea098a999a25657037793738acc8ab7f3780fea Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Tue, 28 Jul 2026 16:11:32 +0000
Subject: [PATCH 11/15] Spec the full pending execution cancellation
infrastructure
---
index.bs | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 152 insertions(+), 5 deletions(-)
diff --git a/index.bs b/index.bs
index 12a59a4..1abf191 100644
--- a/index.bs
+++ b/index.bs
@@ -193,6 +193,76 @@ An annotations is a [=struct=] with the following [=struct/items=]:
:: a [=boolean=], initially false.
+
+
+A pending tool execution is a [=struct=] with the following [=struct/items=]:
+
+
+ : caller document
+ :: a {{Document}}.
+
+ : target document
+ :: a {{Document}}.
+
+ : tool name
+ :: a [=string=].
+
+ : completion steps
+ :: an algorithm that takes a [=string=]-or-null and a [=boolean=].
+
+
+A [=traversable navigable=] has a pending tool executions
+map, which is a [=map=] whose keys are [=unique internal values=] and whose values are
+[=pending tool execution=] [=structs=]. It is initially empty.
+
+Note: This map is only ever mutated from steps [=in parallel=]. This simulates the single,
+authoritative "browser process" that most modern browsers implement, where execution tracking sits
+outside any individual Document process's event loop, and is accessed asynchronously via some
+inter-process communication mechanism.
+
+
+
+
+This specification's [=unloading document cleanup steps=], given a {{Document}} |document|, are as
+follows:
+
+1. Let |traversable| be |document|'s [=node navigable=]'s [=navigable/traversable navigable=].
+
+1. Run the following steps [=in parallel=]:
+
+ 1. Let |executionsToRemove| be an empty [=list=].
+
+ 1. [=map/For each=] |uuid| → |execution| of |traversable|'s [=traversable navigable/pending
+ tool executions map=]:
+
+ 1. If |document| is |execution|'s [=pending tool execution/target document=] or |document| is
+ |execution|'s [=pending tool execution/caller document=], then [=list/append=] |uuid| to
+ |executionsToRemove|.
+
+ 1. [=list/For each=] |uuid| of |executionsToRemove|:
+
+ 1. Let |execution| be |traversable|'s [=traversable navigable/pending tool executions
+ map=][|uuid|].
+
+ 1.
If |document| is |execution|'s [=pending tool
+ execution/target document=], then run |execution|'s [=pending tool execution/completion
+ steps=] given null and false.
+
+ Note: This removes |execution| from the [=traversable navigable/pending tool executions
+ map=].
+
+ 1. If |document| is |execution|'s [=pending tool execution/caller document=], then:
+
+ 1. [=map/Remove=] |traversable|'s [=traversable navigable/pending tool executions
+ map=][|uuid|].
+
+ 1. Issue(#48): Abort the `AbortSignal` in |execution|'s [=pending tool execution/target
+ document=], so the tool can abort early now that the caller is dead.
+
+ 1. [=Assert=]: |traversable|'s [=traversable navigable/pending tool executions map=][|uuid|]
+ does not [=map/exist=].
+
+
@@ -288,7 +358,7 @@ a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follow
when issue #92 is
resolved.
-
+
// -- Tool owner document. --
const oldInputSchema = {...};
@@ -371,10 +441,33 @@ To unregister a tool given a {{ModelContext}} |mo
1. [=map/Remove=] |tool map|[|tool name|].
+1. Let |targetDocument| be |modelContext|'s [=relevant global object=]'s [=associated
+ Document|associated Document=].
+
+1. Let |traversable| be |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=].
+
1. Run the following steps [=in parallel=]:
- 1. [=Notify documents of a tool change=] given |modelContext|'s [=relevant global object=]'s
- [=associated Document|associated Document=] and |exposed origins|.
+ 1. Let |canceledExecutions| be an empty [=list=].
+
+ 1. [=map/For each=] |uuid| → |execution| of |traversable|'s [=traversable navigable/pending
+ tool executions map=]:
+
+ 1. If |execution|'s [=pending tool execution/target document=] is |targetDocument| and
+ |execution|'s [=pending tool execution/tool name=] is |tool name|, then [=list/append=]
+ |uuid| to |canceledExecutions|.
+
+ 1. [=list/For each=] |uuid| of |canceledExecutions|:
+
+ 1. Let |execution| be |traversable|'s [=traversable navigable/pending tool executions
+ map=][|uuid|].
+
+ 1. Run |execution|'s [=pending tool execution/completion steps=] given null and false.
+
+ Note: This removes |execution| from the [=traversable navigable/pending tool executions
+ map=].
+
+ 1. [=Notify documents of a tool change=] given |targetDocument| and |exposed origins|.
@@ -789,14 +882,67 @@ The
executeTool(tool, inputArgument
Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
+ 1. Let |uuid| be a new [=unique internal value=].
+
1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=boolean=] |success|, and runs the following steps:
1. [=Assert=]: these steps are running [=in parallel=].
+ 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] does not [=map/exist=], then return.
+
+
+
It is possible that a pending execution identified by |uuid| no longer exists. This can
+ happen due to a race between (a) tool cancellation when the caller document gets destroyed; and (b) tool promise resolution. Both
+ of these race to invoke |completionSteps|, and the first invocation will remove the
+ pending execution by its key |uuid|, this check protects subsequent racing invocations.
+
+
This can also happen due to a limitation with tool unregistration. Consider this
+ example:
+
+
+
+ const ac = new AbortController();
+ document.modelContext.registerTool({
+ name: "tool"
+ description: "a racy tool",
+ execute: async () => {
+ return new Promise(resolve => {
+ resolve("tool result");
+ ac.abort();
+ });
+ }
+ }, {signal: ac.signal});
+
+
+
In this case, tool unregistration happens after a tool Promise is resolved, but before
+ its fulfillment handler runs. Since both run |completionSteps|, this check protects the
+ second invocation from trying to remove the same pending execution twice.
+
+
+
+ 1. [=map/Remove=] |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|].
+
1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|.
1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}.
+ 1. Let |execution| be a new [=pending tool execution=], with the following [=struct/items=]:
+
+ : [=pending tool execution/caller document=]
+ :: |invokingDocument|
+
+ : [=pending tool execution/target document=]
+ :: |targetDocument|
+
+ : [=pending tool execution/tool name=]
+ :: |toolName|
+
+ : [=pending tool execution/completion steps=]
+ :: |completionSteps|
+
+ 1. Set |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] to |execution|.
+
1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|.
Note: Because documents only process tasks on their event loops when [=Document/fully
@@ -1028,9 +1174,10 @@ The synthesize a declarative JSON Schema object algorithm, given a <{
-The declarative execute steps are as follows.
+The declarative execute steps are as follows:
+
+Issue: Spec the declarative execution steps, and their integration with form elements.
-domfarolino
Events
From 381b37654797159d09a46c93d42b26a686c4b996 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Tue, 28 Jul 2026 12:15:05 -0400
Subject: [PATCH 12/15] specfmt
---
index.bs | 91 +++++++++++++++++++++++++++++++++++---------------------
1 file changed, 57 insertions(+), 34 deletions(-)
diff --git a/index.bs b/index.bs
index 1abf191..e884548 100644
--- a/index.bs
+++ b/index.bs
@@ -169,9 +169,11 @@ A tool definition is a [=struct=] with the following [=struct/items=]
[[!JSON-SCHEMA]]
: execute steps
- :: an algorithm that takes both a [=string=] and an algorithm completionSteps that itself takes a [=string=]-or-null and a [=boolean=].
+ :: an algorithm that takes both a [=string=] and an algorithm completionSteps
+ that itself takes a [=string=]-or-null and a [=boolean=].
- Note: For tools registered imperatively, these steps will simply invoke the [=imperative execute steps=]. For tools registered
+ Note: For tools registered imperatively, these steps will simply invoke the [=imperative
+ execute steps=]. For tools registered
[declaratively](https://github.com/webmachinelearning/webmcp/pull/76), this will be a set of
"internal" steps that have not been defined yet, that describe how to fill out a <{form}> and
its [=form-associated elements=].
@@ -332,11 +334,13 @@ 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|, and an algorithm |completionSteps|, are as follows. The
-|completionSteps| algorithm takes a [=string=]-or-null
result and a [=boolean=]
success.
+The
tool execute steps, given a [=string=] |toolName|, a {{Document}} |targetDocument|, a
+[=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows. The
+|completionSteps| algorithm takes a [=string=]-or-null
result and a [=boolean=]
+
success.
-1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=].
+1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event
+ loop=].
1. Let |toolMap| be |targetDocument|'s [=Document/associated ModelContext|associated
ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=].
@@ -350,13 +354,12 @@ a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follow
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.
+ re-registration of a tool with the same |toolName| but input schema is
not protected
+ against.
-
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.
+
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.
@@ -385,24 +388,28 @@ a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follow
1. Run |tool|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|.
- Note: This is the point where we branch into either the [=imperative execute steps=] or the [=declarative execute steps=].
+ Note: This is the point where we branch into either the [=imperative execute steps=] or the
+ [=declarative execute steps=].
-The
imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Document}} |targetDocument|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows:
+The
imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Document}}
+|targetDocument|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows:
1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event
loop=].
1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given
- |inputArguments| and |targetDocument|'s [=relevant realm=]. If
exception was thrown, then run |completionSteps| given null and false, and abort
+ |inputArguments| and |targetDocument|'s [=relevant realm=]. If
exception was thrown, then run |completionSteps| given null and false, and abort
these steps.
- Issue: Support more granular errors; here we should return something that prompts the caller to reject its {{Promise}} with a "{{DataError}}" {{DOMException}}.
+ Issue: Support more granular errors; here we should return something that prompts the caller to
+ reject its {{Promise}} with a "{{DataError}}" {{DOMException}}.
-1. If |inputObject| [=Object type|is not an Object=] is false, then run |completionSteps| given null and false, and abort these steps.
+1. If |inputObject| [=Object type|is not an Object=] is false, then run |completionSteps| given null
+ and false, and abort these steps.
Issue(#146): Specify and fire the "
toolactivated" event.
@@ -413,7 +420,9 @@ The
imperative execute steps, given a {{ModelContextTool}} |tool|, a
- If |toolPromise| was fulfilled with value |v|:
- 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps.
+ 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON
+ string=] given |v|. If this throws an exception, run |completionSteps| given null and
+ false, and abort these steps.
1. Run |completionSteps| given |serializedResult| and true.
@@ -543,7 +552,8 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}.
document.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}
- Executes a tool on the document it was registered on. Returns a promise that resolves to the stringified result of the tool's execution.
+ Executes a tool on the document it was registered on. Returns a promise that resolves to the
+ stringified result of the tool's execution.
@@ -648,7 +658,9 @@ The
registerTool(tool, optionsexecuteTool(tool, inputArgument
1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=]
an "{{InvalidStateError}}" {{DOMException}}.
-1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and [=this=]'s
- [=relevant settings object=]'s [=environment settings object/origin=]'s [=origin/scheme=] is not
- "file", then return [=a promise rejected with=] a "{{SecurityError}}"
- {{DOMException}}.
+1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and
+ [=this=]'s [=relevant settings object=]'s [=environment settings object/origin=]'s
+ [=origin/scheme=] is not "file", then return [=a promise rejected with=] a
+ "{{SecurityError}}" {{DOMException}}.
1. If |invokingDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise
rejected with=] a "{{NotAllowedError}}" {{DOMException}}.
@@ -884,18 +896,22 @@ The executeTool(tool, inputArgument
1. Let |uuid| be a new [=unique internal value=].
- 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=boolean=] |success|, and runs the following steps:
+ 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a
+ [=boolean=] |success|, and runs the following steps:
1. [=Assert=]: these steps are running [=in parallel=].
- 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] does not [=map/exist=], then return.
+ 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s
+ [=traversable navigable/pending tool executions map=][|uuid|] does not [=map/exist=], then
+ return.
It is possible that a pending execution identified by |uuid| no longer exists. This can
happen due to a race between (a) tool cancellation when the caller document gets destroyed; and (b) tool promise resolution. Both
of these race to invoke |completionSteps|, and the first invocation will remove the
- pending execution by its key |uuid|, this check protects subsequent racing invocations.
+ pending execution by its key |uuid|, this check protects subsequent racing
+ invocations.
This can also happen due to a limitation with tool unregistration. Consider this
example:
@@ -921,11 +937,15 @@ The
executeTool(tool, inputArgument
- 1. [=map/Remove=] |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|].
+ 1. [=map/Remove=] |targetDocument|'s [=node navigable=]'s [=navigable/traversable
+ navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|].
- 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|.
+ 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given
+ |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|.
- 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}.
+ 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s
+ [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}"
+ {{DOMException}}.
1. Let |execution| be a new [=pending tool execution=], with the following [=struct/items=]:
@@ -941,9 +961,11 @@ The
executeTool(tool, inputArgument
: [=pending tool execution/completion steps=]
:: |completionSteps|
- 1. Set |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] to |execution|.
+ 1. Set |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s
+ [=traversable navigable/pending tool executions map=][|uuid|] to |execution|.
- 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|.
+ 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool
+ execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|.
Note: Because documents only process tasks on their event loops when [=Document/fully
active=], if |targetDocument| is not [=Document/fully active=], this will simply queue the
@@ -1068,7 +1090,8 @@ dictionary ModelContextGetToolOptions {
ModelContextExecuteToolOptions Dictionary
-The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to {{ModelContext/executeTool()}}.
+The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to
+{{ModelContext/executeTool()}}.
dictionary ModelContextExecuteToolOptions {
From 0967151ef749e838bf423e838065b61f27141d3d Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Mon, 3 Aug 2026 13:52:16 -0400
Subject: [PATCH 13/15] Rename to callerDocument
---
index.bs | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/index.bs b/index.bs
index e884548..732d006 100644
--- a/index.bs
+++ b/index.bs
@@ -349,7 +349,7 @@ The tool execute steps, given a [=string=] |toolName|, a {{Document}}
and abort these steps.
Issue: Support the plumbing of more granular errors back to the invoker; this should result in a
- "{{NotFoundError}}" in the invoking document.
+ "{{NotFoundError}}" in the calling document.
This protects us against a race between tool unregistration and execution. While tool
@@ -803,10 +803,10 @@ The getTools(options) method steps
The executeTool(tool, inputArguments,
options) method steps are:
-1. Let |invokingDocument| be [=this=]'s [=relevant global object=]'s [=associated
+1. Let |callerDocument| be [=this=]'s [=relevant global object=]'s [=associated
Document|associated Document=].
-1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=]
+1. If |callerDocument| is not [=Document/fully active=], then return [=a promise rejected with=]
an "{{InvalidStateError}}" {{DOMException}}.
1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and
@@ -814,7 +814,7 @@ The executeTool(tool, inputArgument
[=origin/scheme=] is not "file", then return [=a promise rejected with=] a
"{{SecurityError}}" {{DOMException}}.
-1. If |invokingDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise
+1. If |callerDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise
rejected with=] a "{{NotAllowedError}}" {{DOMException}}.
1. Let |expectedTargetOriginURL| be the result of [=URL parser|parsing=] |tool|'s
@@ -854,8 +854,8 @@ The executeTool(tool, inputArgument
1. Run the following steps [=in parallel=]:
1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=] is not
- |invokingDocument|'s [=node navigable=]'s [=navigable/traversable navigable=], then [=queue a
- global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global
+ |callerDocument|'s [=node navigable=]'s [=navigable/traversable navigable=], then [=queue a
+ global task=] on the [=webmcp task source=] given |callerDocument|'s [=relevant global
object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these
steps.
@@ -866,10 +866,10 @@ The executeTool(tool, inputArgument
1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=].
- 1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=].
+ 1. Let |callerOrigin| be |callerDocument|'s [=Document/origin=].
1. If |targetOrigin| is not [=same origin=] with |expectedTargetOrigin|, then [=queue a global
- task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to
+ task=] on the [=webmcp task source=] given |callerDocument|'s [=relevant global object=] to
[=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
@@ -880,7 +880,7 @@ The executeTool(tool, inputArgument
1. Let |toolName| be |tool|'s {{RegisteredTool/name}}.
1. If |targetToolMap|[|toolName|] does not [=map/exist=], then [=queue a global task=] on the
- [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=]
+ [=webmcp task source=] given |callerDocument|'s [=relevant global object=] to [=reject=]
|promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
@@ -889,7 +889,7 @@ The executeTool(tool, inputArgument
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 |invokingDocument|'s [=relevant global object=] to
+ on the [=webmcp task source=] given |callerDocument|'s [=relevant global object=] to
[=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps.
Issue: Support more granular errors than "{{UnknownError}}", based on each failure case.
@@ -941,16 +941,16 @@ The executeTool(tool, inputArgument
navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|].
1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given
- |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|.
+ |callerDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|.
- 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s
+ 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |callerDocument|'s
[=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}"
{{DOMException}}.
1. Let |execution| be a new [=pending tool execution=], with the following [=struct/items=]:
: [=pending tool execution/caller document=]
- :: |invokingDocument|
+ :: |callerDocument|
: [=pending tool execution/target document=]
:: |targetDocument|
From ddc99140861cf2d6538440b68d92394d00bbbde1 Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Thu, 13 Aug 2026 20:32:34 +0000
Subject: [PATCH 14/15] Add examples
---
README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 70 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index c26257d..14c9f95 100644
--- a/README.md
+++ b/README.md
@@ -325,7 +325,7 @@ By default, WebMCP is enabled in top-level `Window`s and its same-origin iframes
Calls to `document.modelContext.registerTool()` will return a promise rejected with `NotAllowedError` DOMException when the permission is disabled, whether by the `allow` attribute or the `Permissions-Policy: tools=()` header. Handling of declarative tool registration errors, including when the permission is disabled is TBD; see [Issue #182](https://github.com/webmachinelearning/webmcp/issues/182).
-#### Cross-origin iframe exposure: `exposedTo`
+#### Cross-origin iframe exposure: `registerTool() and `exposedTo`
By default, tools registered by a document are only exposed to itself, same-origin documents in the same tree, and built-in browser agents (see this discussion). To support author-provided agents running in frames, developers can selectively share tools with secure origins of their choice, `exposedTo` option during registration:
@@ -342,9 +342,76 @@ Any document in the tree matching these origins (and allowed to use `tools` perm
- Receive the `toolchange` event on its `document.modelContext` when the tool is registered or unregistered.
- Be able to discover and run the tools
-#### Discovering and running tools
+#### Discovering and running tools: `getTools()` and `executeTool()`
-TODO: Spec and describe the `modelContext.getTools()` and `modelContext.executeTool()` APIs.
+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`:
+
+```js
+// Discover tools exposed by same-origin frames in the tree (default)
+const tools = await document.modelContext.getTools();
+
+for (const tool of tools) {
+ console.log(`Tool: ${tool.name} (from ${tool.origin})`);
+ console.log(`Description: ${tool.description}`);
+ console.log(`Parameters schema:`, tool.inputSchema);
+}
+
+// Or discover tools exposed by specific cross-origin partner frames:
+const crossOriginTools = await document.modelContext.getTools({
+ fromOrigins: ["https://trusted-partner.example"]
+});
+```
+
+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:
+
+```js
+const tools = await document.modelContext.getTools();
+const addTodoTool = tools.find(t => t.name === "add-todo");
+
+if (addTodoTool) {
+ try {
+ const result = await document.modelContext.executeTool(
+ addTodoTool,
+ { text: "Buy groceries" }
+ );
+ console.log("Tool result:", result);
+ } catch (error) {
+ console.error("Tool execution failed:", error);
+ }
+}
+```
+
+##### Cancelling execution with `AbortSignal`
+
+Tool invocations can be cancelled mid-execution (e.g., if the user aborts an ongoing request, as they might with the "stop button" that's present in most agent UIs) by passing an `AbortSignal`:
+
+```js
+const controller = new AbortController();
+
+const executionPromise = document.modelContext.executeTool(
+ addTodoTool,
+ { text: "Buy groceries" },
+ { signal: controller.signal }
+);
+
+// If the user cancels the interaction:
+stopButton.addEventListener('click', e => controller.abort());
+```
+
+The tool's [execution callback](https://webmachinelearning.github.io/webmcp/#callbackdef-toolexecutecallback) receives this signal via its [`options.signal`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextexecutetooloptions-signal) parameter, allowing it to abort underlying network requests or asynchronous tasks cleanly.
+
+##### Responding to dynamic tool updates: the `toolchange` event
+
+When tools are added, removed, or updated dynamically (such as when user interactions result in new tools being registered), `document.modelContext` fires a `toolchange` event:
+
+```js
+document.modelContext.addEventListener("toolchange", async () => {
+ const currentTools = await document.modelContext.getTools();
+ updateAgentToolRegistry(currentTools);
+});
+```
## Alternatives Considered
From ee98c1b6ce482f3d9bd25883173a1f691802c69d Mon Sep 17 00:00:00 2001
From: Dominic Farolino
Date: Fri, 14 Aug 2026 11:01:16 -0400
Subject: [PATCH 15/15] Correct PR link
---
index.bs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/index.bs b/index.bs
index 732d006..07c7e05 100644
--- a/index.bs
+++ b/index.bs
@@ -844,7 +844,7 @@ The executeTool(tool, inputArgument
Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so
that tool abort is communicated all the way through. This should also fire the
"toolcanceled" event in the target document. See also issue #146.
+ href=https://github.com/webmachinelearning/webmcp/pull/146>pull request #146.
1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}.