Skip to main content
Glama

Interactive MCP

MIT License
97
299
  • Apple
  • Linux
interactive-input.tsx6.34 kB
import React, { FC, useState, useEffect } from 'react'; import { Box, Text } from 'ink'; import { useInput } from 'ink'; // Interface for shared input component props export interface InteractiveInputProps { question: string; // Question text to display questionId?: string; // Optional ID for the question (used in intensive chat) predefinedOptions?: string[]; // Optional list of choices onSubmit: (value: string, questionId?: string) => void; // Callback on submission } // Shared input component using Ink export const InteractiveInput: FC<InteractiveInputProps> = ({ question, questionId, predefinedOptions, onSubmit, }) => { const [mode, setMode] = useState<'option' | 'custom'>('option'); const [selectedIndex, setSelectedIndex] = useState(0); const [customValue, setCustomValue] = useState(''); const [cursorPosition, setCursorPosition] = useState(0); // Get the character under cursor, if any const charUnderCursor = customValue[cursorPosition] || null; // If there are no predefined options, default to custom input mode useEffect(() => { if (!predefinedOptions || predefinedOptions.length === 0) { setMode('custom'); } else { // Ensure mode is 'option' if options become available setMode('option'); setSelectedIndex(0); // Reset selection } }, [predefinedOptions]); // Capture key presses useInput((input, key) => { if ((key.upArrow || key.downArrow) && predefinedOptions?.length) { // cycle selection among predefined options setSelectedIndex((prev) => { if (key.upArrow) { return prev > 0 ? prev - 1 : predefinedOptions.length - 1; } else { return prev < predefinedOptions.length - 1 ? prev + 1 : 0; } }); setMode('option'); } else if (key.leftArrow) { if (mode === 'custom') { // Move cursor left if possible setCursorPosition((prev) => Math.max(0, prev - 1)); } else { // If in option mode, just switch to custom mode but keep cursor at 0 setMode('custom'); setCursorPosition(0); } } else if (key.rightArrow) { if (mode === 'custom') { // Move cursor right if possible setCursorPosition((prev) => Math.min(customValue.length, prev + 1)); } else { // If in option mode, switch to custom mode with cursor at end of text setMode('custom'); setCursorPosition(customValue.length); } } else if (key.return) { const value = mode === 'custom' ? customValue : (predefinedOptions && predefinedOptions[selectedIndex]) || ''; onSubmit(value, questionId); // Pass questionId back if it exists } else if (key.backspace || key.delete) { if (mode === 'custom') { if (key.delete && cursorPosition < customValue.length) { // Delete: remove character at cursor position setCustomValue( (prev) => prev.slice(0, cursorPosition) + prev.slice(cursorPosition + 1), ); } else if (key.backspace && cursorPosition > 0) { // Backspace: remove character before cursor and move cursor left setCustomValue( (prev) => prev.slice(0, cursorPosition - 1) + prev.slice(cursorPosition), ); setCursorPosition((prev) => prev - 1); } } } else if (input && input.length === 1 && !key.ctrl && !key.meta) { // Any other non-modifier key appends to custom input setMode('custom'); // Insert at cursor position instead of appending setCustomValue( (prev) => prev.slice(0, cursorPosition) + input + prev.slice(cursorPosition), ); setCursorPosition((prev) => prev + 1); } }); return ( <> {/* Display the question */} <Box flexDirection="column" marginBottom={1}> <Text bold color="cyan" wrap="wrap"> {question} </Text> </Box> {/* Display predefined options if available */} {predefinedOptions && predefinedOptions.length > 0 && ( <Box flexDirection="column" marginBottom={1}> <Text> Use ↑/↓ to select options, type any key for custom input, Enter to submit </Text> {predefinedOptions.map((opt, i) => ( <Text key={i} color={ i === selectedIndex ? mode === 'option' ? 'greenBright' : 'green' : undefined } > {i === selectedIndex ? (mode === 'option' ? '› ' : ' ') : ' '} {opt} </Text> ))} </Box> )} {/* Custom input line with cursor visualization */} <Box marginBottom={1}> <Box> <Text color={ customValue.length > 0 || mode === 'custom' ? mode === 'custom' ? 'greenBright' : 'green' : undefined } > {customValue.length > 0 && mode === 'custom' ? '✎ ' : ' '} {/* Only show "Custom: " label when there are predefined options */} {predefinedOptions && predefinedOptions.length > 0 ? 'Custom: ' : ''} {customValue.slice(0, cursorPosition)} </Text> {/* Cursor character simulation */} {charUnderCursor ? ( <Text backgroundColor="green" color="black"> {charUnderCursor} </Text> ) : ( // Display block cursor only in custom mode or when options are present (to show where custom input would go) (mode === 'custom' || (predefinedOptions && predefinedOptions.length > 0)) && ( <Text color={mode === 'custom' ? 'green' : undefined}>█</Text> ) )} <Text color={ customValue.length > 0 || mode === 'custom' ? mode === 'custom' ? 'greenBright' : 'green' : undefined } > {customValue.slice(cursorPosition + 1)} </Text> </Box> </Box> </> ); };

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

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