From 3ce3386a54b416445c898826e6f9391d8f962082 Mon Sep 17 00:00:00 2001 From: mehmet turac Date: Fri, 24 Jul 2026 17:14:23 +0300 Subject: [PATCH] docs: validate input-required wipe-cache responses --- docs/servers/input-required.md | 24 +++++++++++++++---- .../guides/servers/input-required.examples.ts | 24 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/servers/input-required.md b/docs/servers/input-required.md index 1fc42383f1..4bf9116f29 100644 --- a/docs/servers/input-required.md +++ b/docs/servers/input-required.md @@ -174,6 +174,9 @@ Sampling and roots are deprecated as of protocol revision 2026-07-28 (SEP-2577) To run rounds in sequence, return an opaque `requestState` string alongside the requests. The client echoes it back byte-for-byte on the retry, and `ctx.mcpReq.requestState()` reads its decoded payload on re-entry. Mint it with the codec from the next section. ```ts source="../../examples/guides/servers/input-required.examples.ts#requestState_mint" +const wipeCacheConfirmationSchema = z.object({ confirm: z.boolean() }); +const wipeCacheScopeSchema = z.object({ scope: z.string() }); + server.registerTool( 'wipe-cache', { description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) }, @@ -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', wipeCacheConfirmationSchema); 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: wipeCacheConfirmationSchema }) } }); @@ -197,15 +200,26 @@ server.registerTool( inputRequests: { scope: inputRequired.elicit({ message: 'Which scope?', - requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] } + requestedSchema: wipeCacheScopeSchema }) }, 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', wipeCacheScopeSchema); + if (scope === undefined) { + return inputRequired({ + inputRequests: { + scope: inputRequired.elicit({ + message: 'Which scope?', + requestedSchema: wipeCacheScopeSchema + }) + }, + 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..cfc93066fa 100644 --- a/examples/guides/servers/input-required.examples.ts +++ b/examples/guides/servers/input-required.examples.ts @@ -125,6 +125,9 @@ server.registerTool( // "Carry state across rounds with `requestState`" — two sequential rounds. //#region requestState_mint +const wipeCacheConfirmationSchema = z.object({ confirm: z.boolean() }); +const wipeCacheScopeSchema = z.object({ scope: z.string() }); + server.registerTool( 'wipe-cache', { description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) }, @@ -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', wipeCacheConfirmationSchema); 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: wipeCacheConfirmationSchema }) } }); @@ -148,15 +151,26 @@ server.registerTool( inputRequests: { scope: inputRequired.elicit({ message: 'Which scope?', - requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] } + requestedSchema: wipeCacheScopeSchema }) }, 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', wipeCacheScopeSchema); + if (scope === undefined) { + return inputRequired({ + inputRequests: { + scope: inputRequired.elicit({ + message: 'Which scope?', + requestedSchema: wipeCacheScopeSchema + }) + }, + requestState: await stateCodec.mint({ step: 'confirmed' }) + }); + } + return { content: [{ type: 'text', text: `Wiped ${scope.scope}` }] }; } ); //#endregion requestState_mint