Skip to content

Tool collections: coarse-grained grouping with progressive disclosure #255

Open

Description

@sdras

In a large JS application, the number of registered tools grows fast. Tools accumulate across routes, across features, and across client/server boundaries. A moderately complex app can easily surface 100+ tools (see also #88, where an MCP bridge exposes 50+ from a single server). Today getTools() hands the agent one flat list, which means:

  • The agent must read every tool description to find the 3 it needs, spending tokens on tools that are irrelevant to the current task.
  • As the tool list grows, the accuracy of the model selection will decrease. This was a well-documented failure mode for large flat tool lists in backend MCP, and it's why progressive disclosure patterns (tool search, toolsets) have emerged in backend MCP.
  • There's no unit between "one tool" and "every tool on the page."

Applications already have natural coarse-grained groupings: an auth flow (sign-in, sign-out, verify-2fa, recover-account), a cart flow (add-to-cart, remove-from-cart, apply-promo, begin-checkout), a video experience (play, seek, set-captions, set-speed). What's missing is a way to expose that structure so an agent can read a handful of group descriptions first, pick the relevant one, and only then drill into those tools.

There's a second problem this addresses. When a tool execution spans a client/server interaction, or across tabs, the tool call triggers a navigation or a server roundtrip, and tools re-register on the way back. We've sometimes observed agents failing to re-find the relevant tools after the roundtrip. A collection whose identity persists (from cart count/data to server to success/error) improves continuity.

Strawman

Keep registerTool() as-is and add a collection primitive alongside it:

document.modelContext.registerCollection({
  name: "cart-checkout",
  description: "Create cart, process order, report to user",
  tools: ["cart-inventory", "confirm-payment", "process-cart-order", "get-order-status", "cancel-order", "cart-success", "cart-error"],
});

Benefits:

  • Backwards compatible, and keeps the existing API simple. Agents that don't understand collections call getTools() and get the flat list they get today. Tools registered without a collection remain top-level. Collections are additive structures that preserve the current API.
  • Deterministic. A collection is a defined set, defined by the application owner. It doesn’t require natural-language interpretation. Devs can define this according to how they would like their application to be used.
  • Cheap for the agent. The initial payload is smaller, the agent doesn’t have to process non-related tools for task completion.
  • Can help with cross-document, and cross-tab actions. We need tools to outlive a single document view: Tool discovery should not be limited to a single traversable navigable #227 extends discovery across the browsing context group, and Cross-document tool response mechanism #135 discusses cross-document tool responses. A collection could provide cohesion beyond a navigation or server roundtrip. The agent could have some continuity of defined tools instead of re-reading an entire flat list.

Frameworks

Filing this partly as a sub-topic of the frameworks meta issue (#199, section 2: scoped, composable registries). This could fit well into Framework mental-models.

Alternatives considered: Skills (#161)

#161 proposes skills that bundle tools with workflow instructions and structured context, and grouping falls out of that as a side effect (tools: ["searchProducts", "addToCart", ...]). Skills are solving a real problem, but I don't think they're the right home for grouping, for two reasons:

  1. Creates two API surface areas to think about: If grouping only exists via skills, a developer who just wants to expose their cart tools has to reach for a primitive whose main job is behavioral guidance, and every framework integration has to reason about both surfaces. Grouping is structural and could live with tool registration itself, where you’re already defining it.
  2. Determinism: As I alluded to above, a skill's instructions field leaves room for LLM-based agents to interpret, which clients may weigh differently or ignore completely. Developers need a consistent, enumerable way to expose groupings that behave the same in every client.

Activity

  1. MiguelsPizza commented on Aug 23, 2026

    @MiguelsPizza
    Contributor

    @sdras The MCP community has been going back and forth on tool grouping for a bit now, with the community and core maintainers pretty split. I'd be curious to hear your thoughts on some of the arguments laid out there: modelcontextprotocol/modelcontextprotocol#3264

    Personally, I found that skills + an agent that supports "codemode" style tool discovery and execution solve most of the issues tool grouping is trying to address. I like to think about each of the options laid out from the model's perspective:

    1) register collection

    Not entirely sure what you were thinking the client side would look like for enumerating and unpacking a collection, but let's assume it works similarly to a skill and that a client that called getCollections() would return it to the model context like this:

    cart-checkout — Create cart, process order, report to user (5 tools, 500 chars)
    find-product — search for product, get product details (2 tools, 200 chars)
    

    Now when the model calls getCollection() it returns the jsonschema tools list for said collection.

    I think this solution works well. The thing I'd push on is that the page now owns the disclosure units — I'd rather it just label tools and let the client decide what to put in context.

    2) client-side codemode + skills

    User asks the agent to find the pantene conditioner and add it to her cart. The agent currently has skills for the site, no tools yet:

    cart-checkout — Create cart, process order, report to user
    find-product — search for product, get product details
    …a bunch more unrelated site-specific skills
    

    It reads those two skills, then uses its code sandbox to list the tools for the page (I'll use Rook's API for example):

    const tools = await ctx.chrome.modelContext.listTools({ tabId });
    // cheap inventory: name, title, description, annotations
    
    const detailed = await ctx.chrome.modelContext.getTools({ tabId });
    // adds inputSchema / outputSchema when it needs to call
    
    await ctx.chrome.modelContext.executeTool({
      tabId,
      name: "searchProducts",
      arguments: { query: "pantene conditioner" },
    });

    grouping is structural and shouldn't only live in skills, +1. I also found that putting exact tool names in the skill is a hard maintenance problem, and it can really damage the reliability of the model if they ever get out of sync. Collections have the same trap though

    Maybe we do something closer to SEP-1300's Group primitive: an optional groups key on registerTool?

    document.modelContext.registerTool({
      name: "addToCart",
      description: "...",
      groups: ["cart-checkout"],
      // inputSchema, execute, ...
    });

    That way the skill can also list that group without enumerating exact tool names, and the model can then filter the live list down to the tools.

    const tools = await ctx.chrome.modelContext.listTools({ tabId });
    const cart = tools.filter((t) => t.groups?.includes("cart-checkout"));

    This also works nicely for agents that don't use codemode and have limited filtering options.

  2. Idan-Levin commented on Aug 23, 2026

    @Idan-Levin

    @MiguelsPizza I think the core tradeoff is that code mode is probably more effective and easier to maintain, but WebMCP cannot assume that every client will support it. For clients that do not, the burden moves to the client to build the filtering mechanism internally, which creates another coordination dimension and makes adoption riskier.

    What you are proposing works well for agents with code mode. But many consumer agents today do not expose this type of programmable harness, and code mode is still concentrated mainly in coding and work products. It can also add latency and complexity that may not fit the consumer experience.

    Non-code clients can still filter tools internally, but then every client needs to independently build the progressive-disclosure behavior: organize the groups, keep the full tool inventory outside the model’s context, and dynamically inject only the relevant tools.

    So adding a groups label to each tool can be part of the solution, but it does not define the progressive-disclosure mechanism itself.

  3. MiguelsPizza commented on Aug 25, 2026

    @MiguelsPizza
    Contributor

    @Idan-Levin there are definitely tradeoffs here, but that's kind of the point. I'm arguing we should not define the progressive disclosure mechanism server-side, as it risks providing a solution today for an issue that might not exist tomorrow. For example, the MCP ecosystem spent a year trying to solve tool bloat server-side since clients were not innovating. Codemode itself started as a middleware, but now that modern clients support code execution for tools directly, it's redundant.

    Regardless of where the progressive disclosure abstraction lives, clients will still need to implement it. Generally speaking it is much easier to provide updated guidance for how client agents should be built than it is to roll back something that has been enshrined in the spec. Harness best practices are evolving quickly, but things like tools and skills appear to be the primitives we have collectively agreed to build on.

  4. Idan-Levin commented on Aug 25, 2026

    @Idan-Levin

    , clients will still need to implement it.

    yeah fair point. So just trying to wrap my head around this - you're saying basically: add optional group meta tags per tool, clients with code mode can filter by it immediately, clients without code mode can implement it ad-hoc, and even if code-mode isn't the actual equilibrium for harnesses eventually, anyways even in the original @sdras proposal the client needs to tweak something to support groups, so it might as well be filtering and not "agent-level" groups api support?

  5. domfarolino commented on Aug 31, 2026

    @domfarolino
    Collaborator

    Hmm, @MiguelsPizza it sounds like you favor group metadata on each tools over explicit groups for two reasons:

    1. Maintenance (you might update a tool name, but forget to update a group/collection referencing it)
    2. Agent clients will have to do work to support any form of progressive discovery mechanism we provide

    Regarding (1) above:

    I also found that putting exact tool names in the skill is a hard maintenance problem, and it can really damage the reliability of the model if they ever get out of sync. Collections have the same trap though

    Won't registration-time group tags have the same trap? You can imagine one team registering tools with groupName: "login", and through some clumsly re-org, another team's tools now being exposed as groupName: "authentication", with the intent being the same.

    One thing I actually like about explicit groups is that I can imagine if we introduce an explicit Tool interface object to represent each tool, you might be able to do something like:

    document.modelContext.registerToolGroup("account-management",
        { tools: [loginToolObj, passwordResetToolObject, …] }
    );

    I feel like this might help solve the synchronization problem using web platform primitives, although I suppose the same is possible on each tool:

    const authenticationGroupHandle = /* maybe create a new object, or something … */;
    document.modelContext.registerTool({
      name: "login",
      group: authenticationGroupToken,
    })

    I'm not sure, I'm just thinking of ways to solve the desync problem that using plain strings introduce, by forcing you to pass in a real object that you have to obtain somewhere, that will throw if you re-architect your code and fail to update group registrations accordingly.

    That aside, one thing I think we need to consider with any proposal is recursive discovery / sub-groups. As modelcontextprotocol/modelcontextprotocol#3264 points out:

    Discovery can be recursive. The mechanism exists independently of the specific thing being discovered. David said he is more interested in a general, recursive discovery mechanism than a flat grouping mechanism. In his view, if grouping is useful, it likely wants to generalize into a tree or directory-like mechanism rather than remain a one-level list.⁠

    I generally agree with this sentiment. If you have 5 different cross-origin iframes on the same site, each with different "groups" of tools, but with each group (or site) possibly comprising a logical "sub-group", we might want whatever primitive we land on to let authors express this. It might not be possible to influence "groups" that span origin boundaries, but we might want groups to be able to be sub-groups of others, and I worry that a single group string on a registered tool definition closes us off from doing that in the future, restricting us to a single flat level of grouping. @MiguelsPizza what do you think?

  6. MiguelsPizza commented on Sep 1, 2026

    @MiguelsPizza
    Contributor

    @Idan-Levin bassically, TLDR the argument is more "we should give the agent/harness more hints to find the tools it needs rather than prescribe how it should handle progressive disclosure (i.e. code mode or not)"

    I worry that a single group string on a registered tool definition closes us off from doing that in the future, restricting us to a single flat level of grouping

    @domfarolino I agree, and I really like the idea of passing actual Tool objects around rather than tool-name strings. It might even be worth having getTools() expose Tool objects/handles rather than a JSON Schema list.

    But I think we are missing the forest for the trees here. I'd argue that the grouping mechanism unintentionally creates a opinion on how the client does progressive disclosure that runs parallel to how the rest of the community is going. Having website side grouping API is similar enough to a skill in content and purpose that having both options would be confusing. Skills & the dynamic nature of websites already give us recursive discovery. The cross-origin iframe case is interesting, do you think "origin" is enough to group by?

    From the model's perspective, the flow becomes (correct me if wrong):

    getGroups() (sort of like a skill with references) → choose group (sort of like a reference?) → inject its tool schemas
    

    But the way most agents are being design is via skills -> reference -> find the tools it needs or go to the subdomain/spa route/menu that holds them

    ## Delta.com
    - [Find flights](...)
    - [Manage a trip](...)
    - [Manage SkyMiles](...)

    I fought the tool list sync problem for a long time (see #30) but have settled on making tool list fetching the agents concern rather than just try to give the right tool list on every turn. Then the agent can do things like playwright's page.waitForLoadState("networkidle") before fetching the tool list rather than get confused when the tool list it gets back is empty because the page failed to load before the next turn

  7. domfarolino commented on Sep 1, 2026

    @domfarolino
    Collaborator

    Yeah, I think I agree with that framing. I do feel an explicit grouping API is likely too opinionated for how early things are, and I think we can avoid that while still assisting whatever tool search mechanism an agent brings itself, by supplying group metadata "hints" on tools that feed into client's opaque mechanism. I think you agree with this, right @MiguelsPizza?

    Apart from that, I want to touch on:

    But the way most agents are being design is via skills -> reference -> find the tools it needs or go to the subdomain/spa route/menu that holds them
    [...]
    I fought the tool list sync problem for a long time (see #30) but have settled on making tool list fetching the agents concern rather than just try to give the right tool list on every turn.

    It sounds like you're leaning towards "skills (with tool references) -> find the tools it needs via some opaque mechanism the agent itself supplies".

    But as long as skills reference tools, we still have the "tools list sync" problem, since names in both places can drift, right? Do you see a good way to have skills reference tools without hitting this issue? I can imagine some Skill primitive that references tool objects explicitly, so that we avoid the naming sync issue directly. Or the inverse: supplying a skill on the tool's definition (to show that this tool helps fulfill some pre-existing skill), and throwing an error whenever you register a tool whose name doesn't match one of the provided skill's expected tool names, etc. Just curious if you've thought about this.

  8. MiguelsPizza commented on Sep 1, 2026

    @MiguelsPizza
    Contributor

    we can avoid that while still assisting whatever tool search mechanism an agent brings itself, by supplying group metadata "hints" on tools that feed into client's opaque mechanism. I think you agree with this, right @MiguelsPizza?

    yep!

    It sounds like you're leaning towards "skills (with tool references) -> find the tools it needs via some opaque mechanism the agent itself supplies".

    A bit of terminology clash here, but skill references are just other markdown documents that the skill itself holds. see: https://agentskills.io/home

    my-skill/
    ├── SKILL.md          # Required: metadata + instructions
    ├── scripts/          # Optional: executable code
    ├── references/       # Optional: documentation
    ├── assets/           # Optional: templates, resources
    └── ...               # Any additional files or directories

    We don't need most of it for a WebMCP implementation. The point is that a reference tells the model what to do ("here's how to find and compare flights"), not which tools to call. Getting from the reference to the right tools is the part I don't have a great answer for and have not really played around with myself.

    But as long as skills reference tools, we still have the "tools list sync" problem

    Agreed, I don't think skills should reference exact tools. Your inverse idea is close to what I was thinking with a group "tag": it's somewhat analogous to a foreign key in SQL, where you define your groups once and pass the reference to both the skill declaration and the tool declaration, like you were showing above with tool objects. Then the skill can say "use the cart-checkout tools" without ever naming a tool, and the tools drift freely underneath it. I don't have strong opinions on the exact shape though. I guess the nice thing about tool registration being async is we can throw if the group doesn't exist, but that only works if groups get declared somewhere first, which is basically your handle idea. Curious to hear your thoughts.

  9. anssiko commented on Sep 3, 2026

    @anssiko
    Member

    RESOLUTION: Seek explicit model harness creator feedback and defer the tool collections feature. (issue #255)

  10. jackdizhu commented on Sep 10, 2026

    @jackdizhu

    Apologies for not seeing this discussion earlier. I would like to share my thoughts on the results I expect to achieve.

    I expect that if I grant an agent access to the tools of specific modules, it should only be able to access and invoke the tools of those modules, rather than having to read through the tools of all modules to find the ones it needs.

    I believe the focus should be on how to reliably manage and disclose tools as I expect, rather than on how to register them.

    Regarding #88 , while that issue suggests having the agent filter for the necessary tools, I would prefer a mechanism analogous to DOM node management.

    graph LR
        A[Module Activated / Mounted] -->|Sync Restore| B(Tools Auto-Registered / Available)
        B -->|Module Cached / Detached| C{Module State Change}
        C -->|Detached Node| D(Tools Auto-Unregistered / Invisible)
        D -->|Reactivated from Cache| A
    

    For example, when a "Product Inventory" module is detached from the active document flow and moved into cache—essentially becoming a detached node—all tools registered by that module should be automatically unregistered as well.
    Conversely, when the module is reactivated from cache, its associated tools should be synchronously restored.This would align with the behavior described in the #declarative-api-explainer .

    Update: To validate these ideas in practice, I've been building #webmcp-example — an open-source reference implementation of the WebMCP draft, consisting of a Chrome MV3 extension (side-panel agent) and a tool-providing web app. A few things we learned that may be relevant to this discussion:

    • Scope-first disclosure works well in practice. The agent never sees a flat list of all tools. Page tools are namespaced with a tab__ prefix, and the agent only receives tools from the context(s) the user explicitly selected — so it invokes tools directly without enumerating or filtering the toolsets of unrelated pages.
    • Tool lifecycle follows context lifecycle. When a tab is deselected or navigates away, its tools drop out of the agent's toolset; when the user re-selects it, the tools are restored. This mirrors the DOM node management analogy above and keeps disclosure reliable without requiring per-module bookkeeping by the author.
  11. jzabroski commented on Sep 11, 2026

    @jzabroski

    @sdras Progressive disclosure means two things to me:

    1. Profiles - the top-level disclosure, not addressed in your spec
    2. Groups/Shared Resources (such as Tools) - what this spec currently addresses

    A Profile is what dsh (DeekSeek Harness) uses to distinguish what tools/skills/agents to load into an operating environment. Example values for WebMCP would be Auto, TV, Desktop, Mobile, Headless.

    I think this is a critical point, and why dsh is currently the most cutting edge harness out there - and since it is open source and works cross platform, it is arguably the most widely adopted open source standard out there by volume of users. ChatGPT, Claude have dropped the ball here by hiding personal productivity assistants/agents behind "Work" tabs. The user experiences that are winning in personal productivity space are Grok Bot and dsh. (If you drive a car and have tried using Claude Work with Android Auto, you know this clearly isn't even on Anthropic's radar, and users are trying workarounds)

  12. johnhenry commented on Sep 15, 2026

    @johnhenry

    Adding a data point from a second production site, since the harness-creator-feedback angle came up in the discussion above.

    Data point

    jjhub is a code-review and change-management app. Its page registers one WebMCP tool per operation, 126 in total, mirroring its remote MCP server. Descriptions are short (median 94 characters, longest 762), and every schema is a flat object.

    • ChatGPT's browser: "WebMCP is disabled for this page because the site's WebMCP configuration exceeds supported limits." Zero tools, no exception, nothing observable from the page.
    • Chrome: no error, but the same flat list of 126 goes to the agent.

    We rewrote registration as progressive loading: a hard budget of 20 registered tools at any time, a core set of about 8 plus three loader tools (list_tool_groups, load_tool_group, search_tools), route-aware auto-loading of the group that matches the current view, and least-recently-used unloading so the budget is never exceeded during a transition. All 126 tools stay callable; an agent discovers, loads, then calls.

    This works, but it's entirely userland — every page reinvents it slightly differently, and an agent has no way to know that search_tools on one site and find_capabilities on another are the same thing.

    What we landed on

    Given the "hints, not a prescribed mechanism" direction this thread seems to be converging on, here's the shape we'd want group metadata to support rather than a full explicit-grouping API:

    • A lazy marker at registration, not a separate active-group call: present in the catalog for discovery, absent from the loaded/working set until asked for. Lets a page register everything once, up front, instead of hand-rolling onMounted/onUnmounted bookkeeping (the keep-alive accumulation problem raised elsewhere in this thread).
    • One discovery surface (searchTools/listTools({ lazy: true })) instead of page-specific loader tools, so an agent's "search, then load" step is the same on every site.
    • Loading is a request, not a guarantee — the user agent can refuse based on its own budget, and eviction (if it happens) is announced via toolchange rather than silent, so the agent knows its working set changed.
    • The budget stays the user agent's to set, same conclusion Two agent-browser behaviours a page cannot detect or recover from #293 reached — this only standardizes the discovery/loading vocabulary, not the ceiling itself.

    Happy to contribute the jjhub case (126 tools, budget 20, groups of about 10) to webmcp-evals as a reproducible fixture if that would help size the numbers for whatever direction this converges on.

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions