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
JavaScript
#!/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);
});