Skip to content

sep: add progressive disclosure via groups proposal - #19

Open
helloeve wants to merge 4 commits into
modelcontextprotocol:mainfrom
helloeve:sep
Open

helloeve wants to merge 4 commits into
modelcontextprotocol:mainfrom
helloeve:sep

Conversation

@helloeve

Copy link
Copy Markdown
Contributor

Adds the draft SEP for the io.modelcontextprotocol/groups extension (and links it from the README contents table).

It defines progressive disclosure of tools and other primitives: a Group is a named, lightweight bundle a server advertises once both sides negotiate the extension, with groups/list keeping listing cheap and groups/get returning a group's scoped tools, prompts, resources, and nested groups as typed, invokable definitions in a single stateless round-trip.

Scoped primitives stay out of tools/list/prompts/list/resources/list so servers can ship rich per-workflow capability without bloating every session, while scoped actions still run through ordinary tools/call (no client filesystem or shell needed).

Add the draft SEP for the io.modelcontextprotocol/groups extension
(progressive disclosure via activatable groups) and link it from the
documentation table.

@OR13 OR13 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this challenge solved by agents and teams today?

So you don't progressively keep adding skills to the same agent, but instead, when you start hitting the limits of what separates a "specialist" from a "generalist", you make specialist teams, and make the generalist aware of how to invoke them?

Does this proposal align with giving specialist agents groups?

I guess my question is really, is there any aspect of this that interacts with agents or skills over mcp, seems like those paths could also address some of this.

@kurtisvg

kurtisvg commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

I think this is a great start. Some quick questions I think we should make sure are discussed or covered more clearly:

Design questions

  • Relationship to Skills: Could we expand the FAQ to clarify why Agent Skills / Skills over MCP alone are insufficient, and how the two extensions compose? In particular, how would a group reference skills?
  • Reference model: The draft currently defines a DAG with multiple parents and no cycles. Should we prefer this over a filesystem-style tree with paths/prefixes, or allow a general graph? Are group identifiers intentionally independent of their navigation hierarchy?

Protocol correctness

  • Dependencies: Instructions may reference top-level tools, but the nesting section requires all tools they depend on to appear in the group’s own contents.tools. Can we reconcile these rules, given that placement is exclusive?
  • Updates: How should clients detect changes to group instructions, scoped schemas, or nested membership? list_changed currently covers only the top-level listing. Should groups/get promise side-effect-free retrieval rather than identical results across calls?

Naming and API semantics

  • Operation name: Would groups/load or groups/activate communicate the intent better than groups/get? Since activation is explicitly client-side, activate may imply a state transition the RPC does not perform.
  • Primitive name: Is Groups the clearest name for this abstraction? Would Bundles or Collections better convey its purpose while retaining the broader scope beyond tools and workflows?
  • Field names and meaning: Could we make the distinction between a group’s description and instructions clearer? The draft already separates them: description helps the model decide when to load the group, while instructions explains how to use its contents after loading. Would summary or selectionGuidance make the former clearer, and should we consistently describe the latter as “group instructions” rather than skill-specific content?

Comment thread seps/0000-progressive-disclosure-groups.md Outdated
Comment thread seps/0000-progressive-disclosure-groups.md Outdated
@andriy-viyatyk

Copy link
Copy Markdown

I've been working on the problem this proposal describes from a different direction, and I think it's a useful data point. ai-vision exposes an app's object model to agents as a tree. The agent navigates it by path, like pages[0].editor.rowCount, through a single MCP tool. Each response lists what's under that node, so the agent discovers the tree step by step. In Persephone, a desktop notepad, the tree holds about 900 methods and properties, and agents navigate it without any special instructions, because it reads like objects in code.

It is "nested groups" taken to the extreme, with its own trade-offs. I wrote it up here, with a recorded agent session:
https://andriy-viyatyk.github.io/ai-vision/mcp-at-scale/

Smaller models handle it too. Here Claude Haiku 5.5 and GPT-6 Luna play chess against each other through Persephone's object model:
https://www.reddit.com/r/PersephoneNotepad/comments/1x1tptg/chess_match_haiku_55_vs_gpt_6_luna_haiku_won_and/

I'm also interested in a standard along these lines for the web. Persephone's built-in browser already mounts a page's WebMCP tools into its object model under that page, pages[id].editor.app. A site can also publish its own ai-vision tree there today, but only through Persephone's own convention. If sites had a standard way to publish a navigable model, as WebMCP gives them for flat tools, any agent host could plug them into its own tree the same way.

I'm happy to share more details.

@helloeve

Copy link
Copy Markdown
Contributor Author

Isn't this challenge solved by agents and teams today?

These are orthogonal. I'd consider agents/teams host-side orchestration — splitting work across agents and routing between them — whereas Groups is a server-side protocol mechanism: how one server discloses its own capabilities over a single connection. They operate at different layers and compose rather than compete.

So you don't progressively keep adding skills to the same agent, but instead, when you start hitting the limits of what separates a "specialist" from a "generalist", you make specialist teams, and make the generalist aware of how to invoke them?

Does this proposal align with giving specialist agents groups?

I guess my question is really, is there any aspect of this that interacts with agents or skills over mcp, seems like those paths could also address some of this.

TL;DR they're complementary: skills disclose instructions and files, groups disclose the typed, invokable tools those instructions drive. A skill can name a tool but can't carry its schema or make it callable; groups fills exactly that gap. Here's the more detailed explanation of how the two connect, both using existing surface:

  1. A group references a skill by scoping its resource. A groups/get response can include the skill's skill://…/SKILL.md (and supporting files) in contents.resources, alongside the tools it orchestrates in contents.tools. The host resolves it through the Skills extension exactly as it would any other skill:// reference following SEP-2640.

  2. A group inlines a skill to achieve what a skill does. When the same server owns both, a group can instead carry the SKILL.md directly in its instructions field to mirror an Agent Skill.

@helloeve

helloeve commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

I think this is a great start. Some quick questions I think we should make sure are discussed or covered more clearly:

Design questions

  • Relationship to Skills: Could we expand the FAQ to clarify why Agent Skills / Skills over MCP alone are insufficient, and how the two extensions compose? In particular, how would a group reference skills?
  • Reference model: The draft currently defines a DAG with multiple parents and no cycles. Should we prefer this over a filesystem-style tree with paths/prefixes, or allow a general graph? Are group identifiers intentionally independent of their navigation hierarchy?

Replied in #19 (comment) but at the same time I will bring the following open question for discussion in WG: do we need a skill-specific identifier in the groups primitive as a way for a group to explicitly declare "this resource is the skill I implement". Or do we leave it as an ordinary resource the host recognizes via the skills extension itself? Adding a marker makes the link explicit but introduces a dependency between the two optional extensions and the complexity that comes with it. I'd lean toward not adding one for now, but worth discussing.

Protocol correctness

  • Dependencies: Instructions may reference top-level tools, but the nesting section requires all tools they depend on to appear in the group’s own contents.tools. Can we reconcile these rules, given that placement is exclusive?
  • Updates: How should clients detect changes to group instructions, scoped schemas, or nested membership? list_changed currently covers only the top-level listing. Should groups/get promise side-effect-free retrieval rather than identical results across calls?

I am leaning towards to weaken the claim on "returns the same result" -- Side-effect-free + may change + cacheable with invalidation. For the updates, I left a comment in #19 (comment) which would like to see the WG's recommendation.

Naming and API semantics

  • Operation name: Would groups/load or groups/activate communicate the intent better than groups/get? Since activation is explicitly client-side, activate may imply a state transition the RPC does not perform.
  • Primitive name: Is Groups the clearest name for this abstraction? Would Bundles or Collections better convey its purpose while retaining the broader scope beyond tools and workflows?
  • Field names and meaning: Could we make the distinction between a group’s description and instructions clearer? The draft already separates them: description helps the model decide when to load the group, while instructions explains how to use its contents after loading. Would summary or selectionGuidance make the former clearer, and should we consistently describe the latter as “group instructions” rather than skill-specific content?

Will do a bike shedding discussion for naming:

  1. name: Groups vs. Bundles vs. Collections
  2. Operation name: groups/get vs. groups/load / groups/activate
  3. Field rename: description (when to load) → summary / selectionGuidance; instructions (how to use) → ?

Comment thread seps/0000-progressive-disclosure-groups.md
@markusvankempen

Copy link
Copy Markdown

Data point: two ways of shrinking a catalog without Groups, and what they didn't fix

I maintain mcp-ticket-demo and a few other MCP servers (portfolio). I presented this at MCP Dev Summit Toronto (session 1282401). This isn't a competing proposal. It's what we shipped before Groups existed, and where it fell short of what this draft is trying to do.

The problem we hit: near-duplicate tools. Every name was accurate, and the model still picked the wrong one.

Two different collapses:

  1. Reads into catalog + query. A Maximo server had seven query_* tools (query_records, query_with_filter, query_work_orders, query_assets, ...). They became describe_server (what am I allowed to do), get_schema (no name: the index; a name: the shape), and run_query (the noun is an argument). Writes stayed separate verbs. Adding a queryable thing adds a schema, not a tool. In mcp-ticket-demo the same rule took 31 tools to 19.
  2. One tool per noun, with an action enum. A Code Engine server went from about 112 tools to 28. A routing table sits over the existing handlers: ticket + create calls the old create_ticket. Scope is checked on the underlying action, not on the bundle. Old tool names stayed callable but unlisted, so saved prompts kept working.

Results relevant to this draft:

  • Fewer tools did not mean fewer bytes. tools/list grew from 42 KB to 45 KB after the 112 → 28 collapse. The gain was clearer selection, not less context. Collapsing tools doesn't address the context cost that groups/get targets.
  • Callable but unlisted worked. That's the mechanism this draft relies on: scoped tools out of */list, still reachable through tools/call. We used it only for backward compatibility, and nothing broke.
  • We avoided oneOf. We used a flat action enum because many clients flattened or ignored oneOf per action. This draft returns typed per-tool definitions on groups/get, which avoids that problem, but only for clients that negotiate the extension. A bundle works on every client.
  • Bundles lose per-tool metadata. A tool that both searches and closes can't be readOnlyHint. A per-tool gate can't switch off close without also switching off search. Authorization survived because we check the action; annotations and kill switches did not survive.
  • Discovery stayed open. describe_server and tools/list answer in every auth mode, and a locked tool still appears in tools/list. A client that can't ask what's required can only guess. I'd expect the same of groups/list.

A grouping axis that falls out of this: by kind of operation, like SQL.

  • Catalog (describe_server, get_schema): stays in tools/list, never behind groups/get.
  • query (run_query): read scope. Every tool readOnlyHint.
  • tickets-write (create_ticket, add_comment, close_ticket): write scope.
  • admin (settings, API keys, users): admin scope.
  • events (timers, log push): doesn't fit the SQL model, so its own group.

The draft already lets groups/get be computed per caller. With this split, a read-only caller would see the catalog plus query and never get the admin tools in context. Each group shares one scope and one set of hints, so a client could set approval policy per group. Two caveats. A group is not an authorization boundary: the server still checks scope on every tools/call. And on a 19-tool server the saving is small; it matters on multi-domain servers.

Not measured: selection accuracy before and after either collapse. "The model stopped picking wrong" is an operator observation, not a benchmark. Hints are also only as good as the server: we shipped openWorldHint: false on tools that called external APIs, so a client setting policy per group shouldn't trust them blindly.

If it would help, I can run two things next to this draft on mcp-ticket-demo: tools/list token counts for a read-only caller with and without that split, and a selection comparison of the 31-tool and 19-tool lists.

Questions:

  1. Should a server collapse near-duplicate tools before grouping them, or is the intent that overlapping tools stay distinct and groups/get keeps them out of context?
  2. Is grouping by operation class or scope in scope for this draft, or should groups stay workflow-only?
  3. Should groups/list be explicitly required to answer without a credential, the way tools/list and describe_server do in our servers?

@helloeve

Copy link
Copy Markdown
Contributor Author

Data point: two ways of shrinking a catalog without Groups, and what they didn't fix

I maintain mcp-ticket-demo and a few other MCP servers (portfolio). I presented this at MCP Dev Summit Toronto (session 1282401). This isn't a competing proposal. It's what we shipped before Groups existed, and where it fell short of what this draft is trying to do.

The problem we hit: near-duplicate tools. Every name was accurate, and the model still picked the wrong one.

Two different collapses:

  1. Reads into catalog + query. A Maximo server had seven query_* tools (query_records, query_with_filter, query_work_orders, query_assets, ...). They became describe_server (what am I allowed to do), get_schema (no name: the index; a name: the shape), and run_query (the noun is an argument). Writes stayed separate verbs. Adding a queryable thing adds a schema, not a tool. In mcp-ticket-demo the same rule took 31 tools to 19.
  2. One tool per noun, with an action enum. A Code Engine server went from about 112 tools to 28. A routing table sits over the existing handlers: ticket + create calls the old create_ticket. Scope is checked on the underlying action, not on the bundle. Old tool names stayed callable but unlisted, so saved prompts kept working.

Results relevant to this draft:

  • Fewer tools did not mean fewer bytes. tools/list grew from 42 KB to 45 KB after the 112 → 28 collapse. The gain was clearer selection, not less context. Collapsing tools doesn't address the context cost that groups/get targets.
  • Callable but unlisted worked. That's the mechanism this draft relies on: scoped tools out of */list, still reachable through tools/call. We used it only for backward compatibility, and nothing broke.
  • We avoided oneOf. We used a flat action enum because many clients flattened or ignored oneOf per action. This draft returns typed per-tool definitions on groups/get, which avoids that problem, but only for clients that negotiate the extension. A bundle works on every client.
  • Bundles lose per-tool metadata. A tool that both searches and closes can't be readOnlyHint. A per-tool gate can't switch off close without also switching off search. Authorization survived because we check the action; annotations and kill switches did not survive.
  • Discovery stayed open. describe_server and tools/list answer in every auth mode, and a locked tool still appears in tools/list. A client that can't ask what's required can only guess. I'd expect the same of groups/list.

A grouping axis that falls out of this: by kind of operation, like SQL.

  • Catalog (describe_server, get_schema): stays in tools/list, never behind groups/get.
  • query (run_query): read scope. Every tool readOnlyHint.
  • tickets-write (create_ticket, add_comment, close_ticket): write scope.
  • admin (settings, API keys, users): admin scope.
  • events (timers, log push): doesn't fit the SQL model, so its own group.

The draft already lets groups/get be computed per caller. With this split, a read-only caller would see the catalog plus query and never get the admin tools in context. Each group shares one scope and one set of hints, so a client could set approval policy per group. Two caveats. A group is not an authorization boundary: the server still checks scope on every tools/call. And on a 19-tool server the saving is small; it matters on multi-domain servers.

Not measured: selection accuracy before and after either collapse. "The model stopped picking wrong" is an operator observation, not a benchmark. Hints are also only as good as the server: we shipped openWorldHint: false on tools that called external APIs, so a client setting policy per group shouldn't trust them blindly.

If it would help, I can run two things next to this draft on mcp-ticket-demo: tools/list token counts for a read-only caller with and without that split, and a selection comparison of the 31-tool and 19-tool lists.

Questions:

  1. Should a server collapse near-duplicate tools before grouping them, or is the intent that overlapping tools stay distinct and groups/get keeps them out of context?
  2. Is grouping by operation class or scope in scope for this draft, or should groups stay workflow-only?
  3. Should groups/list be explicitly required to answer without a credential, the way tools/list and describe_server do in our servers?

Thanks! this data points are very helpful!

Q1: On collapsing near-duplicate tools before grouping -- I take this as orthogonal. The SEP's positioning on grouping is about what shows up in */list versus what the client fetches on demand.

Q2: OA group can be a workflow, a plain toolset, a knowledge bundle, or the operation/scope split you described. The thing I would like to highlight is that a group isn't an access boundary. The server still checks scope on every call, so grouping by scope helps discovery but doesn't enforce anything on access boundary.

Q3: yes, will update the draft to reflect this. Same idea as tools/list and your describe_server: a client should be able to see what's there and work out what it needs to authenticate for. What comes back still depends on the caller's view, and leaving a group off the list is about discovery, not access.

Also I will try to include some sample prototypes and it would be great to see how groups can help address the issues you have described.

Change-notification rationale now points to SEP-3371 as the reason push
notification should not extend the core subscriptions/listen filter.
@markusvankempen

Copy link
Copy Markdown

Thanks @helloeve — the answers help, and thanks for taking the unauthenticated groups/list point into the draft.

Treating tool collapse and groups as orthogonal matches what we saw: collapsing near-duplicate tools made selection clearer, and it did not shrink tools/list. Groups are the piece that keeps that payload out of context until the client asks.

Once a sample prototype is up, I’m happy to try it against mcp-ticket-demo and report how a scope split (read / write / admin) behaves, including the case where a group is hidden from groups/list but the underlying call is still checked.

@markusvankempen

markusvankempen commented Oct 19, 2026 •

Copy link
Copy Markdown

Follow-up on the two measurements I offered. Local prototype of this extension, one server, 19 tools, 5 groups. Still a data point, not a change to the draft.

Tokens. tiktoken o200k_base on the compact JSON of the result, not a provider tool prompt. A read that loads the top-level tools, groups/list, and one groups/get was 2,388 tokens, 23% of the flat tools/list (10,398). The groups envelope added 58 tokens. Fetching every group was 10,948, about 5% more than the flat list. The saving is stopping after the one group the task needs.

Does the model call groups/get? Local 8B and 14B only, 14 tasks, 5 seeds, temperature 0.2. No frontier model. While a catalog call still listed every tool name, group picks were 0. Both models called the tool. After that call listed only the top-level tools and pointed at groups/get, the 14B model took the group path on 65 of 70 trials and the 8B on 47 of 70. With every schema visible, selection was already 70 of 70 and 68 of 70. Groups keep unread schemas out of context. They do not beat a flat list the model can already read.

Anything the model reads before groups/get should not name a scoped tool, and the group sentence has to be the only place those task words appear. A scope that lives only in _meta was ignored. Leaving a group out of groups/list did not block tools/call.

On the three questions: collapse and groups do different jobs, and groups do nothing if an earlier call still lists every tool. Grouping by operation worked when the group sentence carried the task. The same constraint applies to a workflow group. groups/list answered with no credential. Turning groups off should restore the full catalog. An empty group list leaves a client that already negotiated with only the top-level tools.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants