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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
12 changes: 12 additions & 0 deletions .changeset/mrtr-client-engine.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral `InputRequest`/`InputResponse`/`InputRequests`/`InputResponses`/`InputRequiredResult` types and the `isInputRequiredResult()` guard ship as the neutral surface (the
`inputRequired()` builder family and the `acceptedContent()` reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the
client, an `input_required` answer to `tools/call`, `prompts/get`, or `resources/read` on a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the
original call is retried with the collected `inputResponses`, a byte-exact echo of the opaque `requestState`, and a fresh request id, up to `inputRequired.maxRounds` rounds (default 10; exhaustion raises a typed `InputRequiredRoundsExceeded` error carrying the last result).
`client.callTool()` and its siblings keep returning their plain result types. `ClientOptions.inputRequired` (`autoFulfill`, `maxRounds`) configures the driver; manual mode is `autoFulfill: false` plus the per-call `allowInputRequired: true` request option and the
`withInputRequired()` schema wrapper. Retried requests surface their `inputResponses` to server handlers as bare response objects — entries in a wrapped `{method, result}` shape are dropped and reported via `ctx.mcpReq.droppedInputResponseKeys`. 2025-era behavior is unchanged:
the legacy wire has no `input_required` vocabulary and the legacy server-to-client request flow is untouched.
116 changes: 104 additions & 12 deletions packages/client/src/client/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import type {
GetPromptRequest,
GetPromptResult,
Implementation,
InputRequiredOptions,
JSONRPCNotification,
JSONRPCRequest,
JsonSchemaType,
Expand All@@ -31,14 +32,17 @@ import type {
ListToolsResult,
LoggingLevel,
MessageExtraInfo,
NonCompleteResultFlow,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
ReadResourceResult,
RequestMethod,
RequestOptions,
ResolvedInputRequiredDriverConfig,
Result,
ServerCapabilities,
StandardSchemaV1,
SubscribeRequest,
Tool,
Transport,
Expand All@@ -59,6 +63,8 @@ import {
Protocol,
ProtocolError,
ProtocolErrorCode,
resolveInputRequiredDriverConfig,
runInputRequiredFlow,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
Expand DownExpand Up@@ -178,6 +184,31 @@ export type ClientOptions = ProtocolOptions & {
*/
versionNegotiation?: VersionNegotiationOptions;

/**
* Multi-round-trip auto-fulfilment (protocol revision 2026-07-28).
*
* On the 2026-07-28 era, servers obtain client input (elicitation,
* sampling, roots) by answering `tools/call`, `prompts/get`, or
* `resources/read` with an `input_required` result instead of sending a
* server→client request. By default the client fulfils those embedded
* requests automatically through the SAME handlers registered via
* {@linkcode Client.setRequestHandler | setRequestHandler} (e.g.
* `elicitation/create`), then retries the original call with the
* collected `inputResponses` and a byte-exact echo of the opaque
* `requestState`, on a fresh request id, up to `maxRounds` rounds.
* `client.callTool()` (and its siblings) keep returning their plain
* result type — the interactive rounds happen inside the call.
*
* Set `autoFulfill: false` for manual mode: an `input_required` response
* then surfaces as a typed error unless the individual call passes
* `allowInputRequired: true` (pair it with `withInputRequired()` on the
* explicit-schema path to type both outcomes).
*
* Has no effect on 2025-era connections, which have no `input_required`
* vocabulary.
*/
inputRequired?: InputRequiredOptions;

/**
* Configure handlers for list changed notifications (tools, prompts, resources).
*
Expand DownExpand Up@@ -253,6 +284,7 @@ export class Client extends Protocol<ClientContext> {
private _enforceStrictCapabilities: boolean;
private _versionNegotiation?: VersionNegotiationOptions;
private _supportedProtocolVersionsOption?: string[];
private _inputRequiredDriverConfig: ResolvedInputRequiredDriverConfig;

/**
* Initializes this client with the given name and version information.
Expand All@@ -267,6 +299,9 @@ export class Client extends Protocol<ClientContext> {
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
this._versionNegotiation = options?.versionNegotiation;
this._supportedProtocolVersionsOption = options?.supportedProtocolVersions;
// Multi-round-trip auto-fulfilment driver (2026-07-28): on by default,
// configurable via ClientOptions.inputRequired.
this._inputRequiredDriverConfig = resolveInputRequiredDriverConfig(options?.inputRequired);
Comment thread
felixweinberger marked this conversation as resolved.

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
Expand DownExpand Up@@ -299,6 +334,42 @@ export class Client extends Protocol<ClientContext> {
return undefined;
}

/**
* Wires the multi-round-trip auto-fulfilment engine (protocol revision
* 2026-07-28) into the response funnel: an `input_required` answer is
* fulfilled through the registered elicitation/sampling/roots handlers
* and the original request retried via `flow.retry`, up to
* `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the
* response surfaces as a typed error steering to manual mode.
*/
protected override _resolveNonCompleteResult<T extends StandardSchemaV1>(
decoded: { kind: 'input_required'; inputRequests: Record<string, unknown>; requestState?: string },
flow: NonCompleteResultFlow<T>
): Promise<unknown> {
if (!this._inputRequiredDriverConfig.autoFulfill) {
return Promise.reject(
new SdkError(
SdkErrorCode.UnsupportedResultType,
`Unsupported result type 'input_required' for ${flow.request.method}: ` +
`multi-round-trip auto-fulfilment is not enabled on this instance — ` +
`pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`,
{ resultType: 'input_required', method: flow.request.method }
)
);
}
return runInputRequiredFlow(
{
getRequestHandler: method =>
this._getRequestHandler(method) as ((request: JSONRPCRequest, ctx: unknown) => Promise<Result>) | undefined,
buildContext: baseCtx => this.buildContext(baseCtx, undefined),
sessionId: this.transport?.sessionId
},
this._inputRequiredDriverConfig,
decoded,
flow
);
}

/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
Expand DownExpand Up@@ -352,14 +423,16 @@ export class Client extends Protocol<ClientContext> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
// Era-exact validation: the schemas are resolved from the
// instance era at dispatch time (the era gate guarantees the
// method exists on the serving era before we get here).
// instance era at dispatch time. On the 2025 era the method
// is a wire request (registry schemas); on the 2026 era it is
// in-band vocabulary reached only via the multi-round-trip
// driver, so the in-band schemas apply.
const codec = codecForVersion(this._negotiatedProtocolVersion);
const elicitRequestSchema = codec.requestSchema('elicitation/create');
const elicitRequestSchema = codec.requestSchema('elicitation/create') ?? codec.inputRequestSchema('elicitation/create');
// The era registry entry IS the plain ElicitResult schema
// (the result map is aligned to the typed map — no widened
// unions), so no narrower surface is needed.
const elicitResultSchema = codec.resultSchema('elicitation/create');
const elicitResultSchema = codec.resultSchema('elicitation/create') ?? codec.inputResponseSchema('elicitation/create');
if (!elicitRequestSchema || !elicitResultSchema) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era');
}
Expand DownExpand Up@@ -416,9 +489,13 @@ export class Client extends Protocol<ClientContext> {

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
// Era-exact validation via the instance era (see above).
// Era-exact validation via the instance era (see above): wire
// request schema on the 2025 era, in-band schema on the 2026
// era (where sampling reaches the handler only as an embedded
// input request).
const codec = codecForVersion(this._negotiatedProtocolVersion);
const samplingRequestSchema = codec.requestSchema('sampling/createMessage');
const wireSamplingRequestSchema = codec.requestSchema('sampling/createMessage');
const samplingRequestSchema = wireSamplingRequestSchema ?? codec.inputRequestSchema('sampling/createMessage');
if (!samplingRequestSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
Expand All@@ -436,13 +513,28 @@ export class Client extends Protocol<ClientContext> {

const result = await handler(request, ctx);

// The result schema depends on the REQUEST params (tools vs
// no tools) — something a method-keyed registry entry cannot
// express, so the pair is picked here. The era gate keeps
// this era-correct: sampling/createMessage is only ever
// dispatched on an era whose registry defines it.
// The result-side schema mirrors the request-side selection so
// both stay on the same era's vocabulary. On the 2025 era the
// schema depends on the REQUEST params (tools vs no tools) —
// something a method-keyed registry entry cannot express, so
// the pair is picked here. When the request schema came from
// the in-band fallback (2026 era, where sampling reaches the
// handler only as an embedded input request), the embedded
// response schema applies — it covers plain and tool-bearing
// responses alike.
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const resultSchema =
wireSamplingRequestSchema === undefined
? codec.inputResponseSchema('sampling/createMessage')
: hasTools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
if (!resultSchema) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
'No result schema for sampling/createMessage in the resolved era'
);
}
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,5 +75,11 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
export { fromJsonSchema } from './fromJsonSchema.js';

// Multi-round-trip requests (protocol revision 2026-07-28): the client-side
// auto-fulfilment knobs (ClientOptions.inputRequired) and the manual-mode
// schema wrapper for callers that opt out of auto-fulfilment per call.
export type { InputRequiredOptions } from '@modelcontextprotocol/core';
export { withInputRequired } from '@modelcontextprotocol/core';

// re-export curated public API from core
export * from '@modelcontextprotocol/core/public';
Loading
Loading