Skip to content

navigator.modelContext.provideContext allows overwriting of previously registered tools in the same environment #101

Description

@beaufortfrancois

In environments like online stores where first-party code and third-party partner JavaScript run within the same environment, a security concern exists with how WebMCP tools are registered.

Currently, if a site registers an "official" tool, a malicious or accidental third-party script can overwrite it. This could allow the third party to proxy tool calls, effectively observing the entire agent-user interaction, which may include private data.

And here's how it happens: While the navigator.modelContext.registerTool() method throws an error if a tool with the same name already exists, this security mechanism is bypassed with navigator.modelContext.provideContext() that first clears the existing tools before registering new ones.

Note

A similar issue also exists by registering a tool whose name varies slightly with navigator.modelContext.registerTool() or a new <form toolname="...">.

Thoughts for discussion

We need a mechanism to prevent unintentional or malicious clobbering of tools when potentially untrusted scripts are operating in the same environment.

  1. Should the entire navigator.modelContext.provideContext({tools: ...}) call fail if it attempts to register a tool name that is already registered, regardless of the method used for the initial registration?
  2. Shall we add an optional strict parameter for navigator.modelContext.provideContext() that when set would cause the call to fail if any tool name in the new context is attempting to hijack an already registered tool name?
  3. Shall we expose a new listTools() method and toolchange event on navigator.modelContext that would allow the controlling script to check for already registered tools before attempting to update the context:

Activity

  1. johannhof commented on Feb 20, 2026

    @johannhof
    Contributor

    So we should be clear on the threat model here: Are you trying to prevent unintentional or malicious overwriting of tools? There's a big difference between those two things, with the latter being much harder to prevent.

    Right now, it is hard to stop a motivated third party script from messing with the web page it is embedded on, that is just not really supported by the threat model of the web. There are some mechanisms that employ a "trust-on-first-use" model, such as HTTPOnly cookies. We could do the same thing here, but note that this also only goes so far: A malicious script could easily do things such as overwriting DOM elements or global functions to achieve the same effect as overwriting a tool definition, but admittedly that at least protects against permanently storing these tools.

    So, to repeat, the threat model should be really clear: We might be able to achieve a TOFU protection against permanently overriding scripts ala HTTPOnly cookies, but there is no current way to instill complete trust in third party scripts and any web developer who embeds untrusted third party scripts should be clear on the implications. This is also shown by your concern that another script could register a very similar tool (maybe even with an annotation that this is the "correct" tool to use). Web developers should realistically not embed scripts that they don't trust to not do this kind of stuff.

    I agree that we should make registerTool and provideContext be coherent in their threat model regarding when to allow overwrites vs. not.

  2. MiguelsPizza commented on Feb 20, 2026

    @MiguelsPizza
    Contributor

    Beyond the security issue, this is also a major footgun for non-malicious 3p scripts.

    1P script registers a tool:

    navigator.modelContext.registerTool({ name: 'submit_patient_info', ...  });

    Another script runs later and wipes it:

    navigator.modelContext.provideContext({ tools: [] });
    // or
    navigator.modelContext.clearContext();

    This becomes even more of an issue if we let iframes write and read to the hosts too list

    I'm not sure what the upside of providing access to clear and set the global tool registry is? Maybe someone can speak to this?

    What if we remove the provideContext() API and make it so registerTool() just returns its own teardown handle? This fits better with the component model most sites are built with today and aligns more closely with the declarative API, where tools are already scoped to their element's lifetime in the DOM.

    useEffect(() => {
      const unregister = navigator.modelContext.registerTool({ name: 'submit_patient_info', ... })
      return () => unregister()
    }, [])
  3. mfreed7 commented on Feb 20, 2026

    @mfreed7

    What if we remove the provideContext() API and make it so registerTool() just returns its own teardown handle? This fits better with the component model most sites are built with today and aligns more closely with the declarative API, where tools are already scoped to their element's lifetime in the DOM.

    +1 to this approach. It aligns much better with the rest of the web platform, which doesn't usually provide "delete everything that came before" type behavior. There are exceptions, such as window.onload but those are considered to be old/bad patterns, replaced with addEventListener('load') and removeEventListener(), which allow independent scripts to play nicely with each other. It seems like registerTool and unregisterTool are the way here, and I'm not sure provideContext is a good idea.

  4. bvandersloot-mozilla commented on Feb 20, 2026

    @bvandersloot-mozilla
    Contributor

    Do we even need it to return its own teardown handle? Or would

    useEffect(() => {
      const unregister = navigator.modelContext.registerTool({ name: 'submit_patient_info', ... })
      return () => navigator.modelContext.unregisterTool('submit_patient_info');
    }, [])
    

    be sufficient?

    I guess it is nice to be able to prevent tool-name-aliasing from causing you to remove the wrong tool, but at that point I think you are having a bad time with bugs anyway.

  5. MiguelsPizza commented on Feb 20, 2026

    @MiguelsPizza
    Contributor
    navigator.modelContext.unregisterTool(any_possible_toolname);
    

    Still lets any script unregister and replace any tool. What are the upsides of this API vs scoping the tool removal handle to its declaration?

  6. mfreed7 commented on Feb 20, 2026

    @mfreed7
    navigator.modelContext.unregisterTool(any_possible_toolname);
    

    Still lets any script unregister and replace any tool. What are the upsides of this API vs scoping the tool removal handle to its declaration?

    I agree for sure - it shouldn't be possible to unregister a tool that you didn't register. That could be shaped in a few different ways, but it certainly can't be unregisterTool(tool_name).

  7. bvandersloot-mozilla commented on Feb 20, 2026

    @bvandersloot-mozilla
    Contributor

    What are the upsides of this API vs scoping the tool removal handle to its declaration?

    You'd have to move the unregister callback around to be acessed by the cleanup code, rather than being able to refer to it in a per-page namespace (not a good argument, I know).

    But returning the unregister does liberate us on the issue of name-aliasing- tools wouldn't have to be uniquely named. That does end up looking a lot more like @mfreed7's example of registering callbacks, and looks cleaner where iframes are registering tools as well

  8. beaufortfrancois commented on Feb 23, 2026

    @beaufortfrancois
    CollaboratorAuthor

    I'd like to point out that removing provideContext()/clearContext() doesn't entirely mitigate the risks associated with tool lifecycle management. Tools defined via the declarative API are still susceptible to be wiped out by malicious or conflicting scripts. For instance, any script on the page could still effectively 'unregister' all declarative tools by simply stripping the attributes, like so:

    document.querySelectorAll('form[toolname]').forEach(f => f.removeAttribute('toolname'));

    We're still left with the same vulnerability where the page state can be easily cleared by third-party JS.

  9. mfreed7 commented on Feb 23, 2026

    @mfreed7

    I'd like to point out that removing provideContext()/clearContext() doesn't entirely mitigate the risks associated with tool lifecycle management. Tools defined via the declarative API are still susceptible to be wiped out by malicious or conflicting scripts.

    I don't think this is really about creating some kind of security boundary or anything. As with removeEventListener(), malicious (or badly written) script can always monkeypatch addEventListener and step on other listeners. The point is just to avoid the easy footguns, where two independent scripts both call provideContext(), thinking they're the "only" ones on the site, and things break.

  10. DavidMulder0 commented on Feb 26, 2026

    @DavidMulder0
    Contributor

    Once you have a malicious actor executing code on your page your page is compromised. There is no "vulnerability" that comes after "you are fully compromised".

    The only relevant question is: Can we make changes that decrease the likelihood of bugs causing unintended consequences? The proposed idea to remove

    navigator.modelContext.provideContext({ tools: [] }); // this would always add new tools
    navigator.modelContext.clearContext();
    

    and replace it with either a name based tool deregistration, or a clearContext return function (const clearContext = navigator.modelContext.provideContext({ tools: [ { name: "abc", ...} ] });) sound somewhat sensible to me... but this opens up the new bug risk of a script re-registering the same set of tools over and over... which sounds about as likely as a script accidentally removing tools that shouldn't have been removed.

    Frankly speaking, if you have two different 'things' on a page both registering their own set of tools without knowing about each other the chance the LLM is not going to get confused and make a mess seems quite small anyway. Feels a bit like this is a solution in search of a problem tbh.

  11. mfreed7 commented on Feb 26, 2026

    @mfreed7

    Frankly speaking, if you have two different 'things' on a page both registering their own set of tools without knowing about each other the chance the LLM is not going to get confused and make a mess seems quite small anyway. Feels a bit like this is a solution in search of a problem tbh.

    This problem occurs quite regularly on the web, though. Some sites use 3rd party scripts or components, some companies are organized into teams that "own" only part of the page, etc. This is why other web APIs are constructed in the way that they are, to let these distinct teams play nicely with each other.

    I could see real use cases, where the outcome would be fine from the LLM's perspective. One team owns the "search for products" portion of the page (with the search bar, etc.) and they publish a search_products() tool. Another team owns the product detail page (with product specs, images, etc.), and they publish a get_product_details() tool. An LLM could fairly easily see what to do in that scenario, I think. The only problem happens if they both register those tools with modelContext.provideContext({ tools: [individual tool]}).

  12. anssiko commented on Feb 27, 2026

    @anssiko
    Member

    @simoneonofri this is on your turf.

  13. bwalderman commented on Mar 2, 2026

    @bwalderman
    Collaborator

    For some background, the provideContext method was my suggestion in Microsoft's original explainer before this group got together and converged on what is now WebMCP. There were a couple considerations that went into it. First, the name provideContext instead of provideTools was to leave open the possibility for providing other MCP-like artifacts (i.e. resources, prompts, etc.) in the future. Second, the thinking was that web developers might want to organize groups of tools for each state their site can be in and switch the currently available group as the user or agent navigates through the site. provideContext was meant to be a quick way to do this.

    I've seen several comments here and in other threads that this is not what web developers are looking for, and in fact registerTool/unregisterTool is what developers would expect in order to achieve the behavior I described above in frameworks like React. I'd be fine with dropping provideContext/clearContext from the spec entirely.

  14. mfreed7 commented on Mar 3, 2026

    @mfreed7

    First, the name provideContext instead of provideTools was to leave open the possibility for providing other MCP-like artifacts (i.e. resources, prompts, etc.) in the future.

    Thanks for this bit - I kind of suspected that, given the argument was a general options bag. I guess the assumption is that since those other use cases (and parameters) never materialized, it's ok to drop them? Or perhaps if they materialize in the future, another API (modelContext.provideResources(), modelContext.providePrompts(), etc.) could be added?

  15. atesgoral commented on Mar 3, 2026

    @atesgoral

    Has an idea like this been floated/dismissed before? Instead of declarative/imperative tool registration, letting scripts register and unregister browser-local MCP Servers (new browser interface, conceptually using a "null transport"), while the browser simply queries them for context and remove their context contributions when the MCP Server is unregistered. And from the agent's perspective the navigator.modelContext is acting like an MCP Server gateway.

    I can take this to a new issue/proposal.

  16. domfarolino commented on Mar 3, 2026

    @domfarolino
    Collaborator

    I'd be fine with dropping provideContext/clearContext from the spec entirely.

    Following this issue so far, I think this is the right call. I'll upload a PR to remove these APIs, and Chromium will work to remove them from our implementation as well.

    Or perhaps if they materialize in the future, another API (modelContext.provideResources(), modelContext.providePrompts(), etc.) could be added?

    Yep, I think new APIs for new context is the best path forward as the need arises. And if we make some ground there, we might decide to reconsider and not have separate APIs for these things, and converge to a safer version of what provideContext() set out to be in the first place. Until then, I think we're good to proceed removing this API.


    We still need to solve the "you shouldn't be able to unregister a tool you didn't register"1 problem (#101 (comment)). We should file a new issue for that specifically, unless someone wants to point out an existing one that covers this?

    Footnotes

    1. You can still do this with monkeypatching gymnastics and overriding web platform APIs to spy on other people's registrations, just like you can do with addEventListener() and others. Stopping that is not in scope, because any 3p script that's doing this in the 1p with bad intention is malicious, which is a problem outside of our scope. ↩

  17. victorhuangwq commented on Mar 5, 2026

    @victorhuangwq
    Contributor

    Created #129 to continue "you shouldn't be able to unregister a tool you didn't register" discussion

  18. anssiko commented on Mar 5, 2026

    @anssiko
    Member

    RESOLUTION: Drop provideContext/clearContext from the spec as in PR #132. (issue #101)

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