Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
16 changes: 16 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,22 @@
);
```

## 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.

Check warning on line 373 in docs/server.md

View check run for this annotation

Claude / Claude Code Review

Extension capabilities prose: spec link targets latest instead of draft, and settings values are objects not arbitrary JSON

The new "Extension capabilities" section links "MCP extensions" to `specification/latest`, but the `capabilities.extensions` key 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 at `specification/draft` per 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", sinc
Comment on lines +363 to +373

Copy link
Copy Markdown

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 the capabilities.extensions key 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 at specification/draft per 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, the extensions capability 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.tsClientCapabilities/ServerCapabilities have no extensions member (the only "extension" hits are unrelated comments about unofficial params extensions).
  • 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 the Wire2025*Capabilities types omit the key.

Since 2026-07-28 is the draft revision (the docs themselves call it that throughout), the specification/latest alias 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 extensions capability map or the identifier-naming rule the paragraph references → has no spec anchor for the feature. Every other specification/latest link in docs/ (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 via specification/draft (e.g. docs/server.md lines ~380/486/586 and docs/client.md ~594/625/672). Fix: change the link to specification/draft/basic/lifecycle#capability-negotiation, or keep latest and 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: ServerCapabilitiesSchema declares extensions: z.record(z.string(), JSONObjectSchema) in both packages/core-internal/src/wire/rev2025-11-25/schemas.ts:492 and rev2026-07-28/schemas.ts:254, and JSONObjectSchema is z.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.


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]
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
12 changes: 12 additions & 0 deletions examples/extension-capabilities/README.md
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
```
36 changes: 36 additions & 0 deletions examples/extension-capabilities/client.ts
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();
22 changes: 22 additions & 0 deletions examples/extension-capabilities/package.json
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."
}
}
37 changes: 37 additions & 0 deletions examples/extension-capabilities/server.ts
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`);
});
}
16 changes: 16 additions & 0 deletions examples/guides/clientGuide.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions examples/guides/serverGuide.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading