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.
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.
An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request.