Skip to content
Open
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
21 changes: 16 additions & 5 deletions docs/servers/input-required.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down Expand Up @@ -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
})
}
});
Expand All @@ -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}` }] };
}
);
```
Expand Down
21 changes: 16 additions & 5 deletions examples/guides/servers/input-required.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down Expand Up @@ -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
})
}
});
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions examples/mrtr/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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 })
Expand Down
7 changes: 4 additions & 3 deletions packages/core-internal/src/shared/inputRequired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* })
* }
* });
Expand Down
2 changes: 1 addition & 1 deletion packages/server/test/server/inputRequired.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async function elicitingToolServer(options?: ConstructorParameters<typeof McpSer
server.registerTool('deploy', { inputSchema: z.object({ env: z.string() }) }, async ({ env }, ctx) => {
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({
Expand Down
11 changes: 4 additions & 7 deletions test/conformance/src/everythingServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,17 +863,14 @@ function createMcpServer() {
inputSchema: z.object({})
},
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
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() })
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/scenarios/mrtr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ async function allRecordedBytes(wired: Wired): Promise<string> {
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'
Expand Down Expand Up @@ -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'
Expand Down
Loading