Skip to content

Reposition feature as a generic communication tool? #236

Open

Description

@jakearchibald

I only read through the spec and explainers on a plane, so apologies if this is stuff that's already been discussed.

I wonder if it would be better to reposition this idea as a general way to call functions in other environments.

Right now, it's a pain in the arse to do simple function calls between environments (windows, workers, etc). So much so, that there are popular libraries to try and make it easier.

When I was looking at the API, I thought "haha I could totally abuse this to be like a built-in comlink", but then I thought "maybe it doesn't have to be an abuse".

Let's say the whole tool registering thing is just a way for one environment to expose function calls to another. Then, add in the ability to expose particular functions to particular other origins. There's your comlink.

Then, add in the ability to expose particular functions to "the browser" - this would make the function available via WebDriver, and whatever tooling the browser chooses to provide. This is where the schema and description comes in. This means the browser could expose this functionality via its own interface, and build a form for the parameters. Or, sure, an LLM could do all that transparently. I think it might be an easier sell like this - the LLM capability just falls out of the other capabilities provided.

Developers could simply use this as an easier way to call functions in a worker - nothing to do with LLMs. Or they could do it to expose site functionality to the outside world.

A service worker could register functions at install time, meaning these things can be callable without a window open. They may choose to open a window (or interact with another window). All they'd be doing is calling (probably private) tools on the window. With some feature that allows a tool to return "don't return until I've navigated and the next page is dom-ready" - now you have a single public tool in a service worker that can perform MPA automations.

Just don't call it Web Intents I guess.

Activity

  1. martinthomson commented on Aug 5, 2026

    @martinthomson

    If this is just RPC, that doesn't deliver the sort of value that MCP tool calling does, but something that is RPC plus the core features (input/output schema descriptions) does sound like a most useful thing to examine. I would go so far as to make tool descriptions/schema optional.

  2. jakearchibald commented on Aug 5, 2026

    @jakearchibald
    Author

    I think that's the same as I was thinking.

    Like:

    Internal tools:

    windowOrWorker.registerTool({
      name: 'foo',
      execute(repeatCount) {
        return 'bar'.repeat(repeatCount);
      },
    });

    This can be called by same origin environments:

    const result = await windowOrWorker.callTool('foo', [2]);
    console.log(result); // barbar

    Cross-origin tools:

    windowOrWorker.registerTool({
      name: 'foo',
      execute(repeatCount) {
        return 'bar'.repeat(repeatCount);
      },
      allow(origin) {
        // return true/false
      }
    });

    Externally exposed tools:

    E.g. to WebDriver & agents:

    windowOrWorker.registerTool({
      name: 'foo',
      execute(repeatCount) {
        return 'bar'.repeat(repeatCount);
      },
      exposeExternally: true,
      title: '…',
      description: '…',
      inputSchema: {},
      // etc etc
    });
  3. domfarolino commented on Aug 26, 2026

    @domfarolino
    Collaborator

    I'm generally supportive of this direction, thanks for taking the time to write it up. The biggest difference from this and the current proposal are:

    1. ❓ Your examples call registerTool() directly on the global, instead of through an intermediate object (modelContext in our case). Is this intentional? An intermediate like toolContext could be more generic, but I like the idea of some intermediate object we can keep expanding the surface of, should we want to provide different types of page context to models / the outside world / in-page agents. Do you have thoughts on this?
    2. ✅ Presumably you envision calling getTools() on window or worker proxy objects that you intend to get/execute tools from, instead of calling your own global's getTools() that just slurps up all™️ tools around you. This was raised in Various questions & feedback #237. I think this is a good idea. I think we agree on this, right?
    3. ✅ Your examples use callTool() instead of executeTool(). This is kinda raised in Rename execute to run #158, and I personally like call better than both run and `execute. SGTM.
    4. ❓ Your usage of callTool passes in a name, not an object. This actually matches the current spec, which keys tools by global + name, which we preserve if we move these APIs onto the proxy object you're trying to talk to, instead of your own global.
      • My plans were to go further than this, and create a proper Tool interface object representing a tool; it'd have all of the interesting properties you'd want to observe on it (schema, etc). You'd get one from getTools() and you'd pass it into callTool(). One benefit is that the tool could encapsulate the tool's UUID, thus mitigating races like https://webmachinelearning.github.io/webmcp/#unregistration-execution-race. Would love your thoughts on this idea.
    5. ✅ The current proposal's ToolAnnotations dictionary is very useful for AI agent harnesses to consume and act on. While not useful for generic RPC / function calling, I'm assuming you're fine keeping it around as optional, despite it being model-focused? (Prematurely marking with a green check since this one seems harmless, but lmk!)
    6. ❓ Exposure mechanism: The spec uses exposedTo to control which origins can see a tool, through getTools(). This is because tool registration/schema etc. leaks some information by default, so we wanted to prevent other origins from even seeing the tool if the author wouldn't let them call it. This is kind of in contrast with your example allow(origin) { // boolean. }, but I assume that's just a strawman example, not a concrete design recommendation. Are you OK with how exposeTo works now, and keeping that?
    7. ❓ Default exposure scope: There's a question of whether tools should be exposed "externally" by default, or by opt-in. I need to page in @johannhof to remember where we landed on this for subframes, but our vision was that since the API's purpose was to communicate tools to the native browser, tools would be exposed externally by default when exposedTo is absent. If not absent, you have to enumerate each origin you expose to, and some keyword like native-browser. Whether this is done a dict member or a keyword, I'm not concerned with. The question is: if I don't do anything extra, do I opt-in or opt-out to external exposure. My opinion is external exposure should be the default, since that's the benefit of the native API over userland libraries like comlink.

    @jakearchibald Please let me know what you think of the above.

  4. martinthomson commented on Aug 27, 2026

    @martinthomson

    For me, the idea of a getTool (using "tool" in naming seems prejudicial, consider finding an alternative) which you then have a .call() function on seems sensible. As is the idea of iterating on how exposure is controlled. Given comlink's existence, I'd say a default exposure seems right (the browser can always find these, but other frames and windows have the usual task of finding proxies, much like you would with postMessage, unless you were talking about having a browser-global lookup mechanism, which seems problematic: it's not necessarily a good idea to have all the sites that are active in my browser broadcast to all others; it's one thing to expose this on a proxy, because that's roughly analogous to building a by-default postMessage responder that provides the available "tool" calls).

  5. domfarolino commented on Aug 27, 2026

    @domfarolino
    Collaborator

    For me, the idea of a getTool (using "tool" in naming seems prejudicial, consider finding an alternative) which you then have a .call() function on seems sensible.

    Great! I'm honestly torn between:

    • Adding the new Tool object with call() method + removing executeTool()
    • Adding the new Tool object (no call() method) + keeping executecallTool(toolObj, inputArgs)
    • Just renaming executeTool() ➡️ callTool(), and keeping tools keyed by name and treating the race I pointed to above as a developer problem

    I kinda like the second option, but these are just details, we'll work them out.

    unless you were talking about having a browser-global lookup mechanism, which seems problematic: it's not necessarily a good idea to have all the sites that are active in my browser broadcast to all others

    Agreed. The browser-global lookup mechanism would only be available to the browser. A.com could only grab B.com's tools through a proxy it has to that origin; unrelated A.com and B.com tabs would not be able to just "look up" each others tools by origin name. There is a question about how we handle service worker tools though. I kind of like the idea of:

    // A.com
    
    const [serviceWorkerTool] = await navigator.serviceWorker.getTools();
    const w = window.open('b.com');
    w.postMessage(serviceWorkerTool);
    
    // B.com, sometime after `A.com` closes / dies
    serviceWorkerTool.call(inputArgs);

    This would be a novel capability outside of postMessage(): if the Tool is transferrable or serializable, we can let B.com "talk to" A.com's service worker directly and invoke a headless tool. We'll of course need to think about security considerations (hi @johannhof), but it excites me.

  6. jakearchibald commented on Aug 27, 2026

    @jakearchibald
    Author
    • ❓ Your examples call registerTool() directly on the global, instead of through an intermediate object (modelContext in our case). Is this intentional? An intermediate like toolContext could be more generic, but I like the idea of some intermediate object we can keep expanding the surface of, should we want to provide different types of page context to models / the outside world / in-page agents. Do you have thoughts on this?

    An intermediate seems fine. Sorry, I think I made my suggestion look more like a "final API design" than I intended. Everything is open for bikeshedding. I just wanted to communicate the idea that one environment could make something callable, and another environment could call it.

    The only thing to bear in mind is it needs to work cross-origin.

    • ✅ Presumably you envision calling getTools() on window or worker proxy objects that you intend to get/execute tools from, instead of calling your own global's getTools() that just slurps up all™️ tools around you. This was raised in Various questions & feedback #237. I think this is a good idea. I think we agree on this, right?

    Yeah, calling callTool on your own global would only give access to tools that your own global registered.

    I guess there could be a method for "slurps up all™️ tools around you", but I wonder if that could run into issues with multiple iframes pointing to the same URL, or multiple worker instances to the same script (for multithreading).

    • ✅ Your examples use callTool() instead of executeTool(). This is kinda raised in Rename execute to run #158, and I personally like call better than both run and `execute. SGTM.

    I didn't think too hard about the naming, but function.call already exists. Although, I guess you could use that as an argument either way.

    • ❓ Your usage of callTool passes in a name, not an object. This actually matches the current spec, which keys tools by global + name, which we preserve if we move these APIs onto the proxy object you're trying to talk to, instead of your own global.

    Eh I didn't think too hard about this.

    • My plans were to go further than this, and create a proper Tool interface object representing a tool; it'd have all of the interesting properties you'd want to observe on it (schema, etc). You'd get one from getTools() and you'd pass it into callTool(). One benefit is that the tool could encapsulate the tool's UUID, thus mitigating races like https://webmachinelearning.github.io/webmcp/#unregistration-execution-race. Would love your thoughts on this idea.

    Hmm, I'm not sure. With callTool(toolObj, inputArgs), would the following fail: callTool({ ...toolObj }, inputArgs) (or some other cloning method).

    • ✅ The current proposal's ToolAnnotations dictionary is very useful for AI agent harnesses to consume and act on. While not useful for generic RPC / function calling, I'm assuming you're fine keeping it around as optional, despite it being model-focused? (Prematurely marking with a green check since this one seems harmless, but lmk!)

    Yeah, I tried to capture this in "Externally exposed tools". You can imagine the same data being useful for a web extensions (or dev tools), which simply displayed the list of tools available.

    • ❓ Exposure mechanism: The spec uses exposedTo to control which origins can see a tool, through getTools(). This is because tool registration/schema etc. leaks some information by default, so we wanted to prevent other origins from even seeing the tool if the author wouldn't let them call it. This is kind of in contrast with your example allow(origin) { // boolean. }, but I assume that's just a strawman example, not a concrete design recommendation. Are you OK with how exposeTo works now, and keeping that?

    Is there a way to exposeTo everywhere? I'm not tied to a particular design here. Maybe URLPattern could be useful here?

    • ❓ Default exposure scope: There's a question of whether tools should be exposed "externally" by default, or by opt-in. I need to page in @johannhof to remember where we landed on this for subframes, but our vision was that since the API's purpose was to communicate tools to the native browser, tools would be exposed externally by default when exposedTo is absent. If not absent, you have to enumerate each origin you expose to, and some keyword like native-browser. Whether this is done a dict member or a keyword, I'm not concerned with. The question is: if I don't do anything extra, do I opt-in or opt-out to external exposure. My opinion is external exposure should be the default, since that's the benefit of the native API over userland libraries like comlink.

    I guess I assumed tools would be same-origin only by default. And then there's an extra step to expose them cross-origin. And another step to expose them outside the web platform.

    I think this way make sense, as exposing outside the web requires much more data to be provided - ToolAnnotations. Same-origin is a default we use elsewhere, as is opting in to greater availability.

    I'm open to other patterns though.

  7. domfarolino commented on Aug 27, 2026

    @domfarolino
    Collaborator

    Hmm, I'm not sure. With callTool(toolObj, inputArgs), would the following fail: callTool({ ...toolObj }, inputArgs) (or some other cloning method).

    As I envision, yes. The interface object is a handle to the tool's unique identifier, used to target the tool. This internally encapsulates the tool's name (which could now be a unique token without having to be exposed to script in an ugly way, thus preventing these race conditions) and target origin (which would otherwise be necessary to pass in yourself).

    It also opens the door for us to pass tools across postMessage(), so you can run them in environments you don't have proxy access to. For example, passing your document's tool to your shared worker, which is super useful. It's impossible otherwise, since shared workers don't have a WindowProxy to any of their owners.

    Is there a way to exposeTo everywhere?

    I'd like to see us support exposedTo: ['*']. It means "anyone holding a proxy to me can run my tools". Or if we allow Tool objects to be transferred over postMessage(), it means "anyone with a handle to my tools can run my tools".

    I guess I assumed tools would be same-origin only by default. And then there's an extra step to expose them cross-origin. And another step to expose them outside the web platform.

    I think this way make sense, as exposing outside the web requires much more data to be provided - ToolAnnotations. Same-origin is a default we use elsewhere, as is opting in to greater availability.

    For web-platform exposure, yes same-origin by default, and cross-origin opt-in by providing the origin in exposedTo. I see exposure to the browser itself something separate altogether, not "a higher bar" than cross-origin exposure. In part because ToolAnnotations are not actually require for native agent exposure.

  8. jakearchibald commented on Aug 27, 2026

    @jakearchibald
    Author

    It also opens the door for us to pass tools across postMessage(), so you can run them in environments you don't have proxy access to.

    Ohh that's nice. In that case, tool.call(…) seems better. Otherwise it's a bit weird that registerTool and callTool sit side by side, but one concerns this environment and the other is any tool. But eh maybe it doesn't matter.

    since shared workers don't have a WindowProxy to any of their owners.

    We could put the clients API there. But yeah, passing stuff around with postmessage is nice too.

    I see exposure to the browser itself something separate altogether, not "a higher bar" than cross-origin exposure.

    Fair. Although I'd want a way to prevent tools being external (because they're just helper tools for a larger operation). And then the API shape is a bit weird, maybe? Opting in for some things, opting out for others.

  9. johannhof commented on Sep 22, 2026

    @johannhof
    Contributor

    Sorry, I completely missed that I was paged into this convo. Trying to parse the security questions out of this conversation...

    Default exposure scope

    • Cross-origin exposure (i.e., direct document-to-document tool calls) must be opt in on the side of the tool providing origin. In the previous model where the parent origin would get all tools as a single monolithic API call, the parent would also have to opt into which origins to receive tools from, since malicious origins could otherwise override / inject tools. If you get tools directly from WindowProxy that seems less necessary.
    • Browser / Agent exposure must also be opt in from both origins in a cross-origin setting, but can be default for same-origin. The reason for this is that 1) the model will have a hard time correctly distinguishing malicious 3P tools, 2) a webmcp tool could trivially instruct the agent to call a 3P tool as part of its task, which would have a similar effect as direct access to the tool via WindowProxy

    This would be a novel capability outside of postMessage(): if the Tool is transferrable or serializable, we can let B.com "talk to" A.com's service worker directly and invoke a headless tool. We'll of course need to think about security considerations (hi @johannhof), but it excites me.

    I'm not a Service Worker expert so pardon the possibly dumb question, but aren't Service Workers quickly stopped and thus would not be listening to this kind of call? Would you imagine this to wake up the SW? Note that 3P SWs are partitioned like all other storage on the site.

  10. domfarolino commented on Sep 24, 2026

    @domfarolino
    Collaborator

    I'm not a Service Worker expert so pardon the possibly dumb question, but aren't Service Workers quickly stopped and thus would not be listening to this kind of call? Would you imagine this to wake up the SW?

    That's kind of what I was assuming, yeah.

    Note that 3P SWs are partitioned like all other storage on the site.

    Sure, but what's proposed in this thread would still be a new capability.

  11. MiguelsPizza commented on Oct 25, 2026

    @MiguelsPizza
    Contributor

    +1. We'd get a bunch of stuff for free if we did this (assuming WebMCP would be a superset of the general purpose web RPC): #41, #82, #81, #196, #151.

    Some prior art worth looking at is Cap'n Web by @kentonv. They pass around proxy objects and support pluggable transports (ws, message ports). It's close to @domfarolino's Tool handle. Worth reading the announcement

    Let's say the whole tool registering thing is just a way for one environment to expose function calls to another. Then, add in the ability to expose particular functions to particular other origins.

    Why stop at service workers? Correct me if I'm off here @jakearchibald, but the proposal is a web RPC protocol + some semantics over which origins are allowed to call these proxies? Since the browser is basically the trust broker here enforcing the origin exposure policy, you could technically register these tools server side too, and the browser calls them like any other tool.

    As an example, say you have two websites (both hosted on CF workers, which are kinda like a service worker you run server side). They expose tools like this:

    // shop.com
    export default {
      async fetch(request, env, ctx) {
        const user = await getUser(request, env); // from this origin's session cookie
    
        ctx.registerTool({
          name: 'checkout',
          exposedTo: ['native-browser'], // enforced by the browser
          execute: async ({ cartId, pay }) => {
            const cart = await getCart(env, user, cartId);
            // pay is a function from the caller; calling it goes back to the browser
            const receipt = await pay({ amount: cart.total, currency: 'USD', merchant: 'shop.example' });
            return placeOrder(env, cart, receipt); // receipt is signed by the payment provider
          },
        });
      },
    };
    // pay.com
    export default {
      async fetch(request, env, ctx) {
        const user = await getUser(request, env);
    
        ctx.registerTool({
          name: 'charge',
          exposedTo: ['native-browser'],
          execute: ({ amount, currency, merchant }) =>
            chargeSavedCard(env, user, { amount, currency, merchant }),
        });
      },
    };

    Then the browser agent writes code like this at runtime (think a browser agent with a sandbox js env):

    // connecting via the transport api client side (or something like this)
    const shop = await navigator.connectTools('wss://shop.com/tools');
    const wallet = await navigator.connectTools('https://pay.com/tools'); 
    const [checkout] = await shop.getTools();
    const [charge] = await wallet.getTools();
    
    const order = await checkout.call({
      cartId,
      // runs in the browser, so the shop never talks to pay.example or sees the card
      pay: ({ amount, currency, merchant }) => {
        if (amount > 200) throw new Error('over budget');
        return charge.call({ amount, currency, merchant });
      },
    });

    This is sort of like remote MCP but isomorphic, isn't limited to things that are JSON serializable, and delegates auth and origin enforcement to the browser.

  12. bvandersloot-mozilla commented on Oct 30, 2026

    @bvandersloot-mozilla
    Contributor

    Something I thought of after the F2F: we probably want a different meaning for the permission policy if we are looking to allow tools as generic communication. The policy currently is "allowed to register a tool?" where I think it makes more sense to have it be "allowed to be rolled up into in my tool list". Or maybe this is an argument to the getTools call.

  13. johannhof commented on Oct 30, 2026

    @johannhof
    Contributor

    Yeah, we did discuss this a bit, but I'm not sure we really concluded with something actionable. Technically we have fromOrigin, but that does not provide sufficient security here - if we have a site that uses generic tools from embed.example but does not want to give embed.example the ability to expose agentic tools to a native agent, then not including that site in fromOrigins is not enough.

    The top level embedder either needs a separate permissions policy for agentic tools, or another way to allow-list origins for "native" agents.

  14. jakearchibald commented on Oct 30, 2026

    @jakearchibald
    Author

    Yeah, we did discuss this a bit, but I'm not sure we really concluded with something actionable.

    It's just a naming change that's needed to make it clear it's about allowing externally visible tools. Seems fine.

  15. johannhof commented on Oct 30, 2026

    @johannhof
    Contributor

    Sorry, I was responding to @bvandersloot-mozilla's #236 (comment) - I think we need to add a second permissions policy

  16. jakearchibald commented on Nov 2, 2026

    @jakearchibald
    Author

    I think I understood the context. I'm not sure we need a second permissions policy - we don't have a permissions policy for adding a message event. We just need a single policy for the external tools to be exposed at the top level.

  17. anssiko commented on Nov 3, 2026

    @anssiko
    Member

    I'm happy to see active post-F2F discussion on this issue, here's what we resolved last Tuesday:

    RESOLUTION: The group agrees to explore the direction to make the API a more generic communication tool with focus on adequate security bar, tool registration defaults, infrastructure primitives. Next step to deliver API shape sketches for group review. Show that the combined model does not reduce the API's usefulness for agents, or create a DX issue. (issues #236 #237)

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

Metadata

Metadata

Assignees

No one assigned

    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