From 71d924e2d419f15e75c6ada89cca6ff1e013f41d Mon Sep 17 00:00:00 2001 From: Peter Alexander Date: Sun, 7 Jun 2026 13:42:41 +0100 Subject: [PATCH 1/7] Extend Base64 sentinel encoding to the Mcp-Name header Tool and prompt names are only SHOULD-constrained to header-safe characters, so a name outside the safe set previously made the tool uncallable over Streamable HTTP: Mcp-Name is required, but no encoding was defined for it. - Allow the =?base64?...?= sentinel encoding (already defined for Mcp-Param-{Name} headers) for the Mcp-Name header value. - Require servers to decode encoded Mcp-Name and Mcp-Param-{Name} values before comparing them to the request body during server validation. --- .../basic/transports/streamable-http.mdx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/specification/draft/basic/transports/streamable-http.mdx b/docs/specification/draft/basic/transports/streamable-http.mdx index 5c382a276..43a042e41 100644 --- a/docs/specification/draft/basic/transports/streamable-http.mdx +++ b/docs/specification/draft/basic/transports/streamable-http.mdx @@ -265,6 +265,10 @@ the header per [Server Validation](#server-validation). These headers are **REQUIRED** for compliance. +If the `Mcp-Name` source value cannot be safely represented as a plain ASCII +header value, clients **MUST** encode it using the Base64 sentinel format +described in [Value Encoding](#value-encoding). + **`tools/call` request:** ```http @@ -445,10 +449,21 @@ representation with the following format: Mcp-Param-{Name}: =?base64?{Base64EncodedValue}?= ``` +The same encoding rule applies to the `Mcp-Name` header value. Tool and +prompt names are only **SHOULD**-constrained to header-safe characters, so a +name (or resource URI) outside the safe set is carried as: + +```text +Mcp-Name: =?base64?{Base64EncodedValue}?= +``` + The prefix `=?base64?` and suffix `?=` indicate that the value is Base64-encoded. These markers are case-sensitive and **MUST** appear exactly as shown (lowercase). Servers and intermediaries that need to inspect these -values **MUST** decode them accordingly. +values **MUST** decode them accordingly. In particular, servers **MUST** +decode an encoded `Mcp-Name` or `Mcp-Param-{Name}` value before comparing it +to the corresponding request body value during +[Server Validation](#server-validation). To avoid ambiguity, clients **MUST** also Base64-encode any plain-ASCII value that matches the sentinel pattern (i.e., starts with `=?base64?` @@ -575,6 +590,9 @@ Validation failure conditions include: - A required standard header (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`) is missing. - A header value does not match the corresponding request body value. + For headers that permit the Base64 sentinel encoding (`Mcp-Name` and + `Mcp-Param-{Name}`), servers **MUST** decode encoded values (see + [Value Encoding](#value-encoding)) before comparing them to the body value. - A header value contains invalid characters. From 201ee1485ea9f29504322f5a7962543d9227f420 Mon Sep 17 00:00:00 2001 From: Peter Alexander Date: Sun, 7 Jun 2026 13:42:48 +0100 Subject: [PATCH 2/7] Restrict x-mcp-header to statically reachable properties The x-mcp-header extraction rule was undefined for properties nested under array 'items', inside composition or conditional keywords (oneOf/anyOf/allOf/not, if/then/else), or behind $ref: such properties have no single static location in the call arguments. - x-mcp-header annotations are now only valid on properties reachable from the schema root via a chain consisting solely of 'properties' keys. Annotations anywhere else make the tool definition invalid, triggering the existing client rejection rules. - Define extraction as reading the instance value at the annotated property's exact path; if the value is absent, the header is omitted. - Mirror the rule in the Tool.inputSchema schema documentation. --- .../basic/transports/streamable-http.mdx | 19 ++++++++++++++++--- docs/specification/draft/server/tools.mdx | 13 +++++++++++-- schema/draft/schema.ts | 11 +++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/specification/draft/basic/transports/streamable-http.mdx b/docs/specification/draft/basic/transports/streamable-http.mdx index 43a042e41..eca785e9b 100644 --- a/docs/specification/draft/basic/transports/streamable-http.mdx +++ b/docs/specification/draft/basic/transports/streamable-http.mdx @@ -358,8 +358,19 @@ the header name `Mcp-Param-{name}`. string, boolean). Parameters with type `number` are not permitted. Integer values **MUST** be within the safe range for JavaScript (−253+1 to 253−1) -- **MAY** be applied to properties at any nesting depth within the - `inputSchema`, not only top-level properties +- **MUST** only be applied to properties that are _statically reachable_ + from the schema root: reachable via a chain consisting solely of + `properties` keys. The chain **MUST NOT** pass through `items` (or any + other array keyword), composition keywords (`oneOf`, `anyOf`, `allOf`, + `not`), conditional keywords (`if`/`then`/`else`), or `$ref`. Nested + object properties are permitted as long as every step in the chain is a + `properties` key. An `x-mcp-header` annotation anywhere else makes the + annotation — and thus the tool definition — invalid. + +Header extraction is defined as reading the instance value at the exact +property path of the annotated property (the chain of `properties` keys +leading to it). If no value is present at that path in the call arguments, +the header is omitted. Clients using the Streamable HTTP transport **MUST** reject tool definitions where any `x-mcp-header` value violates these constraints. Rejection means @@ -491,7 +502,9 @@ When constructing a `tools/call` request via HTTP transport, the client 2. Append the `Mcp-Method` header and, if applicable, `Mcp-Name` header to the request. 3. Inspect the tool's `inputSchema` for properties marked with - `x-mcp-header` and extract the value for each parameter. + `x-mcp-header` and extract the value at each annotated property's exact + property path, omitting the header when no value is present (see + [Schema Extension](#schema-extension)). 4. Encode the values according to the [Value Encoding](#value-encoding) rules. 5. Append a `Mcp-Param-{Name}: {Value}` header to the request. diff --git a/docs/specification/draft/server/tools.mdx b/docs/specification/draft/server/tools.mdx index 9b9d1668d..582f83c10 100644 --- a/docs/specification/draft/server/tools.mdx +++ b/docs/specification/draft/server/tools.mdx @@ -353,8 +353,17 @@ HTTP header. - **MUST** only be applied to parameters with primitive types (integer, string, boolean). Parameters with type `number` are not permitted. Integer values **MUST** be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−253+1 to 253−1) -- **MAY** be applied to properties at any nesting depth within the `inputSchema`, not - only top-level properties +- **MUST** only be applied to properties that are _statically reachable_ from the schema + root: reachable via a chain consisting solely of `properties` keys. The chain **MUST + NOT** pass through `items` (or any other array keyword), composition keywords + (`oneOf`, `anyOf`, `allOf`, `not`), conditional keywords (`if`/`then`/`else`), or + `$ref`. Nested object properties are permitted as long as every step in the chain is a + `properties` key. An `x-mcp-header` annotation anywhere else makes the annotation — + and thus the tool definition — invalid. + +Header extraction is defined as reading the instance value at the exact property path of +the annotated property (the chain of `properties` keys leading to it). If no value is +present at that path in the call arguments, the header is omitted. Clients using the Streamable HTTP transport **MUST** reject tool definitions where any `x-mcp-header` value violates these constraints. Rejection means the client **MUST** diff --git a/schema/draft/schema.ts b/schema/draft/schema.ts index 2aeafa59c..262529291 100644 --- a/schema/draft/schema.ts +++ b/schema/draft/schema.ts @@ -1898,6 +1898,17 @@ export interface Tool extends BaseMetadata, Icons { * (`if`/`then`/`else`), reference keywords (`$ref`, `$defs`, `$anchor`), and any other * standard validation or annotation keywords. * + * Property schemas may carry an `x-mcp-header` annotation to mirror the + * argument value into an HTTP header on the Streamable HTTP transport. The + * annotation is only valid on properties reachable from the schema root via + * a static chain of `properties` keys — not under `items`, not inside + * composition or conditional keywords (`oneOf`, `anyOf`, `allOf`, `not`, + * `if`/`then`/`else`), and not behind `$ref`. An `x-mcp-header` annotation + * anywhere else makes the annotation (and thus the tool definition) invalid. + * Extraction reads the instance value at that exact property path; if the + * value is absent, the header is omitted. See the Streamable HTTP transport + * specification for the full rules. + * * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. */ inputSchema: { $schema?: string; type: "object"; [key: string]: unknown }; From 43f8ea513a9ffb906651fa1d009d80e5a98098c9 Mon Sep 17 00:00:00 2001 From: Peter Alexander Date: Sun, 7 Jun 2026 13:43:04 +0100 Subject: [PATCH 3/7] Add elicitationComplete to the subscriptions/listen filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit notifications/elicitation/complete had no legal delivery channel: the HTTP GET stream is gone, response-stream notifications must relate to the in-flight request, and the subscriptions/listen filter had no field covering elicitation completion — while servers must not send notification types the client has not requested. - Add an opt-in elicitationComplete boolean to SubscriptionFilter; when true, the server may deliver notifications/elicitation/complete on that subscription's stream. - Document on ElicitationCompleteNotification that it is only sent to clients that opted in via elicitationComplete and is delivered on that subscription's stream (with the subscription ID in _meta). - Document the URL-mode elicitation flow: the client subscribes via subscriptions/listen with elicitationComplete: true, waits for the completion notification, then retries the original request. - Regenerate schema.json and the schema reference. --- .../draft/basic/patterns/subscriptions.mdx | 13 +++++++------ .../draft/client/elicitation.mdx | 16 +++++++++++++++- docs/specification/draft/schema.mdx | 19 +++++++++++++++---- .../elicitation-complete.json | 3 +++ schema/draft/schema.json | 8 ++++++-- schema/draft/schema.ts | 9 +++++++++ 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/docs/specification/draft/basic/patterns/subscriptions.mdx b/docs/specification/draft/basic/patterns/subscriptions.mdx index b85db7072..8f8d86de3 100644 --- a/docs/specification/draft/basic/patterns/subscriptions.mdx +++ b/docs/specification/draft/basic/patterns/subscriptions.mdx @@ -39,12 +39,13 @@ notification types the client has not explicitly requested. ### Notification Filter -| Field | Type | Description | -| ----------------------- | ---------- | ----------------------------------------------------------------- | -| `toolsListChanged` | `boolean` | Receive `notifications/tools/list_changed` when tools change | -| `promptsListChanged` | `boolean` | Receive `notifications/prompts/list_changed` when prompts change | -| `resourcesListChanged` | `boolean` | Receive `notifications/resources/list_changed` when list changes | -| `resourceSubscriptions` | `string[]` | Receive `notifications/resources/updated` for these resource URIs | +| Field | Type | Description | +| ----------------------- | ---------- | --------------------------------------------------------------------------------------- | +| `toolsListChanged` | `boolean` | Receive `notifications/tools/list_changed` when tools change | +| `promptsListChanged` | `boolean` | Receive `notifications/prompts/list_changed` when prompts change | +| `resourcesListChanged` | `boolean` | Receive `notifications/resources/list_changed` when list changes | +| `resourceSubscriptions` | `string[]` | Receive `notifications/resources/updated` for these resource URIs | +| `elicitationComplete` | `boolean` | Receive `notifications/elicitation/complete` for this client's out-of-band elicitations | All fields are optional. Omitting a field is equivalent to not subscribing to that notification type. diff --git a/docs/specification/draft/client/elicitation.mdx b/docs/specification/draft/client/elicitation.mdx index 3164f456d..f4586d17f 100644 --- a/docs/specification/draft/client/elicitation.mdx +++ b/docs/specification/draft/client/elicitation.mdx @@ -384,13 +384,23 @@ of band and the client is not aware of the outcome until and unless the server s Servers **MAY** send a `notifications/elicitation/complete` notification when an out-of-band interaction started by URL mode elicitation is completed. This allows clients to react programmatically if appropriate. +The notification is delivered on a +[`subscriptions/listen`](/specification/draft/basic/patterns/subscriptions) stream. A +client that wants to receive it opens (or already has) a `subscriptions/listen` +subscription with `elicitationComplete: true` in the notification filter, waits for the +completion notification, and then retries the original request. + Servers sending notifications: - **MUST** only send the notification to the client that initiated the elicitation request. +- **MUST** only send the notification on a `subscriptions/listen` stream whose filter + included `elicitationComplete: true`. - **MUST** include the `elicitationId` established in the original `elicitation/create` request. Clients: +- **MUST** subscribe via `subscriptions/listen` with `elicitationComplete: true` to + receive completion notifications. - **MUST** ignore notifications referencing unknown or already-completed IDs. - **MAY** wait for this notification to automatically retry requests that received an [`InputRequiredResult`](/specification/draft/basic/patterns/mrtr#inputrequiredresult) (echoing back its `requestState`), update the user interface, or otherwise continue an interaction. - **SHOULD** still provide manual controls that let the user retry or cancel the original request (or otherwise resume interacting with the client) if the notification never arrives. @@ -402,6 +412,9 @@ Clients: "jsonrpc": "2.0", "method": "notifications/elicitation/complete", "params": { + "_meta": { + "io.modelcontextprotocol/subscriptionId": "1" + }, "elicitationId": "550e8400-e29b-41d4-a716-446655440000" } } @@ -446,12 +459,13 @@ sequenceDiagram User-->>Client: Provide consent Client->>UserAgent: Open URL + Client->>Server: subscriptions/listen (elicitationComplete: true) Client->>Server: tools/call(id: 2, Accept Response, requestState)) Note over Server: Server uses requestState to discover url info.
It may need to block until the request is fulfilled. Note over User,UserAgent: User interaction UserAgent-->>Server: Interaction complete - Server-->>Client: notifications/elicitation/complete (optional) + Server-->>Client: notifications/elicitation/complete (optional,
on the subscriptions/listen stream) Note over Server: Continue processing with new information Server-->Client: Result(id: 2, result) diff --git a/docs/specification/draft/schema.mdx b/docs/specification/draft/schema.mdx index 7012731b8..2def9b710 100644 --- a/docs/specification/draft/schema.mdx +++ b/docs/specification/draft/schema.mdx @@ -776,7 +776,9 @@ the server has no prompts), it is omitted from this set.

### `ElicitationCompleteNotification` -
interface ElicitationCompleteNotification {
  jsonrpc: "2.0";
  method: "notifications/elicitation/complete";
  params: ElicitationCompleteNotificationParams;
}

An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request.

Example: Elicitation complete
{
"jsonrpc": "2.0",
"method": "notifications/elicitation/complete",
"params": {
"elicitationId": "550e8400-e29b-41d4-a716-446655440000"
}
}
+
interface ElicitationCompleteNotification {
  jsonrpc: "2.0";
  method: "notifications/elicitation/complete";
  params: ElicitationCompleteNotificationParams;
}

An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request.

This is only sent to clients that opted in via the elicitationComplete +field of a subscriptions/listen +request, and is delivered on that subscription's stream.

Example: Elicitation complete
{
"jsonrpc": "2.0",
"method": "notifications/elicitation/complete",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": "listen-1"
},
"elicitationId": "550e8400-e29b-41d4-a716-446655440000"
}
}
@@ -1368,9 +1370,10 @@ requested.

### `SubscriptionFilter` -
interface SubscriptionFilter {
  toolsListChanged?: boolean;
  promptsListChanged?: boolean;
  resourcesListChanged?: boolean;
  resourceSubscriptions?: string[];
}

The set of notification types a client may opt in to on a subscriptions/listen request.

Each notification type is opt-in; the server MUST NOT send +

interface SubscriptionFilter {
  toolsListChanged?: boolean;
  promptsListChanged?: boolean;
  resourcesListChanged?: boolean;
  resourceSubscriptions?: string[];
  elicitationComplete?: boolean;
}

The set of notification types a client may opt in to on a subscriptions/listen request.

Each notification type is opt-in; the server MUST NOT send notification types the client has not explicitly requested here.

Subscribe to notifications/resources/updated for these resource URIs. -Replaces the former resources/subscribe RPC.

+Replaces the former resources/subscribe RPC.

If true, receive notifications/elicitation/complete +for out-of-band (URL mode) elicitations initiated by this client.

@@ -1469,7 +1472,15 @@ if present).