Skip to content

Tool discovery should not be limited to a single traversable navigable #227

Open

Description

@domfarolino

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.

Activity

  1. self-assigned this
    on Jul 21, 2026
  2. added a commit that references this issue on Jul 21, 2026
  3. bvandersloot-mozilla commented on Jul 22, 2026

    @bvandersloot-mozilla
    Contributor

    I would find this surprising. Particularly if the modelContext is bound to the document.

  4. Idan-Levin commented on Jul 24, 2026

    @Idan-Levin

    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.

  5. bvandersloot-mozilla commented on Jul 24, 2026

    @bvandersloot-mozilla
    Contributor

    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.

  6. johannhof commented on Jul 29, 2026

    @johannhof
    Contributor

    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.

  7. domfarolino commented on Jul 29, 2026

    @domfarolino
    CollaboratorAuthor

    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.

  8. johannhof commented on Jul 30, 2026

    @johannhof
    Contributor

    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.

  9. johannhof commented on Jul 31, 2026

    @johannhof
    Contributor

    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.

  10. domfarolino commented on Aug 3, 2026

    @domfarolino
    CollaboratorAuthor

    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.

  11. beaufortfrancois commented on Aug 6, 2026

    @beaufortfrancois
    Collaborator

    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

  12. Svishwa2004 commented on Sep 1, 2026

    @Svishwa2004

    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.

    • host.html registers 3 tools; a same-origin <iframe src="widget.html"> registers 1 (widget_ping).
    • The top frame's document.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 -->
    <iframe src="widget.html"></iframe>
    <script type="module">
      for (const name of ['host_alpha', 'host_beta', 'host_gamma']) {
        await document.modelContext.registerTool({
          name,
          description: `Host-frame tool ${name}.`,
          inputSchema: { type: 'object', properties: {} },
          annotations: { readOnlyHint: true },
          async execute() { return { tool: name, registeredIn: 'top' }; },
        });
      }
    </script>
    
    <!-- widget.html -->
    <script type="module">
      await document.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 },
        async execute() { return { registeredIn: 'iframe', href: location.href }; },
      });
    </script>
    (await document.modelContext.getTools()).map(t => t.name)
    // → ["host_alpha", "host_beta", "host_gamma", "widget_ping"]

    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.

    Bounds, so none of this is over-read:

    Happy to re-run any of the above against another build or platform if that would be useful.

  13. Svishwa2004 commented on Sep 1, 2026

    @Svishwa2004

    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's getTools() returns only its own 3 tools;
    • the embed's getTools() returns only widget_ping;
    • the browser's toolsAdded 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.

  14. anssiko commented on Sep 17, 2026

    @anssiko
    Member

    RESOLUTION: Draft a concrete proposal for review. (issue #227)

  15. khushalsagar commented on Oct 26, 2026

    @khushalsagar
    Collaborator

    Here's a concrete proposal, following up on the last resolution.

    1. ModelContext stays on Document, since it manages the tool registry, which is 1:1 with a Document. It's reduced to the tool provider side of the API:

      [Exposed=Window, SecureContext]
      interface ModelContext : EventTarget {
        Promise<undefined> registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
      
        attribute EventHandler ontoolactivated;
        attribute EventHandler ontoolcancel;
      };
    2. The consumer side moves to a new interface (naming TBD):

      [Exposed=Window, SecureContext]
      interface ModelContextClient : EventTarget {
        Promise<sequence<Tool>> getTools(optional ModelContextGetToolOptions options = {});
        Promise<DOMString> executeTool(Tool tool, optional object inputObject, optional ModelContextExecuteToolOptions options = {});
      
        attribute EventHandler ontoolchange;
      };

      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.

    3. 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:

         partial interface Window {
         [SecureContext, SameObject] readonly attribute ModelContextClient modelContextClient;
         };

    Here's a JS snippet showing how the API can be used:

    await document.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"] });
    
    const client = window.parent.modelContextClient;
    client.ontoolchange = async () => updateAgentTools(await client.getTools());
    
    const tools = await client.getTools();  // only tools exposed to agent.example
    const search = tools.find(t => t.name === "search_flights");
    const result = await client.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:
    const workerClient = new Worker("tools.js").modelContextClient;
    const swClient = navigator.serviceWorker.controller.modelContextClient;
  16. bvandersloot-mozilla commented on Oct 30, 2026

    @bvandersloot-mozilla
    Contributor

    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

  17. khushalsagar commented on Oct 30, 2026

    @khushalsagar
    Collaborator

    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?

  18. bvandersloot-mozilla commented on Oct 30, 2026

    @bvandersloot-mozilla
    Contributor
         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`?
    

    Yes.

  19. johannhof commented on Oct 30, 2026

    @johannhof
    Contributor

    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.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions