Tmux MCP Server

by nickgnd
Verified
import { parseArgs } from 'node:util'; import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import * as tmux from "./tmux.js"; // Create MCP server const server = new McpServer({ name: "tmux-context", version: "0.1.0" }, { capabilities: { resources: { subscribe: true, listChanged: true }, tools: { listChanged: true }, logging: {} } }); // List all tmux sessions - Tool server.tool( "list-sessions", "List all active tmux sessions", {}, async () => { try { const sessions = await tmux.listSessions(); return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error listing tmux sessions: ${error}` }], isError: true }; } } ); // Find session by name - Tool server.tool( "find-session", "Find a tmux session by name", { name: z.string().describe("Name of the tmux session to find") }, async ({ name }) => { try { const session = await tmux.findSessionByName(name); return { content: [{ type: "text", text: session ? JSON.stringify(session, null, 2) : `Session not found: ${name}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error finding tmux session: ${error}` }], isError: true }; } } ); // List windows in a session - Tool server.tool( "list-windows", "List windows in a tmux session", { sessionId: z.string().describe("ID of the tmux session") }, async ({ sessionId }) => { try { const windows = await tmux.listWindows(sessionId); return { content: [{ type: "text", text: JSON.stringify(windows, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error listing windows: ${error}` }], isError: true }; } } ); // List panes in a window - Tool server.tool( "list-panes", "List panes in a tmux window", { windowId: z.string().describe("ID of the tmux window") }, async ({ windowId }) => { try { const panes = await tmux.listPanes(windowId); return { content: [{ type: "text", text: JSON.stringify(panes, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error listing panes: ${error}` }], isError: true }; } } ); // Capture pane content - Tool server.tool( "capture-pane", "Capture content from a tmux pane", { paneId: z.string().describe("ID of the tmux pane"), lines: z.string().optional().describe("Number of lines to capture") }, async ({ paneId, lines }) => { try { // Parse lines parameter if provided const linesCount = lines ? parseInt(lines, 10) : undefined; const content = await tmux.capturePaneContent(paneId, linesCount); return { content: [{ type: "text", text: content || "No content captured" }] }; } catch (error) { return { content: [{ type: "text", text: `Error capturing pane content: ${error}` }], isError: true }; } } ); // Create new session - Tool server.tool( "create-session", "Create a new tmux session", { name: z.string().describe("Name for the new tmux session") }, async ({ name }) => { try { const session = await tmux.createSession(name); return { content: [{ type: "text", text: session ? `Session created: ${JSON.stringify(session, null, 2)}` : `Failed to create session: ${name}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating session: ${error}` }], isError: true }; } } ); // Create new window - Tool server.tool( "create-window", "Create a new window in a tmux session", { sessionId: z.string().describe("ID of the tmux session"), name: z.string().describe("Name for the new window") }, async ({ sessionId, name }) => { try { const window = await tmux.createWindow(sessionId, name); return { content: [{ type: "text", text: window ? `Window created: ${JSON.stringify(window, null, 2)}` : `Failed to create window: ${name}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating window: ${error}` }], isError: true }; } } ); // Execute command in pane - Tool server.tool( "execute-command", "Execute a command in a tmux pane and get results", { paneId: z.string().describe("ID of the tmux pane"), command: z.string().describe("Command to execute") }, async ({ paneId, command }) => { try { const commandId = await tmux.executeCommand(paneId, command); // Create the resource URI for this command's results const resourceUri = `tmux://command/${commandId}/result`; return { content: [{ type: "text", text: `Command execution started.\n\nTo get results, subscribe to and read resource: ${resourceUri}\n\nStatus will change from 'pending' to 'completed' or 'error' when finished.` }] }; } catch (error) { return { content: [{ type: "text", text: `Error executing command: ${error}` }], isError: true }; } } ); // Get command result - Tool server.tool( "get-command-result", "Get the result of an executed command", { commandId: z.string().describe("ID of the executed command") }, async ({ commandId }) => { try { // Check and update command status const command = await tmux.checkCommandStatus(commandId); if (!command) { return { content: [{ type: "text", text: `Command not found: ${commandId}` }], isError: true }; } // Format the response based on command status let resultText; if (command.status === 'pending') { resultText = `Command still executing...\nStarted: ${command.startTime.toISOString()}\nCommand: ${command.command}`; } else { resultText = `Status: ${command.status}\nExit code: ${command.exitCode}\nCommand: ${command.command}\n\n--- Output ---\n${command.result}`; } return { content: [{ type: "text", text: resultText }] }; } catch (error) { return { content: [{ type: "text", text: `Error retrieving command result: ${error}` }], isError: true }; } } ); // Expose tmux session list as a resource server.resource( "Tmux Sessions", "tmux://sessions", async () => { try { const sessions = await tmux.listSessions(); return { contents: [{ uri: "tmux://sessions", text: JSON.stringify(sessions.map(session => ({ id: session.id, name: session.name, attached: session.attached, windows: session.windows })), null, 2) }] }; } catch (error) { return { contents: [{ uri: "tmux://sessions", text: `Error listing tmux sessions: ${error}` }] }; } } ); // Expose pane content as a resource server.resource( "Tmux Pane Content", new ResourceTemplate("tmux://pane/{paneId}", { list: async () => { try { // Get all sessions const sessions = await tmux.listSessions(); const paneResources = []; // For each session, get all windows for (const session of sessions) { const windows = await tmux.listWindows(session.id); // For each window, get all panes for (const window of windows) { const panes = await tmux.listPanes(window.id); // For each pane, create a resource with descriptive name for (const pane of panes) { paneResources.push({ name: `Pane: ${session.name} - ${pane.id} - ${pane.title} ${pane.active ? "(active)" : ""}`, uri: `tmux://pane/${pane.id}`, description: `Content from pane ${pane.id} - ${pane.title} in session ${session.name}` }); } } } return { resources: paneResources }; } catch (error) { server.server.sendLoggingMessage({ level: 'error', data: `Error listing panes: ${error}` }); return { resources: [] }; } } }), async (uri, { paneId }) => { try { // Ensure paneId is a string const paneIdStr = Array.isArray(paneId) ? paneId[0] : paneId; const content = await tmux.capturePaneContent(paneIdStr); return { contents: [{ uri: uri.href, text: content || "No content captured" }] }; } catch (error) { return { contents: [{ uri: uri.href, text: `Error capturing pane content: ${error}` }] }; } } ); // Create dynamic resource for command executions server.resource( "Command Execution Result", new ResourceTemplate("tmux://command/{commandId}/result", { list: async () => { // Only list active commands that aren't too old tmux.cleanupOldCommands(10); // Clean commands older than 10 minutes const resources = []; for (const id of tmux.getActiveCommandIds()) { const command = tmux.getCommand(id); if (command) { resources.push({ name: `Command: ${command.command.substring(0, 30)}${command.command.length > 30 ? '...' : ''}`, uri: `tmux://command/${id}/result`, description: `Execution status: ${command.status}` }); } } return { resources }; } }), async (uri, { commandId }) => { try { // Ensure commandId is a string const commandIdStr = Array.isArray(commandId) ? commandId[0] : commandId; // Check command status const command = await tmux.checkCommandStatus(commandIdStr); if (!command) { return { contents: [{ uri: uri.href, text: `Command not found: ${commandIdStr}` }] }; } // Format the response based on command status let resultText; if (command.status === 'pending') { resultText = `Command still executing...\nStarted: ${command.startTime.toISOString()}\nCommand: ${command.command}`; } else { resultText = `Status: ${command.status}\nExit code: ${command.exitCode}\nCommand: ${command.command}\n\n--- Output ---\n${command.result}`; } return { contents: [{ uri: uri.href, text: resultText }] }; } catch (error) { return { contents: [{ uri: uri.href, text: `Error retrieving command result: ${error}` }] }; } } ); async function main() { try { const { values } = parseArgs({ options: { 'shell-type': { type: 'string', default: 'bash', short: 's' } } }); // Set shell configuration tmux.setShellConfig({ type: values['shell-type'] as string }); // Check if tmux is running const tmuxRunning = await tmux.isTmuxRunning(); if (!tmuxRunning) { server.server.sendLoggingMessage({ level: 'error', data: 'Tmux seems not running' }); throw "Tmux server is not running"; } // Start the MCP server const transport = new StdioServerTransport(); await server.connect(transport); } catch (error) { console.error("Failed to start MCP server:", error); process.exit(1); } } main().catch(error => { console.error("Fatal error:", error); process.exit(1); });