From 69df98e5d773cbce0376cbc5cf16cd17744ddb0f Mon Sep 17 00:00:00 2001 From: Sravan Vadla <44364763+sravan-v@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:39:13 -0700 Subject: [PATCH] docs: validate input-required elicitation examples --- docs/servers/input-required.md | 21 ++++++++++++++----- .../guides/servers/input-required.examples.ts | 21 ++++++++++++++----- examples/mrtr/server.ts | 6 +++--- .../core-internal/src/shared/inputRequired.ts | 7 ++++--- .../server/test/server/inputRequired.test.ts | 2 +- .../server/legacyInputRequiredShim.test.ts | 2 +- test/conformance/src/everythingServer.ts | 11 ++++------ test/e2e/scenarios/mrtr.test.ts | 10 ++++----- 8 files changed, 50 insertions(+), 30 deletions(-) diff --git a/docs/servers/input-required.md b/docs/servers/input-required.md index 1fc42383f1..27139c7b79 100644 --- a/docs/servers/input-required.md +++ b/docs/servers/input-required.md @@ -14,6 +14,9 @@ const confirmationSchema = z.object({ confirm: z.boolean().meta({ title: 'Confirm deployment' }) }); +const cacheConfirmationSchema = z.object({ confirm: z.boolean() }); +const cacheScopeSchema = z.object({ scope: z.string() }); + server.registerTool( 'deploy', { @@ -181,13 +184,13 @@ server.registerTool( const state = ctx.mcpReq.requestState<{ step: string }>(); if (state?.step !== 'confirmed') { - const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); + const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', cacheConfirmationSchema); if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: 'Really wipe the cache?', - requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] } + requestedSchema: cacheConfirmationSchema }) } }); @@ -197,15 +200,23 @@ server.registerTool( inputRequests: { scope: inputRequired.elicit({ message: 'Which scope?', - requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] } + requestedSchema: cacheScopeSchema }) }, requestState: await stateCodec.mint({ step: 'confirmed' }) }); } - const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope'); - return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] }; + const scope = acceptedContent(ctx.mcpReq.inputResponses, 'scope', cacheScopeSchema); + if (scope === undefined) { + return inputRequired({ + inputRequests: { + scope: inputRequired.elicit({ message: 'Which scope?', requestedSchema: cacheScopeSchema }) + }, + requestState: await stateCodec.mint({ step: 'confirmed' }) + }); + } + return { content: [{ type: 'text', text: `Wiped ${scope.scope}` }] }; } ); ``` diff --git a/examples/guides/servers/input-required.examples.ts b/examples/guides/servers/input-required.examples.ts index 8b3755ca17..bfb4995408 100644 --- a/examples/guides/servers/input-required.examples.ts +++ b/examples/guides/servers/input-required.examples.ts @@ -40,6 +40,9 @@ const confirmationSchema = z.object({ confirm: z.boolean().meta({ title: 'Confirm deployment' }) }); +const cacheConfirmationSchema = z.object({ confirm: z.boolean() }); +const cacheScopeSchema = z.object({ scope: z.string() }); + server.registerTool( 'deploy', { @@ -132,13 +135,13 @@ server.registerTool( const state = ctx.mcpReq.requestState<{ step: string }>(); if (state?.step !== 'confirmed') { - const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); + const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', cacheConfirmationSchema); if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: 'Really wipe the cache?', - requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] } + requestedSchema: cacheConfirmationSchema }) } }); @@ -148,15 +151,23 @@ server.registerTool( inputRequests: { scope: inputRequired.elicit({ message: 'Which scope?', - requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] } + requestedSchema: cacheScopeSchema }) }, requestState: await stateCodec.mint({ step: 'confirmed' }) }); } - const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope'); - return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] }; + const scope = acceptedContent(ctx.mcpReq.inputResponses, 'scope', cacheScopeSchema); + if (scope === undefined) { + return inputRequired({ + inputRequests: { + scope: inputRequired.elicit({ message: 'Which scope?', requestedSchema: cacheScopeSchema }) + }, + requestState: await stateCodec.mint({ step: 'confirmed' }) + }); + } + return { content: [{ type: 'text', text: `Wiped ${scope.scope}` }] }; } ); //#endregion requestState_mint diff --git a/examples/mrtr/server.ts b/examples/mrtr/server.ts index 1f89fe236d..2eaa9b7bd1 100644 --- a/examples/mrtr/server.ts +++ b/examples/mrtr/server.ts @@ -30,7 +30,7 @@ import { acceptedContent, createMcpHandler, createRequestStateCodec, inputRequir import { serveStdio } from '@modelcontextprotocol/server/stdio'; import * as z from 'zod/v4'; -const CONFIRM_SCHEMA = { type: 'object' as const, properties: { confirm: { type: 'boolean' as const } }, required: ['confirm'] }; +const CONFIRM_SCHEMA = z.object({ confirm: z.boolean() }); type DeployState = { step: 'confirm' | 'signed-in'; env: string }; @@ -67,8 +67,8 @@ function buildServer(): McpServer { console.error(`[server] tools/call deploy(${env}) step=${step}`); if (step === 'confirm') { - const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); - if (!confirmed?.confirm) { + const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', CONFIRM_SCHEMA); + if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: `Deploy to ${env}?`, requestedSchema: CONFIRM_SCHEMA }) diff --git a/packages/core-internal/src/shared/inputRequired.ts b/packages/core-internal/src/shared/inputRequired.ts index b2736e23b6..bf518e39b0 100644 --- a/packages/core-internal/src/shared/inputRequired.ts +++ b/packages/core-internal/src/shared/inputRequired.ts @@ -113,13 +113,14 @@ function buildInputRequired(spec: InputRequiredSpec): InputRequiredResult { * @example Write-once tool requesting confirmation * ```ts * server.registerTool('deploy', { inputSchema: z.object({ env: z.string() }) }, async ({ env }, ctx) => { - * const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); - * if (!confirmed) { + * const confirmationSchema = z.object({ confirm: z.boolean() }); + * const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', confirmationSchema); + * if (confirmed?.confirm !== true) { * return inputRequired({ * inputRequests: { * confirm: inputRequired.elicit({ * message: `Deploy to ${env}?`, - * requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] } + * requestedSchema: confirmationSchema * }) * } * }); diff --git a/packages/server/test/server/inputRequired.test.ts b/packages/server/test/server/inputRequired.test.ts index 8dfae39b63..1f775d9a12 100644 --- a/packages/server/test/server/inputRequired.test.ts +++ b/packages/server/test/server/inputRequired.test.ts @@ -125,7 +125,7 @@ describe('input-required returns on the 2026-07-28 era', () => { { inputSchema: z.object({ env: z.string() }), outputSchema: z.object({ deployed: z.boolean() }) }, async ({ env }, ctx) => { const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', confirmationSchema); - if (!confirmed?.confirm) { + if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ diff --git a/packages/server/test/server/legacyInputRequiredShim.test.ts b/packages/server/test/server/legacyInputRequiredShim.test.ts index ce5a384df8..4c5f17af93 100644 --- a/packages/server/test/server/legacyInputRequiredShim.test.ts +++ b/packages/server/test/server/legacyInputRequiredShim.test.ts @@ -44,7 +44,7 @@ async function elicitingToolServer(options?: ConstructorParameters { seenResponses.push(ctx.mcpReq.inputResponses); const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', CONFIRM_SCHEMA); - if (!confirmed?.confirm) { + if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ diff --git a/test/conformance/src/everythingServer.ts b/test/conformance/src/everythingServer.ts index 425b4d6647..0639a4f45c 100644 --- a/test/conformance/src/everythingServer.ts +++ b/test/conformance/src/everythingServer.ts @@ -863,17 +863,14 @@ function createMcpServer() { inputSchema: z.object({}) }, async (_args, ctx): Promise => { - const confirmation = acceptedContent<{ ok: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); - if (confirmation === undefined) { + const confirmationSchema = z.object({ ok: z.boolean() }); + const confirmation = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', confirmationSchema); + if (confirmation?.ok !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: 'Please confirm', - requestedSchema: { - type: 'object', - properties: { ok: { type: 'boolean' } }, - required: ['ok'] - } + requestedSchema: confirmationSchema }) }, requestState: await requestStateCodec.mint({ tool: 'request_state', nonce: randomUUID() }) diff --git a/test/e2e/scenarios/mrtr.test.ts b/test/e2e/scenarios/mrtr.test.ts index 08887ac126..c1d0c15f74 100644 --- a/test/e2e/scenarios/mrtr.test.ts +++ b/test/e2e/scenarios/mrtr.test.ts @@ -43,14 +43,14 @@ async function allRecordedBytes(wired: Wired): Promise { return [...requests, ...responses].join('\n'); } -const CONFIRM_SCHEMA = { type: 'object' as const, properties: { confirm: { type: 'boolean' as const } }, required: ['confirm'] }; +const CONFIRM_SCHEMA = z.object({ confirm: z.boolean() }); verifies('typescript:mrtr:tools-call:write-once-roundtrip', async ({ transport }: TestArgs) => { const makeServer = () => { const server = new McpServer({ name: 'mrtr-server', version: '1.0.0' }, { capabilities: { tools: {} } }); server.registerTool('deploy', { inputSchema: z.object({ env: z.string() }) }, async ({ env }, ctx) => { - const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); - if (!confirmed?.confirm) { + const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', CONFIRM_SCHEMA); + if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: `Deploy to ${env}?`, requestedSchema: CONFIRM_SCHEMA }) }, requestState: 'opaque-deploy-state' @@ -487,8 +487,8 @@ verifies('typescript:mrtr:legacy-shim:write-once-on-2025', async ({ transport }: // legacy shim converts the embedded request into a real // elicitation/create over the session and re-enters the handler. server.registerTool('deploy', { inputSchema: z.object({ env: z.string() }) }, async ({ env }, ctx) => { - const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); - if (!confirmed?.confirm) { + const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', CONFIRM_SCHEMA); + if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: `Deploy to ${env}?`, requestedSchema: CONFIRM_SCHEMA }) }, requestState: 'shim-opaque-state'