You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Tool discovery should not be limited to a single traversable navigable #227
Right now, getTools() retrieves tools from all documents underneath one's traversable navigable, i.e., top-level frame tree. This excludes tools from openers and opened documents, i.e., other documents in the browsing context group, and it probably shouldn't. Within a given browsing context group, we should probably collect tools from all top-level browsing contexts, including auxiliary top browsing contexts.
Right now, getTools() retrieves tools from all documents underneath one's traversable navigable, i.e., top-level frame tree. This excludes tools from openers and opened documents, i.e., other documents in the browsing context group, and it probably shouldn't. Within a given browsing context group, we should probably collect tools from all top-level browsing contexts, including auxiliary top browsing contexts.
I think this relates to a higher-level question - what is the agent’s “discovery radius” (which WebMCP tools it can discover) and related to that, its “action radius” (which tools it can actually use).
As a north star for WebMCP, I think both should eventually extend as far across the internet as possible, subject to the security and performance challenges that need to be solved along the way.
Increasing these radiuses can help agents perform richer tasks for users, but each expansion also introduces tradeoffs. So whenever we move up an abstraction level - for example expanding getTools() from the current document tree to the browsing context group - I think we should explain which new use cases this unlocks, which new risks it introduces, and why the tradeoff is worthwhile.
For example, if a flight-booking site opens seat selection in a separate window, the agent could use the seat-selection tools there and then continue the booking in the original window.
I see where there may be use cases, but I worry about the security properties. Keeping consequential tools away from places where there is more likely to be prompt injection seems like a good principle. Having tools shared between different pages seems like an easy way to have developers mess it up. Consider a travel booking site where a user has the Account Information page (with CRUD operations on credit card, passport, name, email) open separate from another window that renders user reviews of hotels or airlines. Page-locality of tools is a layer of the defense in depth that I might expect there. Of course this only affects in-page agents, where the name in-page itself also strongly suggests this locality! At the very minimum, this would have to follow partitioning identical to Storages, and I would strongly argue that it should be opt-in at the time of tool registration if it exist at all.
I also have a negative reaction to enabling this as a blanket capability without describing the use cases we're trying to enable here.
Assuming there's a good reason to do this, I'm worried that if we just tack it on to the existing getTools / registerTools exposure mechanism sites would not have sufficient granularity for deciding between iframe and opener exposure. This could lead to a lot of issues where developers were not aware that one type of exposure also allowed for the other (for example, sites could assume that a relatively liberal tool exposure policy combined with strict frame-ancestors is enough to prevent other sites from accessing their tools).
I don't think this is impossible, but it needs a careful security analysis.
Particularly if the modelContext is bound to the document.
...
Consider a travel booking site where a user has the Account Information page (with CRUD operations on credit card, passport, name, email) open separate from another window that renders user reviews of hotels or airlines.
...
At the very minimum, this would have to follow partitioning identical to Storages
Yeah, to be clear the scope of getTools() that I had in mind was browsing context group — it should only be possible to invoke tools in windows that you already have postMessage() access to, and could otherwise invoke less ergonomically by just messaging them and telling them to invoke stuff. The idea is that getTools() shouldn't have any farther reach than what your browsing context group has.
I think this mitigates the risk of two, non-coordinating same-origin travel booking tabs accidentally invoking each others tools without realizing each other's state, while still enabling the use case where an in-page agent on that travel booking site can invoke tools in a "flight seat-picker" pop-up window, which is not possible today. Does this clarification about scoping make things less scary @johannhof@bvandersloot-mozilla?
We might want to think through the case where A.com/foo window.open()s another A.com/foo window, which registers the same duplicate set of tools as its opener. Preserving Tool#window lets the agent harness distinguish which tool is from which window, so it doesn't confuse things, but maybe we can go further.
I still doesn't seem to address the concern that sites may not understand that embedding prevention only will not help them avoid exposure of their tools.
I still doesn't seem to address the concern that sites may not understand that embedding prevention only will not help them avoid exposure of their tools.
Update: after further discussion with @domfarolino, I might be convinced that this risk is acceptable. XFA / CSP: F-A operate on origin granularity, like exposedTo, so it's less likely that an origin uses the former mechanisms as a more granular control for tool exposure.
It would still be good if the spec and developer documentation would call out that tool exposure happens across all documents in the BCG, i.e. iframes and popups, to avoid developer confusion which could lead to unintentionally exposing or consuming the wrong tools.
Of course if we resolve this in the affirmative, we'd update the spec and documentation accordingly. I generally agree with what you said about origin granularity, and I think the main risk here is tool coherence; as we expand the tool registration surface area to more "contexts" that might not share application state, it becomes more likely an agent calls two tools consecutively, expecting them to build off each other, when they really poke at different parts of the system, not producing the result the agent expected.
This risk depends on the granularity of tools people write, and might demand a skill-like "tool orchestration layer" to weave together related tools that form a user journey. More experimentation is required to know how this will shake out.
Exposure, from an origin/security and tool coherence perspective are both important to keep in mind as we approach worker integration, which will likely pose questions about bending the boundary beyond browsing context groups.
FYI I'm exploring how in-page agents could discover and execute WebMCP tools inside separate windows — both standard popup windows (window.open) and Document Picture-in-Picture windows (using adoptNode(iframe)). See GoogleChromeLabs/webmcp-tools#363
A measured baseline for the iframe half of this question, from shipping Chrome
Data rather than an opinion. I have been building a measurement harness for WebMCP tool-invocation rates, and since this thread's premise — getTools() collecting from all documents under one traversable navigable — is easy to assume, it seemed worth pinning to a build with a reproduction.
Build: Chrome 152.0.7977.65, Windows 11, chrome://flags/#enable-webmcp-testing enabled, fresh profile.
Result: a same-origin subframe's tool does land in the top frame's manifest.
The top frame'sdocument.modelContext.getTools() → ["host_alpha","host_beta","host_gamma","widget_ping"].
The browser's own view agrees: the DevTools WebMCP domain's toolsAdded events report the same 4 tools, arriving across 2 distinct frameIds. Page view and browser view never disagreed.
Reproduction, no harness needed — serve both files over http (not file:) and read getTools() in the top frame:
<!-- host.html --><iframesrc="widget.html"></iframe><scripttype="module">for(constnameof['host_alpha','host_beta','host_gamma']){awaitdocument.modelContext.registerTool({
name,description: `Host-frame tool ${name}.`,inputSchema: {type: 'object',properties: {}},annotations: {readOnlyHint: true},asyncexecute(){return{tool: name,registeredIn: 'top'};},});}</script><!-- widget.html --><scripttype="module">awaitdocument.modelContext.registerTool({name: 'widget_ping',description: 'A tool registered by an embedded document, not by the host page.',inputSchema: {type: 'object',properties: {}},annotations: {readOnlyHint: true},asyncexecute(){return{registeredIn: 'iframe',href: location.href};},});</script>
Why this may bear on the granularity concern above. Provenance already exists at the browser layer but not in the page API. Each toolsAdded event carries a frameId (and a stack trace), so the browser knows which document registered each tool — but getTools() returns no frame or origin attribution, so a host page cannot tell its own tools from an embed's. If the registration surface widens to more contexts, that asymmetry looks worth closing: frame/origin attribution on the page side is the information a site would need both for @johannhof's point about embedding controls not being sufficient, and for @domfarolino's tool-coherence risk — an agent calling two tools consecutively cannot currently be told that they came from different documents either.
One adjacent measurement, since widening the surface implies larger manifests: on this build I found no observable tool-count ceiling up to 507 registered tools — all accepted, all returned by getTools(), all surfaced in the browser view — with settle time rising only from 1.04 s to 1.30 s. Tool volume does not appear to self-limit as more contexts are folded in.
Cross-origin follow-up: tools is a Permissions Policy feature, and with it delegated the page view and the agent view disagree
Following up my own comment above — the cross-origin case I said was untested is now measured, same build (Chrome 152.0.7977.65, #enable-webmcp-testing, fresh profile). Three cases, host page registering 3 tools and an embed registering widget_ping:
embed
host's getTools()
embed's own getTools()
browser's WebMCP.toolsAdded
same-origin
host_alpha, host_beta, host_gamma, widget_ping
same 4
4, across 2 frames
cross-origin, no allow
3 host tools
throws
3, across 1 frame
cross-origin, allow="tools"
3 host tools
widget_ping only
4, across 2 frames
Two things fall out of that.
1. There is already an origin-granular control, and it is not allow-listed by default. In a cross-origin child without delegation, document.modelContext exists but every call throws:
Access to the feature "tools" is disallowed by permissions policy.
So a cross-origin embed cannot silently add tools to its host — <iframe allow="tools"> is required, which is opt-in at origin granularity and inherited from the framing document. That seems worth having explicit in this thread, since a few comments above reason about what sites can and cannot control.
2. With delegation, the page-side and agent-side views of the manifest diverge. This is the part I did not expect. In the allow="tools" row:
the host page'sgetTools() returns only its own 3 tools;
the embed'sgetTools() returns only widget_ping;
the browser'stoolsAdded stream reports all 4, arriving across 2 frameIds.
So cross-origin frames keep separate manifests as seen from script, while the browser aggregates both for whatever consumes tools. A host page therefore cannot enumerate what an agent can actually call on it — no script-visible surface returns the union. Same-origin frames behave the opposite way: one shared manifest, both frames see all 4.
For what it is worth, that divergence is the only one I have found: page view and browser view agreed exactly at 7, 71, 187, 307 and 507 tools, and across a same-origin iframe. This is the first case where they do not, and it is structural rather than a race — it reproduces on repeated runs.
Reproduction is the two files from my earlier comment, plus allow="tools" on the iframe and the embed served from a second origin; the embed's own frame is reachable over CDP by matching Runtime.executionContextCreated on the child frameId.
Bounds: one build, one platform, flag rather than origin trial. The two origins differ only by port, so this is an origin boundary and not a site boundary — same-site cross-origin, no OOPIF involved (verified: the child gets no separate CDP target even with Target.setDiscoverTargets). A true cross-site test needs real hostnames and might behave differently. Happy to run that, or allow via a Permissions-Policy header rather than the attribute, if either is useful to the discussion.
Tool is the interface from Introduce the Tool interface #348. getTools() and executeTool() operate on the document currently bound to the target window, and toolchange fires when the tools that document exposes to the consumer change.
A ModelContextClient is a platform constructible object exposed as an attribute to WindowProxy. It is implicitly available for any window the caller has a reference to, i.e., within the same browsing context group. The set of tools returned by getTools() still depends on exposedTo. For example, a page exposing a tool to an agent in a cross-origin iframe, and the agent using it:
Here's a JS snippet showing how the API can be used:
awaitdocument.modelContext.registerTool({name: "search_flights",description: "Searches flights between two airports.",inputSchema: {type: "object",properties: {from: {type: "string"},to: {type: "string"}},},execute: async({ from, to })=>searchFlights(from,to),},{exposedTo: ["https://agent.example"]});constclient=window.parent.modelContextClient;client.ontoolchange=async()=>updateAgentTools(awaitclient.getTools());consttools=awaitclient.getTools();// only tools exposed to agent.exampleconstsearch=tools.find(t=>t.name==="search_flights");constresult=awaitclient.executeTool(search,{from: "SFO",to: "JFK"});
The same shape extends to tools registered by workers. A worker registers tools on its global, and the page consumes them through the worker handle it already holds:
// tools.js, running in a dedicated, shared, or service worker:self.modelContext.registerTool({name: "sync_inbox",/* ... */});// The page:constworkerClient=newWorker("tools.js").modelContextClient;constswClient=navigator.serviceWorker.controller.modelContextClient;
I think I agree with the bulk of the proposal here. It's worth noting that we are increasing the attack surface by enabling window.opener.modelContextClient. I also prefer keeping the interfaces on the same object, but spelling is a much larger discussion, particularly pending #236
It's worth noting that we are increasing the attack surface by enabling window.opener.modelContextClient
The tools exposed is still gated on the origin based allowlist in exposedOrigins, so I don't think its significant? The only difference I can think of is an author wanting a different allowlist based on whether the Document is the top-level origin vs embedded. Since that changes what cookie partition is used. But they can trivially do that by detecting this and setting the exposedOrigins accordingly.
I also prefer keeping the interfaces on the same object
Do you mean keeping modelContext and modelContextClient (naming bikeshed aside) on window?
The tools exposed is still gated on the origin based allowlist in `exposedOrigins`, so I don't think its significant? The only difference I can think of is an author wanting a different allowlist based on whether the Document is the top-level origin vs embedded. Since that changes what cookie partition is used. But they can trivially do that by detecting this and setting the `exposedOrigins` accordingly.
We are giving website authors the right tool in exposedOrigins. It is just a little different to expose invocation to embedders vs openers, that's all. Particularly for sites that already set frame-ancestors.
Do you mean keeping `modelContext` and `modelContextClient` (naming bikeshed aside) on `window`?
We are giving website authors the right tool in exposedOrigins. It is just a little different to expose invocation to embedders vs openers, that's all. Particularly for sites that already set frame-ancestors.
I called out this risk in #227 (comment) but, after some discussion, personally think it's manageable.
Activity
Remove spec issue now that #227 is filed
bvandersloot-mozilla commentedon Jul 22, 2026
I would find this surprising. Particularly if the modelContext is bound to the document.
Idan-Levin commentedon Jul 24, 2026
I think this relates to a higher-level question - what is the agent’s “discovery radius” (which WebMCP tools it can discover) and related to that, its “action radius” (which tools it can actually use).
As a north star for WebMCP, I think both should eventually extend as far across the internet as possible, subject to the security and performance challenges that need to be solved along the way.
Increasing these radiuses can help agents perform richer tasks for users, but each expansion also introduces tradeoffs. So whenever we move up an abstraction level - for example expanding
getTools()from the current document tree to the browsing context group - I think we should explain which new use cases this unlocks, which new risks it introduces, and why the tradeoff is worthwhile.For example, if a flight-booking site opens seat selection in a separate window, the agent could use the seat-selection tools there and then continue the booking in the original window.
bvandersloot-mozilla commentedon Jul 24, 2026
I see where there may be use cases, but I worry about the security properties. Keeping consequential tools away from places where there is more likely to be prompt injection seems like a good principle. Having tools shared between different pages seems like an easy way to have developers mess it up. Consider a travel booking site where a user has the Account Information page (with CRUD operations on credit card, passport, name, email) open separate from another window that renders user reviews of hotels or airlines. Page-locality of tools is a layer of the defense in depth that I might expect there. Of course this only affects in-page agents, where the name in-page itself also strongly suggests this locality! At the very minimum, this would have to follow partitioning identical to Storages, and I would strongly argue that it should be opt-in at the time of tool registration if it exist at all.
johannhof commentedon Jul 29, 2026
I also have a negative reaction to enabling this as a blanket capability without describing the use cases we're trying to enable here.
Assuming there's a good reason to do this, I'm worried that if we just tack it on to the existing getTools / registerTools exposure mechanism sites would not have sufficient granularity for deciding between iframe and opener exposure. This could lead to a lot of issues where developers were not aware that one type of exposure also allowed for the other (for example, sites could assume that a relatively liberal tool exposure policy combined with strict frame-ancestors is enough to prevent other sites from accessing their tools).
I don't think this is impossible, but it needs a careful security analysis.
domfarolino commentedon Jul 29, 2026
Yeah, to be clear the scope of
getTools()that I had in mind was browsing context group — it should only be possible to invoke tools in windows that you already have postMessage() access to, and could otherwise invoke less ergonomically by just messaging them and telling them to invoke stuff. The idea is thatgetTools()shouldn't have any farther reach than what your browsing context group has.I think this mitigates the risk of two, non-coordinating same-origin travel booking tabs accidentally invoking each others tools without realizing each other's state, while still enabling the use case where an in-page agent on that travel booking site can invoke tools in a "flight seat-picker" pop-up window, which is not possible today. Does this clarification about scoping make things less scary @johannhof @bvandersloot-mozilla?
We might want to think through the case where A.com/foo
window.open()s another A.com/foo window, which registers the same duplicate set of tools as its opener. PreservingTool#windowlets the agent harness distinguish which tool is from which window, so it doesn't confuse things, but maybe we can go further.johannhof commentedon Jul 30, 2026
I still doesn't seem to address the concern that sites may not understand that embedding prevention only will not help them avoid exposure of their tools.
johannhof commentedon Jul 31, 2026
Update: after further discussion with @domfarolino, I might be convinced that this risk is acceptable. XFA / CSP: F-A operate on origin granularity, like exposedTo, so it's less likely that an origin uses the former mechanisms as a more granular control for tool exposure.
It would still be good if the spec and developer documentation would call out that tool exposure happens across all documents in the BCG, i.e. iframes and popups, to avoid developer confusion which could lead to unintentionally exposing or consuming the wrong tools.
domfarolino commentedon Aug 3, 2026
Of course if we resolve this in the affirmative, we'd update the spec and documentation accordingly. I generally agree with what you said about origin granularity, and I think the main risk here is tool coherence; as we expand the tool registration surface area to more "contexts" that might not share application state, it becomes more likely an agent calls two tools consecutively, expecting them to build off each other, when they really poke at different parts of the system, not producing the result the agent expected.
This risk depends on the granularity of tools people write, and might demand a skill-like "tool orchestration layer" to weave together related tools that form a user journey. More experimentation is required to know how this will shake out.
Exposure, from an origin/security and tool coherence perspective are both important to keep in mind as we approach worker integration, which will likely pose questions about bending the boundary beyond browsing context groups.
beaufortfrancois commentedon Aug 6, 2026
FYI I'm exploring how in-page agents could discover and execute WebMCP tools inside separate windows — both standard popup windows (
window.open) and Document Picture-in-Picture windows (usingadoptNode(iframe)). See GoogleChromeLabs/webmcp-tools#363Svishwa2004 commentedon Sep 1, 2026
A measured baseline for the iframe half of this question, from shipping Chrome
Data rather than an opinion. I have been building a measurement harness for WebMCP tool-invocation rates, and since this thread's premise —
getTools()collecting from all documents under one traversable navigable — is easy to assume, it seemed worth pinning to a build with a reproduction.Build: Chrome
152.0.7977.65, Windows 11,chrome://flags/#enable-webmcp-testingenabled, fresh profile.Result: a same-origin subframe's tool does land in the top frame's manifest.
host.htmlregisters 3 tools; a same-origin<iframe src="widget.html">registers 1 (widget_ping).document.modelContext.getTools()→["host_alpha","host_beta","host_gamma","widget_ping"].WebMCPdomain'stoolsAddedevents report the same 4 tools, arriving across 2 distinctframeIds. Page view and browser view never disagreed.Reproduction, no harness needed — serve both files over http (not
file:) and readgetTools()in the top frame:Why this may bear on the granularity concern above. Provenance already exists at the browser layer but not in the page API. Each
toolsAddedevent carries aframeId(and a stack trace), so the browser knows which document registered each tool — butgetTools()returns no frame or origin attribution, so a host page cannot tell its own tools from an embed's. If the registration surface widens to more contexts, that asymmetry looks worth closing: frame/origin attribution on the page side is the information a site would need both for @johannhof's point about embedding controls not being sufficient, and for @domfarolino's tool-coherence risk — an agent calling two tools consecutively cannot currently be told that they came from different documents either.One adjacent measurement, since widening the surface implies larger manifests: on this build I found no observable tool-count ceiling up to 507 registered tools — all accepted, all returned by
getTools(), all surfaced in the browser view — with settle time rising only from 1.04 s to 1.30 s. Tool volume does not appear to self-limit as more contexts are folded in.Bounds, so none of this is over-read:
#enable-webmcp-testingrather than the origin trial.getTools()across a frame tree only — not openers orwindow.open, which @beaufortfrancois is exploring separately.Happy to re-run any of the above against another build or platform if that would be useful.
File the first contribution back to the spec, with the repro run befo…
Svishwa2004 commentedon Sep 1, 2026
Cross-origin follow-up:
toolsis a Permissions Policy feature, and with it delegated the page view and the agent view disagreeFollowing up my own comment above — the cross-origin case I said was untested is now measured, same build (Chrome
152.0.7977.65,#enable-webmcp-testing, fresh profile). Three cases, host page registering 3 tools and an embed registeringwidget_ping:getTools()getTools()WebMCP.toolsAddedhost_alpha, host_beta, host_gamma, widget_pingallowallow="tools"widget_pingonlyTwo things fall out of that.
1. There is already an origin-granular control, and it is not
allow-listed by default. In a cross-origin child without delegation,document.modelContextexists but every call throws:So a cross-origin embed cannot silently add tools to its host —
<iframe allow="tools">is required, which is opt-in at origin granularity and inherited from the framing document. That seems worth having explicit in this thread, since a few comments above reason about what sites can and cannot control.2. With delegation, the page-side and agent-side views of the manifest diverge. This is the part I did not expect. In the
allow="tools"row:getTools()returns only its own 3 tools;getTools()returns onlywidget_ping;toolsAddedstream reports all 4, arriving across 2frameIds.So cross-origin frames keep separate manifests as seen from script, while the browser aggregates both for whatever consumes tools. A host page therefore cannot enumerate what an agent can actually call on it — no script-visible surface returns the union. Same-origin frames behave the opposite way: one shared manifest, both frames see all 4.
For what it is worth, that divergence is the only one I have found: page view and browser view agreed exactly at 7, 71, 187, 307 and 507 tools, and across a same-origin iframe. This is the first case where they do not, and it is structural rather than a race — it reproduces on repeated runs.
Reproduction is the two files from my earlier comment, plus
allow="tools"on the iframe and the embed served from a second origin; the embed's own frame is reachable over CDP by matchingRuntime.executionContextCreatedon the childframeId.Bounds: one build, one platform, flag rather than origin trial. The two origins differ only by port, so this is an origin boundary and not a site boundary — same-site cross-origin, no OOPIF involved (verified: the child gets no separate CDP target even with
Target.setDiscoverTargets). A true cross-site test needs real hostnames and might behave differently. Happy to run that, orallowvia aPermissions-Policyheader rather than the attribute, if either is useful to the discussion.anssiko commentedon Sep 17, 2026
khushalsagar commentedon Oct 26, 2026
Here's a concrete proposal, following up on the last resolution.
ModelContextstays onDocument, since it manages the tool registry, which is 1:1 with a Document. It's reduced to the tool provider side of the API:The consumer side moves to a new interface (naming TBD):
Toolis the interface from Introduce theToolinterface #348.getTools()andexecuteTool()operate on the document currently bound to the target window, andtoolchangefires when the tools that document exposes to the consumer change.A
ModelContextClientis a platform constructible object exposed as an attribute to WindowProxy. It is implicitly available for anywindowthe caller has a reference to, i.e., within the same browsing context group. The set of tools returned bygetTools()still depends onexposedTo. For example, a page exposing a tool to an agent in a cross-origin iframe, and the agent using it:Here's a JS snippet showing how the API can be used:
The same shape extends to tools registered by workers. A worker registers tools on its global, and the page consumes them through the worker handle it already holds:
bvandersloot-mozilla commentedon Oct 30, 2026
I think I agree with the bulk of the proposal here. It's worth noting that we are increasing the attack surface by enabling
window.opener.modelContextClient. I also prefer keeping the interfaces on the same object, but spelling is a much larger discussion, particularly pending #236khushalsagar commentedon Oct 30, 2026
The tools exposed is still gated on the origin based allowlist in
exposedOrigins, so I don't think its significant? The only difference I can think of is an author wanting a different allowlist based on whether the Document is the top-level origin vs embedded. Since that changes what cookie partition is used. But they can trivially do that by detecting this and setting theexposedOriginsaccordingly.Do you mean keeping
modelContextandmodelContextClient(naming bikeshed aside) onwindow?bvandersloot-mozilla commentedon Oct 30, 2026
We are giving website authors the right tool in
exposedOrigins. It is just a little different to expose invocation to embedders vs openers, that's all. Particularly for sites that already setframe-ancestors.Yes.
johannhof commentedon Oct 30, 2026
I called out this risk in #227 (comment) but, after some discussion, personally think it's manageable.