Skip to main content
Glama

request_user_input

Prompt users for input to clarify requirements, validate assumptions, or confirm actions before proceeding. Ensures accuracy and avoids errors by resolving uncertainties through direct user interaction.

Instructions

Input Schema

NameRequiredDescriptionDefault
messageYesThe specific question for the user (appears in the prompt)
predefinedOptionsNoPredefined options for the user to choose from (optional)
projectNameYesIdentifies the context/project making the request (used in prompt formatting)

Input Schema (JSON Schema)

{ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "message": { "description": "The specific question for the user (appears in the prompt)", "type": "string" }, "predefinedOptions": { "description": "Predefined options for the user to choose from (optional)", "items": { "type": "string" }, "type": "array" }, "projectName": { "description": "Identifies the context/project making the request (used in prompt formatting)", "type": "string" } }, "required": [ "projectName", "message" ], "type": "object" }

Implementation Reference

  • The main handler function for the 'request_user_input' tool. It destructures the arguments, calls the helper getCmdWindowInput to prompt the user, handles timeout and empty responses, and returns the appropriate content block.
    async (args) => { // Use inferred args type const { projectName, message, predefinedOptions } = args; const promptMessage = `${projectName}: ${message}`; const answer = await getCmdWindowInput( projectName, promptMessage, globalTimeoutSeconds, true, predefinedOptions, ); // Check for the specific timeout indicator if (answer === '__TIMEOUT__') { return { content: [ { type: 'text', text: 'User did not reply: Timeout occurred.' }, ], }; } // Empty string means user submitted empty input, non-empty is actual reply else if (answer === '') { return { content: [{ type: 'text', text: 'User replied with empty input.' }], }; } else { const reply = `User replied: ${answer}`; return { content: [{ type: 'text', text: reply }] }; } },
  • Zod schema definition (raw shape) for the tool parameters: projectName, message, and optional predefinedOptions. This is used in the server.tool registration.
    const rawSchema: z.ZodRawShape = { projectName: z .string() .describe( 'Identifies the context/project making the request (used in prompt formatting)', ), message: z .string() .describe('The specific question for the user (appears in the prompt)'), predefinedOptions: z .array(z.string()) .optional() .describe('Predefined options for the user to choose from (optional)'), };
  • src/index.ts:111-151 (registration)
    Registration of the 'request_user_input' tool on the MCP server using server.tool(), conditionally based on enablement. Includes dynamic description resolution and schema.
    if (isToolEnabled('request_user_input')) { // Use properties from the imported tool object server.tool( 'request_user_input', // Need to handle description potentially being a function typeof requestUserInputTool.description === 'function' ? requestUserInputTool.description(globalTimeoutSeconds) : requestUserInputTool.description, requestUserInputTool.schema, // Use schema property async (args) => { // Use inferred args type const { projectName, message, predefinedOptions } = args; const promptMessage = `${projectName}: ${message}`; const answer = await getCmdWindowInput( projectName, promptMessage, globalTimeoutSeconds, true, predefinedOptions, ); // Check for the specific timeout indicator if (answer === '__TIMEOUT__') { return { content: [ { type: 'text', text: 'User did not reply: Timeout occurred.' }, ], }; } // Empty string means user submitted empty input, non-empty is actual reply else if (answer === '') { return { content: [{ type: 'text', text: 'User replied with empty input.' }], }; } else { const reply = `User replied: ${answer}`; return { content: [{ type: 'text', text: reply }] }; } }, ); }
  • Core helper function that implements the user input prompt via spawning a platform-specific UI process (Terminal on macOS, cmd on Windows/Linux), using temp files for communication, heartbeat for liveness, and timeout handling. Called by the tool handler.
    export async function getCmdWindowInput( projectName: string, promptMessage: string, timeoutSeconds: number = USER_INPUT_TIMEOUT_SECONDS, // Use constant as default showCountdown: boolean = true, predefinedOptions?: string[], ): Promise<string> { // Create a temporary file for the detached process to write to const sessionId = crypto.randomBytes(8).toString('hex'); const tempDir = os.tmpdir(); const tempFilePath = path.join(tempDir, `cmd-ui-response-${sessionId}.txt`); const heartbeatFilePath = path.join( tempDir, `cmd-ui-heartbeat-${sessionId}.txt`, ); const optionsFilePath = path.join( tempDir, `cmd-ui-options-${sessionId}.json`, ); // New options file path return new Promise<string>((resolve) => { // Wrap the async setup logic in an IIFE void (async () => { // Path to the UI script (will be in the same directory after compilation) const uiScriptPath = path.join(__dirname, 'ui.js'); // Gather options const options = { projectName, prompt: promptMessage, timeout: timeoutSeconds, showCountdown, sessionId, outputFile: tempFilePath, heartbeatFile: heartbeatFilePath, // Pass heartbeat file path too predefinedOptions, }; let ui; // Moved setup into try block try { // Write options to the file before spawning await fsPromises.writeFile( optionsFilePath, JSON.stringify(options), 'utf8', ); // Platform-specific spawning const platform = os.platform(); if (platform === 'darwin') { // macOS const escapedScriptPath = uiScriptPath; const escapedSessionId = sessionId; // Only need sessionId now // Construct the command string directly for the shell. Quotes handle paths with spaces. // Pass only the sessionId const nodeCommand = `exec node "${escapedScriptPath}" "${escapedSessionId}"; exit 0`; // Escape the node command for osascript's AppleScript string: const escapedNodeCommand = nodeCommand .replace(/\\/g, '\\\\') // Escape backslashes .replace(/"/g, '\\"'); // Escape double quotes // Activate Terminal first, then do script with exec const command = `osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "${escapedNodeCommand}"'`; const commandArgs: string[] = []; ui = spawn(command, commandArgs, { stdio: ['ignore', 'ignore', 'ignore'], shell: true, detached: true, }); } else if (platform === 'win32') { // Windows // Pass only the sessionId ui = spawn('node', [uiScriptPath, sessionId], { stdio: ['ignore', 'ignore', 'ignore'], shell: true, detached: true, windowsHide: false, }); } else { // Linux or other // Pass only the sessionId ui = spawn('node', [uiScriptPath, sessionId], { stdio: ['ignore', 'ignore', 'ignore'], shell: true, detached: true, }); } let watcher: FSWatcher | null = null; let timeoutHandle: NodeJS.Timeout | null = null; let heartbeatInterval: NodeJS.Timeout | null = null; let heartbeatFileSeen = false; // Track if we've ever seen the heartbeat file const startTime = Date.now(); // Record start time for initial grace period // Define cleanupAndResolve inside the promise scope const cleanupAndResolve = async (response: string) => { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } if (watcher) { watcher.close(); watcher = null; } if (timeoutHandle) { clearTimeout(timeoutHandle); timeoutHandle = null; } // Pass optionsFilePath to cleanupResources await cleanupResources( heartbeatFilePath, tempFilePath, optionsFilePath, ); resolve(response); }; // Listen for process exit events - moved definition before IIFE start const handleExit = (code?: number | null) => { // If the process exited with a non-zero code and watcher/timeout still exist if (code !== 0 && (watcher || timeoutHandle)) { void cleanupAndResolve(''); } }; const handleError = () => { if (watcher || timeoutHandle) { // Only cleanup if not already cleaned up void cleanupAndResolve(''); } }; ui.on('exit', handleExit); ui.on('error', handleError); // Unref the child process so the parent can exit independently ui.unref(); // Create an empty temp file before watching for user response await fsPromises.writeFile(tempFilePath, '', 'utf8'); // Use renamed import // Wait briefly for the heartbeat file to potentially be created await new Promise((res) => setTimeout(res, 500)); // Watch for content being written to the temp file watcher = watch(tempFilePath, (eventType: string) => { // Removed async if (eventType === 'change') { // Read the response and cleanup // Use an async IIFE inside the non-async callback void (async () => { try { const data = await fsPromises.readFile(tempFilePath, 'utf8'); // Use renamed import if (data) { const response = data.trim(); void cleanupAndResolve(response); // Mark promise as intentionally ignored } } catch (readError) { logger.error('Error reading response file:', readError); void cleanupAndResolve(''); // Cleanup on read error } })(); } }); // Start heartbeat check interval heartbeatInterval = setInterval(() => { // Removed async // Use an async IIFE inside the non-async callback void (async () => { try { const stats = await fsPromises.stat(heartbeatFilePath); // Use renamed import const now = Date.now(); // If file hasn't been modified in the last 3 seconds, assume dead if (now - stats.mtime.getTime() > 3000) { logger.info( `Heartbeat file ${heartbeatFilePath} hasn't been updated recently. Process likely exited.`, // Added logger info ); void cleanupAndResolve(''); // Mark promise as intentionally ignored } else { heartbeatFileSeen = true; // Mark that we've seen the file } } catch (err: unknown) { // Type err as unknown // Check if err is an error object with a code property if (err && typeof err === 'object' && 'code' in err) { const error = err as { code: string }; // Type assertion if (error.code === 'ENOENT') { // File not found if (heartbeatFileSeen) { // File existed before but is now gone, assume dead logger.info( `Heartbeat file ${heartbeatFilePath} not found after being seen. Process likely exited.`, // Added logger info ); void cleanupAndResolve(''); // Mark promise as intentionally ignored } else if (Date.now() - startTime > 7000) { // File never appeared and initial grace period (7s) passed, assume dead logger.info( `Heartbeat file ${heartbeatFilePath} never appeared. Process likely failed to start.`, // Added logger info ); void cleanupAndResolve(''); // Mark promise as intentionally ignored } // Otherwise, file just hasn't appeared yet, wait longer } else { // Removed check for !== 'ENOENT' as it's implied // Log other errors and resolve logger.error('Heartbeat check error:', error); void cleanupAndResolve(''); // Resolve immediately on other errors? Marked promise as intentionally ignored } } else { // Handle cases where err is not an object with a code property logger.error('Unexpected heartbeat check error:', err); void cleanupAndResolve(''); // Mark promise as intentionally ignored } } })(); }, 1500); // Check every 1.5 seconds // Timeout to stop watching if no response within limit timeoutHandle = setTimeout( () => { logger.info( `Input timeout reached after ${timeoutSeconds} seconds.`, ); // Added logger info void cleanupAndResolve(''); // Mark promise as intentionally ignored }, timeoutSeconds * 1000 + 5000, ); // Add a bit more buffer } catch (setupError) { logger.error('Error during cmd-input setup:', setupError); // Ensure cleanup happens even if setup fails // Pass optionsFilePath to cleanupResources await cleanupResources( heartbeatFilePath, tempFilePath, optionsFilePath, ); resolve(''); // Resolve with empty string after attempting cleanup } })(); // Execute the IIFE }); }
  • JSON Schema-like parameters definition in the capabilityInfo, used for MCP tool capabilities declaration.
    const capabilityInfo: ToolCapabilityInfo = { description: 'Send a question to the user via a pop-up command prompt and await their reply.', parameters: { type: 'object', properties: { projectName: { type: 'string', description: 'Identifies the context/project making the request (used in prompt formatting)', }, message: { type: 'string', description: 'The specific question for the user (appears in the prompt)', }, predefinedOptions: { type: 'array', items: { type: 'string' }, optional: true, // Mark as optional here too for consistency description: 'Predefined options for the user to choose from (optional)', }, }, required: ['projectName', 'message'], }, };

Other Tools

Related Tools

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/mikeysrecipes/interactive-mcp'

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