-
Notifications
You must be signed in to change notification settings - Fork 1.9k
docs(server,examples): document extension capabilities with a runnable example #2387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+187
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # extension-capabilities | ||
|
|
||
| The server declares one extension capability, `com.example/feature-flags`, with | ||
| a small settings object via `server.registerCapabilities({ extensions: { … } })`. | ||
| The client connects once per era leg and asserts the entry and its settings are | ||
| advertised — by the `initialize` result on the legacy leg and by | ||
| `server/discover` on the modern leg. | ||
|
|
||
| ```bash | ||
| pnpm tsx examples/extension-capabilities/client.ts # modern (server/discover) | ||
| pnpm tsx examples/extension-capabilities/client.ts --legacy # 2025 initialize handshake | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** | ||
| * Connects to `./server.ts` and asserts the `com.example/feature-flags` | ||
| * extension capability and its settings are advertised on both the legacy | ||
| * (`--legacy`, 2025 `initialize`) and modern (`server/discover`) legs. | ||
| * | ||
| * Spawns the sibling `server.ts` over stdio by default, or connects to a | ||
| * running endpoint under `--http <url>`. See `examples/CONTRIBUTING.md` for | ||
| * the canonical shape. | ||
| */ | ||
| import { check, parseExampleArgs, siblingPath } from '@mcp-examples/shared'; | ||
| import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; | ||
| import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; | ||
|
|
||
| const { transport, url, era } = parseExampleArgs(); | ||
|
|
||
| const client = new Client( | ||
| { name: 'extension-capabilities-example-client', version: '1.0.0' }, | ||
| { versionNegotiation: { mode: era === 'modern' ? 'auto' : 'legacy' } } | ||
| ); | ||
|
|
||
| await client.connect( | ||
| transport === 'stdio' | ||
| ? new StdioClientTransport({ command: 'npx', args: ['-y', 'tsx', siblingPath(import.meta.url, 'server.ts')] }) | ||
| : new StreamableHTTPClientTransport(new URL(url)) | ||
| ); | ||
|
|
||
| // Read the negotiated extension map after connecting. | ||
| const extensions = client.getServerCapabilities()?.extensions ?? {}; | ||
| console.log( | ||
| `[client] ${era} leg (${client.getNegotiatedProtocolVersion()}) advertised extensions: ${Object.keys(extensions).join(', ') || '(none)'}` | ||
| ); | ||
|
|
||
| check.ok('com.example/feature-flags' in extensions); | ||
| check.deepEqual(extensions['com.example/feature-flags'], { flags: ['dark-mode', 'beta-search'] }); | ||
|
|
||
| await client.close(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "name": "@mcp-examples/extension-capabilities", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "server": "tsx server.ts", | ||
| "client": "tsx client.ts" | ||
| }, | ||
| "dependencies": { | ||
| "@mcp-examples/shared": "workspace:*", | ||
| "@modelcontextprotocol/client": "workspace:*", | ||
| "@modelcontextprotocol/node": "workspace:*", | ||
| "@modelcontextprotocol/server": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "tsx": "catalog:devTools" | ||
| }, | ||
| "example": { | ||
| "era": "dual", | ||
| "//": "Both legs assert the extension entry and its settings are advertised." | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * Declares one extension capability, `com.example/feature-flags`, with a small | ||
| * settings object. The entry is advertised to every peer — by the `initialize` | ||
| * result on legacy connections and by `server/discover` on modern ones. | ||
| * | ||
| * One binary, either transport — selected by `--http --port <N>` (defaults to | ||
| * stdio). See `examples/CONTRIBUTING.md` for the canonical shape. | ||
| */ | ||
| import { createServer } from 'node:http'; | ||
|
|
||
| import { parseExampleArgs } from '@mcp-examples/shared'; | ||
| import { toNodeHandler } from '@modelcontextprotocol/node'; | ||
| import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; | ||
| import { serveStdio } from '@modelcontextprotocol/server/stdio'; | ||
|
|
||
| function buildServer(): McpServer { | ||
| const mcp = new McpServer({ name: 'extension-capabilities-server', version: '1.0.0' }); | ||
|
|
||
| // Declare the extension and its settings before connecting. | ||
| mcp.server.registerCapabilities({ | ||
| extensions: { 'com.example/feature-flags': { flags: ['dark-mode', 'beta-search'] } } | ||
| }); | ||
|
|
||
| return mcp; | ||
| } | ||
|
|
||
| const { transport, port } = parseExampleArgs(); | ||
|
|
||
| if (transport === 'stdio') { | ||
| void serveStdio(buildServer); | ||
| console.error('[server] serving over stdio'); | ||
| } else { | ||
| const handler = createMcpHandler(buildServer); | ||
| createServer(toNodeHandler(handler)).listen(port, () => { | ||
| console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The new "Extension capabilities" section links "MCP extensions" to
specification/latest, but thecapabilities.extensionskey only exists in the 2026-07-28 draft revision (it is absent from the 2025-11-25 spec types and the 2025 wire view), so the link should point atspecification/draftper the repo's convention for draft-only material — or the prose should note the spec revision. Secondarily, "each value is free-form JSON" on line 372 would be more precise as "free-form JSON object", since the schema (z.record(z.string(), JSONObjectSchema)) requires each extension's settings value to be a JSON object.Extended reasoning...
1. The spec link targets
latest, but the feature is draft-only.Line 363 introduces the section with a link to
https://modelcontextprotocol.io/specification/latest/basic/lifecycle#capability-negotiation. However, theextensionscapability key is not part of any published spec revision — it exists only in the 2026-07-28 draft schema. The repo's own spec mirrors are decisive here:packages/core-internal/src/types/spec.types.2025-11-25.ts—ClientCapabilities/ServerCapabilitieshave noextensionsmember (the only "extension" hits are unrelated comments about unofficialparamsextensions).packages/core-internal/src/types/spec.types.2026-07-28.ts(~lines 744–754 and 841–851) —extensions?: { [key: string]: JSONObject }is declared on both capability types, including the_meta-key-naming rule the new paragraph paraphrases.packages/core-internal/src/wire/rev2025-11-25/wireTypes.ts:23— the adjudication ledger states: "'extensions' capability key: 2026-only; absent from the 2025 wire view", and theWire2025*Capabilitiestypes omit the key.Since 2026-07-28 is the draft revision (the docs themselves call it that throughout), the
specification/latestalias resolves to the latest published revision (2025-11-25), whose lifecycle/capability-negotiation page does not define MCP extensions — so the introductory link sends readers to a spec page that doesn't document the feature being introduced.Step-by-step: a reader hits the new section → clicks "MCP extensions" → lands on the 2025-11-25 lifecycle capability-negotiation section → finds no mention of an
extensionscapability map or the identifier-naming rule the paragraph references → has no spec anchor for the feature. Every otherspecification/latestlink indocs/(instructions, progress, logging, authorization) points at features present in the published revision, while draft-only content (the SEP-2577 deprecated-features registry) is consistently linked viaspecification/draft(e.g. docs/server.md lines ~380/486/586 and docs/client.md ~594/625/672). Fix: change the link tospecification/draft/basic/lifecycle#capability-negotiation, or keeplatestand add a parenthetical noting the capability is defined in the 2026-07-28 draft revision. (Caveat: modelcontextprotocol.io could not be fetched from the review environment; the claim rests on the in-repo spec twins and the wire adjudication ledger, which pin extensions as 2026-only.)2. "free-form JSON" → "free-form JSON object".
Line 372 says "each value is free-form JSON for that extension's settings". The implementation constrains each value to a JSON object:
ServerCapabilitiesSchemadeclaresextensions: z.record(z.string(), JSONObjectSchema)in bothpackages/core-internal/src/wire/rev2025-11-25/schemas.ts:492andrev2026-07-28/schemas.ts:254, andJSONObjectSchemaisz.record(z.string(), JSONValueSchema)— a bare string, number, or array value is rejected by the Zod schema (and by the TypeScript capability type at compile time). One reviewer noted the surrounding sentence already says "settings object" twice and shows{}, so the misread risk is low and TypeScript would catch a wrong value anyway — fair, which is why this half is purely a one-word precision tweak ("free-form JSON object") rather than anything blocking; it just keeps the prose strictly consistent with what the schema accepts (the contents of the object are free-form, the value's top-level type is not).Both points are documentation-accuracy fixes in a docs-only PR — small, mechanical, and non-blocking.