Skip to main content
Glama
mikeysrecipes

interactive-mcp

request_user_input

Ask users questions to clarify requirements, confirm plans, or resolve ambiguity before proceeding with actions like code edits or file operations.

Instructions

Input Schema

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

Implementation Reference

  • The asynchronous handler function for the 'request_user_input' tool. It extracts arguments, constructs the prompt, calls getCmdWindowInput, and returns the formatted user response or timeout/empty message.
    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 }] };
      }
    },
  • src/index.ts:111-151 (registration)
    Conditional registration of the 'request_user_input' tool using server.tool(), including dynamic description resolution and schema from the tool definition.
    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 }] };
          }
        },
      );
    }
  • Zod raw schema shape for the tool's input parameters: projectName, message, and optional predefinedOptions.
    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)'),
    };
  • Core helper function that implements the user input prompting by spawning a detached Node.js UI process, using temporary files for communication, heartbeat checks for process liveness, and handling timeouts across platforms.
    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
      });
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure and does so comprehensively. It describes timeout behavior ('Returns user response or timeout notification (timeout defaults to 60 seconds)'), context maintenance ('Maintains context across user interactions'), error handling ('Handles empty responses gracefully'), and formatting behavior ('Properly formats prompt with project context'). This goes well beyond basic functionality disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections (<importantNotes>, <whenToUseThisTool>, etc.), but it's overly verbose with redundant information across sections. The core purpose is stated multiple times, and some sections (like <features>) repeat information that could be more efficiently integrated. While organized, it could be more concise without losing clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (interactive user input with timeout handling) and the absence of both annotations and output schema, the description provides exceptional completeness. It covers purpose, usage scenarios, behavioral characteristics, parameters, examples, and best practices. The extensive <examples> section and detailed behavioral descriptions in <features> compensate for the lack of structured metadata, making this fully self-contained.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

While schema description coverage is 100% (providing baseline 3), the description adds significant value through the <parameters> section that explains each parameter's purpose and usage context. It clarifies that projectName is 'used in prompt formatting' and message 'appears in the prompt,' and provides examples showing how these parameters work together. However, it doesn't explain format constraints or edge cases beyond what's in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Send a question to the user via a pop-up command prompt.' It specifies the exact action (send a question) and mechanism (pop-up command prompt), and distinguishes itself from sibling tools like ask_intensive_chat by focusing on discrete user queries rather than ongoing chat sessions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides extensive, explicit guidance on when to use this tool versus alternatives. It includes a dedicated <whenToUseThisTool> section with 10 specific scenarios, emphasizes proactive questioning over assumptions, and explicitly states 'Do not ask the question if you have another tool that can answer the question' in the best practices section, providing clear alternative selection criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other 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