UNPKG

last-words-mcp

Version:

MCP Server that opens a chat window for final user input to save message credits

210 lines (209 loc) 7.57 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; const execAsync = promisify(exec); // Get the directory where this script is located (works with npx!) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Create an MCP server with tool capability for getting final user input */ const server = new Server({ name: "last-words-mcp", version: "1.0.24", }, { capabilities: { tools: {}, }, }); // Function to create a script that will be run in a new terminal function createInputScript(context) { const contextDisplay = context ? ` echo "📝 Context from AI:" echo "${context.replace(/"/g, '\\"')}" echo "$(printf '%*s' 40 '' | tr ' ' '-')" echo ""` : ''; return `#!/bin/bash clear echo "$(printf '%*s' 60 '' | tr ' ' '=')" echo "🗣️ LAST WORDS - Final Input to AI Assistant" echo "$(printf '%*s' 60 '' | tr ' ' '=')" echo ""${contextDisplay} echo "💡 This tool saves message credits by using a tool call instead of a chat message." echo "Type your final instructions, clarifications, or additional context below." echo "Press Enter when finished, or Ctrl+C to cancel." echo "" read -p "Your message: " user_input echo "" if [ -n "$user_input" ]; then echo "✅ Message sent to AI assistant!" echo "$user_input" > "$1" else echo "❌ No input provided. Cancelled." echo "CANCELLED" > "$1" fi echo "$(printf '%*s' 60 '' | tr ' ' '=')" echo "You can close this terminal window now." read -p "Press Enter to close..." `; } /** * Function that runs the shell script and waits for user input */ async function getUserInputViaShellScript(context) { const tempDir = tmpdir(); const outputPath = path.join(tempDir, `last-words-output-${Date.now()}.txt`); const contextPath = path.join(tempDir, `last-words-context-${Date.now()}.txt`); // Write context to file if provided if (context) { writeFileSync(contextPath, context); } // Get the path to our shell script const scriptPath = path.join(__dirname, '..', 'last-words-input.sh'); // Check if script exists if (!existsSync(scriptPath)) { throw new Error(`Shell script not found: ${scriptPath}`); } // Prepare arguments for the shell script const args = [outputPath]; if (context) { args.push(contextPath); } // Just run the shell script directly - it will handle opening a terminal if needed const command = `chmod +x "${scriptPath}" && bash "${scriptPath}" ${args.map(arg => `"${arg}"`).join(' ')}`; try { // Execute the command - the script will open its own terminal await execAsync(command); // Now poll for the output file return new Promise((resolve, reject) => { const pollInterval = setInterval(() => { if (existsSync(outputPath)) { clearInterval(pollInterval); try { const result = readFileSync(outputPath, 'utf8').trim(); // Cleanup try { unlinkSync(outputPath); if (context && existsSync(contextPath)) { unlinkSync(contextPath); } } catch (cleanupError) { // Ignore cleanup errors } if (result === 'CANCELLED' || result === '') { reject(new Error('User cancelled input')); } else { resolve(result); } } catch (error) { reject(new Error('Failed to read user input: ' + (error instanceof Error ? error.message : String(error)))); } } }, 1000); // Timeout after 5 minutes setTimeout(() => { clearInterval(pollInterval); try { if (existsSync(outputPath)) { unlinkSync(outputPath); } if (context && existsSync(contextPath)) { unlinkSync(contextPath); } } catch (cleanupError) { // Ignore cleanup errors } reject(new Error('Input timeout - no response after 5 minutes')); }, 5 * 60 * 1000); }); } catch (error) { // Cleanup on error try { if (existsSync(outputPath)) { unlinkSync(outputPath); } if (context && existsSync(contextPath)) { unlinkSync(contextPath); } } catch (cleanupError) { // Ignore cleanup errors } throw new Error('Failed to run shell script: ' + (error instanceof Error ? error.message : String(error))); } } /** * Handler that lists available tools. * Exposes a "last_words" tool that opens a terminal for final user input. */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "last_words", description: "Opens a terminal window with shell script for final user input to save message credits. Use this as your very last step to get additional instructions or clarifications from the user.", inputSchema: { type: "object", properties: { context: { type: "string", description: "Optional context about what you've completed so far" } } } } ] }; }); /** * Handler for the last_words tool. * Opens a terminal window and gets user input. */ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== "last_words") { throw new Error("Unknown tool"); } try { const context = request.params.arguments?.context; const userInput = await getUserInputViaShellScript(context); return { content: [{ type: "text", text: `User's final input: "${userInput}"` }] }; } catch (error) { return { content: [{ type: "text", text: `Failed to get user input: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } }); /** * Start the server using stdio transport */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error(`Last Words MCP server running`); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });