Skip to content

Spec the executeTool() API#226

Draft
domfarolino wants to merge 2 commits into
mainfrom
executeTools
Draft

Spec the executeTool() API#226
domfarolino wants to merge 2 commits into
mainfrom
executeTools

Conversation

@domfarolino

@domfarolino domfarolino commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

This PR specifies the executeTool() API on ModelContext.

Closes #57.
Closes #117.
Closes #159.
Closes #160.


Preview | Diff

domfarolino and others added 2 commits July 21, 2026 19:42
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
Comment thread index.bs
interface ModelContext : EventTarget {
Promise<undefined> registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
Promise<sequence<RegisteredTool>> getTools(optional ModelContextGetToolOptions options = {});
Promise<DOMString?> executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the options naming convention, should we also prepend ModelContext to this dictionary name (e.g., ModelContextExecuteToolOptions)?

Suggested change
Promise<DOMString?> executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {});
Promise<DOMString?> executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {});

I now wonder if we should have used ModelContextRegisteredTool as well ;)

Comment thread index.bs
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

This suggestion applies as well to getTools and registerTool.

Comment thread index.bs
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using NotSupportedError instead and return a rejected promise in both cases?

  • expectedTargetOriginURL is a failure
  • expectedTargetOrigin is opaque

Comment thread index.bs
Comment on lines +662 to +664
1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}} if it [=map/exists=]; otherwise null.

1. If |signal| is not null, then:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}} if it [=map/exists=]; otherwise null.
1. If |signal| is not null, then:
1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}}.
1. If |signal| [=map/exists=], then:

Comment thread index.bs
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnknownError feels weird to me in these cases. Maybe InvalidStateError?

Comment thread index.bs
<dt><code><var ignore>document</var>.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}</code></dt>
<dd>
<p>Executes a tool on the document it was registered on. Returns a promise that resolves to the
result of the tool's execution.</p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe extends that it's a stringified result only for now.

Comment thread index.bs
The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArguments</var>,
<var>options</var>)</dfn> method steps are:

1. Let |invokingDocument| be [=this=]'s [=relevant global object=]'s [=associated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Shall we use executingDocument?

Comment thread index.bs
</div>

<div algorithm>
The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArguments</var>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A JS example of how to execute a tool from a cross-origin iframe may come handy in this spec. See previous comment at #223 (comment)

Comment thread index.bs
loop=].

1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given
|inputArguments| and |targetWindow|'s [=relevant realm=]. If <a spec=webidl lt="an exception was thrown">exception was thrown</a>, then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add this webidl bit to <pre class="link-defaults"> at the top of the file?

Comment thread index.bs
interface ModelContext : EventTarget {
Promise<undefined> registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
Promise<sequence<RegisteredTool>> getTools(optional ModelContextGetToolOptions options = {});
Promise<DOMString?> executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR.

Have we consider moving executeTool from document.modelContext to RegisteredTool interface hinted in https://issues.chromium.org/536063275? FWIW This would match what Puppeteer does: https://pptr.dev/guides/webmcp#executing-tools

@beaufortfrancois beaufortfrancois left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread index.bs

1. Run the following steps [=in parallel=]:

1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=] is not

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check that this document is fully active?

Comment thread index.bs

1. Let |toolName| be |tool|'s {{RegisteredTool/name}}.

1. If |targetToolMap|[|toolName|] does not [=map/exist=], then [=queue a global task=] on the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird case we could get into here: |targetToolMap|[|toolName|] isn't the same tool as returned from getTools. It may have been modified in between.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, some modifications are OK, others are not. I'll be carrying over some from our implementation before taking this PR out of draft mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants