|
| 1 | +# MCP Grouping Extension — TypeScript SDK |
| 2 | + |
| 3 | +Organize MCP tools, resources, and other primitives into named groups. This package implements the [Grouping Extension specification](../../specification/draft/grouping.mdx) as an add-on for the |
| 4 | +[`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk). |
| 5 | + |
| 6 | +> **Status:** Experimental. The capability is registered under `experimental["io.modelcontextprotocol/grouping"]` until the SDK ships the `extensions` field on `ServerCapabilities`. |
| 7 | +
|
| 8 | +## Install |
| 9 | + |
| 10 | +```bash |
| 11 | +npm install @anthropic/ext-grouping |
| 12 | +``` |
| 13 | + |
| 14 | +Peer dependency: `@modelcontextprotocol/sdk` >= 1.27. |
| 15 | + |
| 16 | +## Quick start — Server |
| 17 | + |
| 18 | +```typescript |
| 19 | +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 20 | +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; |
| 21 | +import { GroupingExtension, GROUPS_META_KEY } from '@anthropic/ext-grouping'; |
| 22 | +import { z } from 'zod/v4'; |
| 23 | + |
| 24 | +const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' }); |
| 25 | +const grouping = new GroupingExtension(mcpServer); |
| 26 | + |
| 27 | +// Register groups |
| 28 | +grouping.registerGroup('email', { |
| 29 | + title: 'Email Tools', |
| 30 | + description: 'Tools for email workflows.' |
| 31 | +}); |
| 32 | + |
| 33 | +// Assign tools to groups via _meta |
| 34 | +mcpServer.registerTool( |
| 35 | + 'send_email', |
| 36 | + { |
| 37 | + description: 'Send an email', |
| 38 | + inputSchema: { to: z.string(), body: z.string() }, |
| 39 | + _meta: { [GROUPS_META_KEY]: ['email'] } |
| 40 | + }, |
| 41 | + async ({ to, body }) => ({ |
| 42 | + content: [{ type: 'text', text: `Sent to ${to}` }] |
| 43 | + }) |
| 44 | +); |
| 45 | + |
| 46 | +// Assign resources to groups via _meta |
| 47 | +mcpServer.registerResource( |
| 48 | + 'inbox', |
| 49 | + 'email://inbox', |
| 50 | + { |
| 51 | + description: 'Current inbox', |
| 52 | + _meta: { [GROUPS_META_KEY]: ['email'] } |
| 53 | + }, |
| 54 | + async () => ({ |
| 55 | + contents: [{ uri: 'email://inbox', text: 'No messages.', mimeType: 'text/plain' }] |
| 56 | + }) |
| 57 | +); |
| 58 | + |
| 59 | +const transport = new StdioServerTransport(); |
| 60 | +await mcpServer.connect(transport); |
| 61 | +``` |
| 62 | + |
| 63 | +## Quick start — Client |
| 64 | + |
| 65 | +```typescript |
| 66 | +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; |
| 67 | +import { GroupingClient, GROUPS_META_KEY } from '@anthropic/ext-grouping'; |
| 68 | + |
| 69 | +const client = new Client({ name: 'my-client', version: '1.0.0' }); |
| 70 | +await client.connect(transport); |
| 71 | + |
| 72 | +const groupingClient = new GroupingClient(client); |
| 73 | + |
| 74 | +// List groups |
| 75 | +const { groups } = await groupingClient.listGroups(); |
| 76 | + |
| 77 | +// Filter tools by group |
| 78 | +const { tools } = await client.listTools(); |
| 79 | +const emailTools = tools.filter(t => { |
| 80 | + const membership = GroupingClient.getGroupMembership(t._meta); |
| 81 | + return membership.includes('email'); |
| 82 | +}); |
| 83 | + |
| 84 | +// Listen for group changes |
| 85 | +groupingClient.onGroupsChanged(async () => { |
| 86 | + const updated = await groupingClient.listGroups(); |
| 87 | + console.log('Groups updated:', updated.groups); |
| 88 | +}); |
| 89 | +``` |
| 90 | + |
| 91 | +## Nested groups |
| 92 | + |
| 93 | +Groups can belong to other groups via the same `_meta` key: |
| 94 | + |
| 95 | +```typescript |
| 96 | +grouping.registerGroup('productivity', { |
| 97 | + title: 'Productivity Suite' |
| 98 | +}); |
| 99 | + |
| 100 | +grouping.registerGroup('email', { |
| 101 | + title: 'Email Tools', |
| 102 | + _meta: { [GROUPS_META_KEY]: ['productivity'] } |
| 103 | +}); |
| 104 | + |
| 105 | +grouping.registerGroup('calendar', { |
| 106 | + title: 'Calendar Tools', |
| 107 | + _meta: { [GROUPS_META_KEY]: ['productivity'] } |
| 108 | +}); |
| 109 | +``` |
| 110 | + |
| 111 | +A client can then expand a parent group into all its descendants: |
| 112 | + |
| 113 | +```typescript |
| 114 | +const { groups } = await groupingClient.listGroups(); |
| 115 | + |
| 116 | +// Build parent → children map |
| 117 | +const children = new Map<string, string[]>(); |
| 118 | +for (const g of groups) { |
| 119 | + for (const parent of GroupingClient.getGroupMembership(g._meta)) { |
| 120 | + if (!children.has(parent)) children.set(parent, []); |
| 121 | + children.get(parent)!.push(g.name); |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +// BFS to collect all descendant group names |
| 126 | +function expand(name: string): Set<string> { |
| 127 | + const visited = new Set<string>(); |
| 128 | + const queue = [name]; |
| 129 | + while (queue.length) { |
| 130 | + const cur = queue.shift()!; |
| 131 | + if (visited.has(cur)) continue; |
| 132 | + visited.add(cur); |
| 133 | + for (const child of children.get(cur) ?? []) queue.push(child); |
| 134 | + } |
| 135 | + return visited; |
| 136 | +} |
| 137 | + |
| 138 | +const allProductivity = expand('productivity'); |
| 139 | +// Set { "productivity", "email", "calendar" } |
| 140 | +``` |
| 141 | + |
| 142 | +## API reference |
| 143 | + |
| 144 | +### `GroupingExtension` (server) |
| 145 | + |
| 146 | +```typescript |
| 147 | +import { GroupingExtension } from '@anthropic/ext-grouping'; |
| 148 | + |
| 149 | +const grouping = new GroupingExtension(mcpServer); |
| 150 | +``` |
| 151 | + |
| 152 | +| Method | Description | |
| 153 | +| ------------------------------ | ----------------------------------------------------------------- | |
| 154 | +| `registerGroup(name, config?)` | Register a group. Returns a `RegisteredGroup` handle. | |
| 155 | +| `removeGroup(name)` | Remove a group by name. | |
| 156 | +| `sendGroupListChanged()` | Manually send a `notifications/groups/list_changed` notification. | |
| 157 | + |
| 158 | +**`RegisteredGroup`** handle returned by `registerGroup`: |
| 159 | + |
| 160 | +| Property / Method | Description | |
| 161 | +| ------------------------------------------------------- | --------------------------------------------------- | |
| 162 | +| `title`, `description`, `icons`, `annotations`, `_meta` | Group metadata (read/write). | |
| 163 | +| `enabled` | Whether the group appears in `groups/list` results. | |
| 164 | +| `enable()` / `disable()` | Toggle visibility. | |
| 165 | +| `update(updates)` | Batch-update fields (including rename via `name`). | |
| 166 | +| `remove()` | Remove the group. | |
| 167 | + |
| 168 | +### `GroupingClient` (client) |
| 169 | + |
| 170 | +```typescript |
| 171 | +import { GroupingClient } from '@anthropic/ext-grouping'; |
| 172 | + |
| 173 | +const groupingClient = new GroupingClient(client); |
| 174 | +``` |
| 175 | + |
| 176 | +| Method | Description | |
| 177 | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- | |
| 178 | +| `listGroups(params?)` | Send `groups/list` request. Accepts `{ cursor?: string }` for pagination. | |
| 179 | +| `onGroupsChanged(handler)` | Register a callback for `notifications/groups/list_changed`. | |
| 180 | +| `GroupingClient.getGroupMembership(meta)` | Static utility — extracts the `string[]` of group names from a primitive's `_meta`. Returns `[]` if absent. | |
| 181 | + |
| 182 | +### Constants and types |
| 183 | + |
| 184 | +| Export | Description | |
| 185 | +| ------------------------------------ | ----------------------------------------------------------------------------------- | |
| 186 | +| `GROUPS_META_KEY` | `"io.modelcontextprotocol/groups"` — the reserved `_meta` key for group membership. | |
| 187 | +| `GROUPING_EXTENSION_ID` | `"io.modelcontextprotocol/grouping"` — the canonical extension identifier. | |
| 188 | +| `GroupSchema` | Zod schema for a `Group` object. | |
| 189 | +| `ListGroupsRequestSchema` | Zod schema for the `groups/list` request. | |
| 190 | +| `ListGroupsResultSchema` | Zod schema for the `groups/list` response. | |
| 191 | +| `GroupListChangedNotificationSchema` | Zod schema for `notifications/groups/list_changed`. | |
| 192 | +| `Group`, `ListGroupsResult`, … | TypeScript type aliases inferred from the above schemas. | |
| 193 | + |
| 194 | +## Capability registration |
| 195 | + |
| 196 | +The extension registers its capability at: |
| 197 | + |
| 198 | +```json |
| 199 | +{ |
| 200 | + "experimental": { |
| 201 | + "io.modelcontextprotocol/grouping": { |
| 202 | + "listChanged": true |
| 203 | + } |
| 204 | + } |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +When the MCP SDK adds the `extensions` field to `ServerCapabilities`, this will move to `capabilities.extensions["io.modelcontextprotocol/grouping"]`. |
| 209 | + |
| 210 | +## Running the examples |
| 211 | + |
| 212 | +```bash |
| 213 | +cd sdk/typescript |
| 214 | +npm install |
| 215 | + |
| 216 | +# Run the example server (stdio) |
| 217 | +npx tsx examples/server.ts |
| 218 | + |
| 219 | +# Run the interactive example client (spawns the server automatically) |
| 220 | +npx tsx examples/client.ts |
| 221 | +``` |
| 222 | + |
| 223 | +## Development |
| 224 | + |
| 225 | +```bash |
| 226 | +npm install |
| 227 | +npm run build # Compile TypeScript |
| 228 | +npm test # Run tests (vitest) |
| 229 | +npm run test:watch # Watch mode |
| 230 | +``` |
0 commit comments