Skip to main content
Glama
ask-question.ts5.31 kB
import path from 'node:path'; import {fileURLToPath} from 'node:url'; import type {McpServer, ToolCallback} from '@modelcontextprotocol/sdk/server/mcp.js'; import {z} from 'zod'; import {ErrorCode, McpError} from '@modelcontextprotocol/sdk/types.js'; import type {ToolConfig, Questionnaire, QuestionnaireResponse} from '../types.js'; import {TerminalSpawner} from '../terminal/spawn-terminal.js'; // Zod schema for question option validation const questionOptionSchema = z.object({ id: z.string().describe('Unique identifier for the option'), label: z.string().describe('Display text for the option'), description: z.string().optional().describe('Optional description for the option'), }); // Zod schema for open question validation const questionOpenSchema = z.object({ type: z.literal('open'), id: z.string().describe('Unique identifier for the question'), prompt: z.string().describe('The question prompt text to display to the user'), description: z.string().optional().describe('Optional description or additional context'), placeholder: z.string().optional().describe('Placeholder text to show in the input field'), minLength: z.number().int().min(0).optional().describe('Minimum length for the answer'), maxLength: z.number().int().min(1).optional().describe('Maximum length for the answer'), }); // Zod schema for multiple choice question validation const questionMultipleChoiceSchema = z.object({ type: z.literal('multiple_choice'), id: z.string().describe('Unique identifier for the question'), prompt: z.string().describe('The question prompt text to display to the user'), description: z.string().optional().describe('Optional description or additional context'), options: z.array(questionOptionSchema).min(1).describe('Array of available choices'), defaultOptionID: z.string().optional().describe('ID of the option selected by default'), allowMultiple: z.boolean().optional().describe('Whether multiple selections are allowed'), minSelections: z.number().int().min(1).optional().describe('Minimum number of selections required'), maxSelections: z.number().int().min(1).optional().describe('Maximum number of selections allowed'), allowOwnVariant: z .boolean() .optional() .describe('Whether to allow user to provide their own variant/answer (default: true)'), }); // Union schema for all question types const questionSchema = z.union([questionOpenSchema, questionMultipleChoiceSchema]); // Questionnaire input schema export const ASK_QUESTION_INPUT_SCHEMA = { questions: z.array(questionSchema).min(1).max(10).describe('Array of questions to ask the user'), }; /** * Ask Question Tool * Allows agents to ask users questions through terminal interface */ export class AskQuestionTool { readonly config: ToolConfig<typeof ASK_QUESTION_INPUT_SCHEMA, never> = { description: 'Interactive user questionnaire system that spawns a terminal window for direct user input. ' + 'Supports open-ended text questions and multiple choice questions with customizable options. ' + 'Handles validation, defaults, and multi-selection scenarios. ' + 'Returns structured responses with user input and metadata. ' + 'Great to use when you need to ask a question to the user and get a response in situations of unclear context or decision making.', inputSchema: ASK_QUESTION_INPUT_SCHEMA, annotations: { title: 'Ask Question', readOnlyHint: false, }, }; private readonly questionnaireWorkloadPath: string; constructor() { // Get the path to the questionnaire workload const currentDir = path.dirname(fileURLToPath(import.meta.url)); this.questionnaireWorkloadPath = path.join(currentDir, '../ui/runner.js'); } get name() { return 'ask-question'; } register(srv: McpServer) { srv.registerTool(this.name, this.config, this.#handle); } /** * Handle the ask question tool request */ readonly #handle: ToolCallback<typeof ASK_QUESTION_INPUT_SCHEMA> = async (input) => { // Validate input and create questionnaire const questionnaire: Questionnaire = { questions: input.questions, }; // Create terminal spawner with questionnaire workload const spawner = new TerminalSpawner<Questionnaire, QuestionnaireResponse>({ scriptPath: this.questionnaireWorkloadPath, inputData: questionnaire, ttlMs: 300_000, // 5 minutes timeout }); // Execute questionnaire through terminal interface const result = await spawner.spawn().catch((error: unknown) => { throw new McpError(ErrorCode.InternalError, `Failed to spawn terminal for questionnaire.\n${String(error)}`); }); // Check if questionnaire was cancelled or timed out if (result.timedOut) { throw new McpError(ErrorCode.RequestTimeout, 'Questionnaire timed out waiting for user input.'); } if (!result.isSuccess) { throw new McpError(ErrorCode.InternalError, `Questionnaire failed for unknown reason.\n ${String(result.error)}`); } // Check if user cancelled if (result.output.cancelled) { throw new McpError(ErrorCode.InvalidRequest, 'Questionnaire was cancelled by the user.'); } // Return successful response return { content: [ { type: 'text', text: 'Questionnaire completed successfully.', }, { type: 'text', text: JSON.stringify(result.output, null, 2), }, ], }; }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ver0-project/mcps'

If you have feedback or need assistance with the MCP directory API, please join our Discord server