UNPKG

@modelcontextprotocol/server-everything

Version:

MCP server that exercises all the features of the MCP protocol

241 lines (240 loc) • 8.67 kB
// The `trigger-elicitation-request` tool: asks the user to fill in a form // that exercises every field type form-mode elicitation supports, and // summarizes the answer. // // Written once for both protocol eras with a multi-round-trip request // (#4852): the first call returns `input_required` carrying an // `elicitation/create` request, the client shows the form and retries with // the answer, and the retry summarizes it. A 2026-07-28 client does that // in-band; for a 2025-era client the SDK's legacy shim sends a real // `elicitation/create` request and re-enters the handler with the answer. import { inputRequired, inputResponse, } from "@modelcontextprotocol/server"; import { TOOL_ICONS } from "../server/icons.js"; // Tool configuration const name = "trigger-elicitation-request"; const config = { title: "Trigger Elicitation Request Tool", description: "Trigger a Request from the Server for User Elicitation", inputSchema: {}, icons: TOOL_ICONS, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }; /** The form's fields: one of each type form-mode elicitation supports. */ const properties = { name: { title: "String", type: "string", description: "Your full, legal name", }, check: { title: "Boolean", type: "boolean", description: "Agree to the terms and conditions", }, firstLine: { title: "String with default", type: "string", description: "Favorite first line of a story", default: "It was a dark and stormy night.", }, email: { title: "String with email format", type: "string", format: "email", description: "Your email address (will be verified, and never shared with anyone else)", }, homepage: { type: "string", format: "uri", title: "String with uri format", description: "Portfolio / personal website", }, birthdate: { title: "String with date format", type: "string", format: "date", description: "Your date of birth", }, integer: { title: "Integer", type: "integer", description: "Your favorite integer (do not give us your phone number, pin, or other sensitive info)", minimum: 1, maximum: 100, default: 42, }, number: { title: "Number in range 1-1000", type: "number", description: "Favorite number (there are no wrong answers)", minimum: 0, maximum: 1000, default: 3.14, }, untitledSingleSelectEnum: { type: "string", title: "Untitled Single Select Enum", description: "Choose your favorite friend", enum: ["Monica", "Rachel", "Joey", "Chandler", "Ross", "Phoebe"], default: "Monica", }, untitledMultipleSelectEnum: { type: "array", title: "Untitled Multiple Select Enum", description: "Choose your favorite instruments", minItems: 1, maxItems: 3, items: { type: "string", enum: ["Guitar", "Piano", "Violin", "Drums", "Bass"], }, default: ["Guitar"], }, titledSingleSelectEnum: { type: "string", title: "Titled Single Select Enum", description: "Choose your favorite hero", oneOf: [ { const: "hero-1", title: "Superman" }, { const: "hero-2", title: "Green Lantern" }, { const: "hero-3", title: "Wonder Woman" }, ], default: "hero-1", }, titledMultipleSelectEnum: { type: "array", title: "Titled Multiple Select Enum", description: "Choose your favorite types of fish", minItems: 1, maxItems: 3, items: { anyOf: [ { const: "fish-1", title: "Tuna" }, { const: "fish-2", title: "Salmon" }, { const: "fish-3", title: "Trout" }, ], }, default: ["fish-1"], }, legacyTitledEnum: { type: "string", title: "Legacy Titled Single Select Enum", description: "Choose your favorite type of pet", enum: ["pet-1", "pet-2", "pet-3", "pet-4", "pet-5"], enumNames: ["Cats", "Dogs", "Birds", "Fish", "Reptiles"], default: "pet-1", }, }; /** The form, as the elicitation request carries it. */ const requestedSchema = { type: "object", properties, required: ["name"], }; /** * The label each field gets in the summary of an accepted form. Keyed by the * form's own field names, so a field added to or removed from the form fails * to compile until its label follows, and the summary cannot drift from the * form again (#5209). */ const summaryLabels = { name: "Name", check: "Agreed to terms", firstLine: "Favorite first line", email: "Email", homepage: "Homepage", birthdate: "Birthdate", integer: "Favorite integer", number: "Favorite number", untitledSingleSelectEnum: "Favorite friend", untitledMultipleSelectEnum: "Favorite instruments", titledSingleSelectEnum: "Favorite hero", titledMultipleSelectEnum: "Favorite fish", legacyTitledEnum: "Favorite pet", }; /** * One summary line per form field the user answered, in the form's order. * A field the form does not define is left out, whatever the client sent. */ const summarize = (userData) => Object.keys(properties) .filter((field) => userData[field] !== undefined) .map((field) => { const value = userData[field]; const shown = Array.isArray(value) ? value.join(", ") : String(value); return `- ${summaryLabels[field]}: ${shown}`; }); /** * Registers the 'trigger-elicitation-request' tool. * * The tool needs a client that supports form-mode elicitation. On a 2025-era * connection the server factory registers it only for a client that declared * `elicitation.form`, or an `elicitation` object with neither `form` nor * `url`, which the spec treats as form mode for backwards compatibility; a * client that declared only `elicitation.url` does not get it (#4985). On * 2026-07-28 the SDK refuses the embedded request (-32021) of a client whose * request did not declare form-mode elicitation. * * The first round sends the form. The retry reads the user's response, which * may be acceptance with content, decline, or cancellation of the dialog, and * returns a structured summary with the raw result for debugging. * * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerElicitationRequestTool = (server) => { server.registerTool(name, config, async (_args, ctx) => { const answer = inputResponse(ctx.mcpReq.inputResponses, "form"); // First round: show the user the form if (answer.kind !== "elicit") { return inputRequired({ inputRequests: { form: inputRequired.elicit({ message: "Please provide inputs for the following fields:", requestedSchema, }), }, }); } // The answer as the client sent it, for the debugging output const rawResult = { action: answer.action, ...(answer.content !== undefined && { content: answer.content }), }; // Handle different response actions const content = []; if (answer.action === "accept" && answer.content) { content.push({ type: "text", text: `✅ User provided the requested information!`, }); // Only access answer.content when action is accept const lines = summarize(answer.content); content.push({ type: "text", text: `User inputs:\n${lines.join("\n")}`, }); } else if (answer.action === "decline") { content.push({ type: "text", text: `❌ User declined to provide the requested information.`, }); } else if (answer.action === "cancel") { content.push({ type: "text", text: `⚠️ User cancelled the elicitation dialog.`, }); } // Include raw result for debugging content.push({ type: "text", text: `\nRaw result: ${JSON.stringify(rawResult, null, 2)}`, }); return { content }; }); };