From 57582524f9685674eaf90b43201867e57fc577b5 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 29 Jun 2026 15:50:07 +0000 Subject: [PATCH] docs(server,examples): document extension capabilities with a runnable example --- docs/client.md | 14 ++++++++ docs/server.md | 16 +++++++++ examples/README.md | 1 + examples/extension-capabilities/README.md | 12 +++++++ examples/extension-capabilities/client.ts | 36 +++++++++++++++++++ examples/extension-capabilities/package.json | 22 ++++++++++++ examples/extension-capabilities/server.ts | 37 ++++++++++++++++++++ examples/guides/clientGuide.examples.ts | 16 +++++++++ examples/guides/serverGuide.examples.ts | 14 ++++++++ pnpm-lock.yaml | 19 ++++++++++ 10 files changed, 187 insertions(+) create mode 100644 examples/extension-capabilities/README.md create mode 100644 examples/extension-capabilities/client.ts create mode 100644 examples/extension-capabilities/package.json create mode 100644 examples/extension-capabilities/server.ts diff --git a/docs/client.md b/docs/client.md index 3b7d208b24..16efc274f1 100644 --- a/docs/client.md +++ b/docs/client.md @@ -166,6 +166,20 @@ const systemPrompt = ['You are a helpful assistant.', instructions].filter(Boole console.log(systemPrompt); ``` +### Extension capabilities + +The negotiated server capabilities include `extensions` — a map from extension identifier to that extension's settings object. Read it after connecting via {@linkcode @modelcontextprotocol/client!client/client.Client#getServerCapabilities | client.getServerCapabilities()}: + +```ts source="../examples/guides/clientGuide.examples.ts#extensionCapabilities_read" +const extensions = client.getServerCapabilities()?.extensions ?? {}; + +if ('com.example/feature-flags' in extensions) { + // Advertised on this connection; the entry's value is its settings object. +} +``` + +See [Extension capabilities](./server.md#extension-capabilities) in the server guide for the declaring side. + ## Authentication MCP servers can require authentication before accepting client connections (see [Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization) in the MCP specification). Pass an {@linkcode @modelcontextprotocol/client!client/auth.AuthProvider | diff --git a/docs/server.md b/docs/server.md index 38163bd4c8..0f8e4d120a 100644 --- a/docs/server.md +++ b/docs/server.md @@ -358,6 +358,22 @@ server.registerPrompt( ); ``` +## Extension capabilities + +A server advertises support for [MCP extensions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#capability-negotiation) through `capabilities.extensions` — a map from extension identifier to that extension's settings object. Declare entries with +{@linkcode @modelcontextprotocol/server!server/server.Server#registerCapabilities | server.server.registerCapabilities()} before connecting: + +```ts source="../examples/guides/serverGuide.examples.ts#extensionCapabilities_register" +server.server.registerCapabilities({ + extensions: { 'com.example/feature-flags': { flags: ['dark-mode', 'beta-search'] } } +}); +``` + +The map is advertised in the `initialize` result on legacy connections and in the `server/discover` response on 2026-07-28 ones. Identifiers are prefix-qualified per the spec's `_meta` key naming rules (e.g. `com.example/feature-flags`); each value is free-form JSON for +that extension's settings — `{}` means supported with no settings. + +For a runnable pair, see the [`extension-capabilities/` example](../examples/extension-capabilities/README.md); reading the map client-side is covered in the [client guide](./client.md#extension-capabilities). + ## Logging > [!WARNING] diff --git a/examples/README.md b/examples/README.md index 89ee80f756..dee74f7735 100644 --- a/examples/README.md +++ b/examples/README.md @@ -42,6 +42,7 @@ The one exception to the generic commands is the reference pair: [`cli-client/`] | [`caching/`](./caching/README.md) | `cacheHints` stamping on cacheable results (2026-07-28) | stdio + http | modern | | [`gateway/`](./gateway/README.md) | `connect({ prior })` — probe once, zero-round-trip connect for every worker (gateway pattern) | http | modern | | [`custom-methods/`](./custom-methods/README.md) | Vendor-prefixed methods + custom notifications | stdio + http | dual | +| [`extension-capabilities/`](./extension-capabilities/README.md) | Declaring `capabilities.extensions` and reading the negotiated map | stdio + http | dual | | [`schema-validators/`](./schema-validators/README.md) | ArkType, Valibot, Zod, and `outputSchema` | stdio + http | dual | | [`custom-version/`](./custom-version/README.md) | `supportedProtocolVersions` / version negotiation | stdio + http | legacy | | [`parallel-calls/`](./parallel-calls/README.md) | Multiple clients / parallel tool calls, per-client notifications | stdio + http | dual | diff --git a/examples/extension-capabilities/README.md b/examples/extension-capabilities/README.md new file mode 100644 index 0000000000..0996228bf5 --- /dev/null +++ b/examples/extension-capabilities/README.md @@ -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 +``` diff --git a/examples/extension-capabilities/client.ts b/examples/extension-capabilities/client.ts new file mode 100644 index 0000000000..539ecc70a6 --- /dev/null +++ b/examples/extension-capabilities/client.ts @@ -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 `. 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(); diff --git a/examples/extension-capabilities/package.json b/examples/extension-capabilities/package.json new file mode 100644 index 0000000000..1acb70d384 --- /dev/null +++ b/examples/extension-capabilities/package.json @@ -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." + } +} diff --git a/examples/extension-capabilities/server.ts b/examples/extension-capabilities/server.ts new file mode 100644 index 0000000000..9b6ac21ace --- /dev/null +++ b/examples/extension-capabilities/server.ts @@ -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 ` (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`); + }); +} diff --git a/examples/guides/clientGuide.examples.ts b/examples/guides/clientGuide.examples.ts index 0ce10cca0f..db6a2fd14f 100644 --- a/examples/guides/clientGuide.examples.ts +++ b/examples/guides/clientGuide.examples.ts @@ -142,6 +142,21 @@ async function serverInstructions_basic(client: Client) { //#endregion serverInstructions_basic } +// --------------------------------------------------------------------------- +// Extension capabilities +// --------------------------------------------------------------------------- + +/** Example: Read the negotiated extension capabilities after connecting. */ +function extensionCapabilities_read(client: Client) { + //#region extensionCapabilities_read + const extensions = client.getServerCapabilities()?.extensions ?? {}; + + if ('com.example/feature-flags' in extensions) { + // Advertised on this connection; the entry's value is its settings object. + } + //#endregion extensionCapabilities_read +} + // --------------------------------------------------------------------------- // Authentication // --------------------------------------------------------------------------- @@ -738,6 +753,7 @@ void connect_sseFallback; void Client_versionNegotiation; void disconnect_streamableHttp; void serverInstructions_basic; +void extensionCapabilities_read; void auth_tokenProvider; void auth_clientCredentials; void auth_privateKeyJwt; diff --git a/examples/guides/serverGuide.examples.ts b/examples/guides/serverGuide.examples.ts index ec9eed210d..b8f8ec8f04 100644 --- a/examples/guides/serverGuide.examples.ts +++ b/examples/guides/serverGuide.examples.ts @@ -276,6 +276,19 @@ function registerPrompt_completion(server: McpServer) { //#endregion registerPrompt_completion } +// --------------------------------------------------------------------------- +// Extension capabilities +// --------------------------------------------------------------------------- + +/** Example: Declare an extension capability with its settings. */ +function extensionCapabilities_register(server: McpServer) { + //#region extensionCapabilities_register + server.server.registerCapabilities({ + extensions: { 'com.example/feature-flags': { flags: ['dark-mode', 'beta-search'] } } + }); + //#endregion extensionCapabilities_register +} + // --------------------------------------------------------------------------- // Logging // --------------------------------------------------------------------------- @@ -599,6 +612,7 @@ void registerResource_static; void registerResource_template; void registerPrompt_basic; void registerPrompt_completion; +void extensionCapabilities_register; void streamableHttp_stateful; void streamableHttp_stateless; void streamableHttp_jsonResponse; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8df5ee1608..256377b6ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -539,6 +539,25 @@ importers: specifier: catalog:devTools version: 4.21.0 + examples/extension-capabilities: + dependencies: + '@mcp-examples/shared': + specifier: workspace:* + version: link:../shared + '@modelcontextprotocol/client': + specifier: workspace:* + version: link:../../packages/client + '@modelcontextprotocol/node': + specifier: workspace:* + version: link:../../packages/middleware/node + '@modelcontextprotocol/server': + specifier: workspace:* + version: link:../../packages/server + devDependencies: + tsx: + specifier: catalog:devTools + version: 4.21.0 + examples/gateway: dependencies: '@mcp-examples/shared':