Repository navigation
Tool collections: coarse-grained grouping with progressive disclosure #255
Description
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:
- 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.
- Determinism: As I alluded to above, a skill's
instructionsfield 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
MiguelsPizza commentedon Aug 23, 2026
@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: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:
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):
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
groupskey onregisterTool?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.
This also works nicely for agents that don't use codemode and have limited filtering options.
Idan-Levin commentedon Aug 23, 2026
@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
groupslabel to each tool can be part of the solution, but it does not define the progressive-disclosure mechanism itself.MiguelsPizza commentedon Aug 25, 2026
@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.
Idan-Levin commentedon Aug 25, 2026
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?
domfarolino commentedon Aug 31, 2026
Hmm, @MiguelsPizza it sounds like you favor group metadata on each tools over explicit groups for two reasons:
Regarding (1) above:
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 asgroupName: "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
Toolinterface object to represent each tool, you might be able to do something like:I feel like this might help solve the synchronization problem using web platform primitives, although I suppose the same is possible on each tool:
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:
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
groupstring 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?MiguelsPizza commentedon Sep 1, 2026
@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)"
@domfarolino I agree, and I really like the idea of passing actual
Toolobjects around rather than tool-name strings. It might even be worth havinggetTools()exposeToolobjects/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):
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. 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 turndomfarolino commentedon Sep 1, 2026
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:
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
Skillprimitive 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.MiguelsPizza commentedon Sep 1, 2026
yep!
A bit of terminology clash here, but skill references are just other markdown documents that the skill itself holds. see: https://agentskills.io/home
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.
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.
anssiko commentedon Sep 3, 2026
jackdizhu commentedon Sep 10, 2026
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.
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:
jzabroski commentedon Sep 11, 2026
@sdras Progressive disclosure means two things to me:
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
dshis 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 anddsh. (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)johnhenry commentedon Sep 15, 2026
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.
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_toolson one site andfind_capabilitieson 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:
lazymarker 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-rollingonMounted/onUnmountedbookkeeping (thekeep-aliveaccumulation problem raised elsewhere in this thread).searchTools/listTools({ lazy: true })) instead of page-specific loader tools, so an agent's "search, then load" step is the same on every site.toolchangerather than silent, so the agent knows its working set changed.Happy to contribute the jjhub case (126 tools, budget 20, groups of about 10) to
webmcp-evalsas a reproducible fixture if that would help size the numbers for whatever direction this converges on.