diff --git a/src/everything/tools/index.ts b/src/everything/tools/index.ts index 1cbedb9d..bb950e0a 100644 --- a/src/everything/tools/index.ts +++ b/src/everything/tools/index.ts @@ -12,6 +12,7 @@ import { registerLongRunningOperationTool } from "./long-running-operation.js"; import { registerToggleLoggingTool } from "./toggle-logging.js"; import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates.js"; import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js"; +import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js"; /** * Register the tools with the MCP server. @@ -30,5 +31,6 @@ export const registerTools = (server: McpServer) => { registerLongRunningOperationTool(server); registerToggleLoggingTool(server); registerToggleSubscriberUpdatesTool(server); + registerTriggerElicitationRequestTool(server); registerTriggerSamplingRequestTool(server); }; diff --git a/src/everything/tools/trigger-elicitation-request.ts b/src/everything/tools/trigger-elicitation-request.ts new file mode 100644 index 00000000..cb112122 --- /dev/null +++ b/src/everything/tools/trigger-elicitation-request.ts @@ -0,0 +1,204 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ElicitResultSchema } from "@modelcontextprotocol/sdk/types.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.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: {}, +}; + +/** + * Registers the 'trigger-elicitation-request' tool within the provided McpServer instance. + * + * The registered tool performs the following operations: + * + * This tool sends a detailed request for the user to provide information based + * on a pre-defined schema of fields including text inputs, booleans, numbers, + * email, dates, etc. It uses validation and handles multiple possible outcomes + * from the user's response, such as acceptance with content, decline, or + * cancellation of the dialog. The process also ensures parsing and validating + * the elicitation input arguments at runtime. + * + * The tool resolves the elicitation dialog response into a structured result, + * which contains both user-submitted input data (if provided) and debugging + * information, including raw results. + * + * @param {McpServer} server - The MCP server instance to which the tool will be registered. + */ +export const registerTriggerElicitationRequestTool = (server: McpServer) => { + server.registerTool( + name, + config, + async (args, extra): Promise => { + const elicitationResult = await extra.sendRequest( + { + method: "elicitation/create", + params: { + message: "Please provide inputs for the following fields:", + requestedSchema: { + type: 'object', + 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', + } + }, + required: ['name'], + }, + }, + }, + ElicitResultSchema, + { timeout: 10 * 60 * 1000 /* 10 minutes */ } + ); + + // Handle different response actions + const content: CallToolResult["content"] = []; + + if (elicitationResult.action === "accept" && elicitationResult.content) { + content.push({ + type: "text", + text: `✅ User provided the requested information!`, + }); + + // Only access elicitationResult.content when action is accept + const userData = elicitationResult.content; + const lines = []; + if (userData.name) lines.push(`- Name: ${userData.name}`); + if (userData.check !== undefined) + lines.push(`- Agreed to terms: ${userData.check}`); + if (userData.color) lines.push(`- Favorite Color: ${userData.color}`); + if (userData.email) lines.push(`- Email: ${userData.email}`); + if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`); + if (userData.birthdate) + lines.push(`- Birthdate: ${userData.birthdate}`); + if (userData.integer !== undefined) + lines.push(`- Favorite Integer: ${userData.integer}`); + if (userData.number !== undefined) + lines.push(`- Favorite Number: ${userData.number}`); + if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`); + + content.push({ + type: "text", + text: `User inputs:\n${lines.join("\n")}`, + }); + } else if (elicitationResult.action === "decline") { + content.push({ + type: "text", + text: `❌ User declined to provide the requested information.`, + }); + } else if (elicitationResult.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(elicitationResult, null, 2)}`, + }); + + return { content }; + } + ); +};