Skip to main content
Glama

Tmux MCP Server

by nickgnd
MIT License
148
115
  • Linux
  • Apple
#!/usr/bin/env node 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-mcp", version: "0.2.2" }, { 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 with configurable lines count and optional color preservation", { paneId: z.string().describe("ID of the tmux pane"), lines: z.string().optional().describe("Number of lines to capture"), colors: z.boolean().optional().describe("Include color/escape sequences for text and background attributes in output") }, async ({ paneId, lines, colors }) => { try { // Parse lines parameter if provided const linesCount = lines ? parseInt(lines, 10) : undefined; const includeColors = colors || false; const content = await tmux.capturePaneContent(paneId, linesCount, includeColors); 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 }; } } ); // Kill session - Tool server.tool( "kill-session", "Kill a tmux session by ID", { sessionId: z.string().describe("ID of the tmux session to kill") }, async ({ sessionId }) => { try { await tmux.killSession(sessionId); return { content: [{ type: "text", text: `Session ${sessionId} has been killed` }] }; } catch (error) { return { content: [{ type: "text", text: `Error killing session: ${error}` }], isError: true }; } } ); // Kill window - Tool server.tool( "kill-window", "Kill a tmux window by ID", { windowId: z.string().describe("ID of the tmux window to kill") }, async ({ windowId }) => { try { await tmux.killWindow(windowId); return { content: [{ type: "text", text: `Window ${windowId} has been killed` }] }; } catch (error) { return { content: [{ type: "text", text: `Error killing window: ${error}` }], isError: true }; } } ); // Kill pane - Tool server.tool( "kill-pane", "Kill a tmux pane by ID", { paneId: z.string().describe("ID of the tmux pane to kill") }, async ({ paneId }) => { try { await tmux.killPane(paneId); return { content: [{ type: "text", text: `Pane ${paneId} has been killed` }] }; } catch (error) { return { content: [{ type: "text", text: `Error killing pane: ${error}` }], isError: true }; } } ); // Split pane - Tool server.tool( "split-pane", "Split a tmux pane horizontally or vertically", { paneId: z.string().describe("ID of the tmux pane to split"), direction: z.enum(["horizontal", "vertical"]).optional().describe("Split direction: 'horizontal' (side by side) or 'vertical' (top/bottom). Default is 'vertical'"), size: z.number().min(1).max(99).optional().describe("Size of the new pane as percentage (1-99). Default is 50%") }, async ({ paneId, direction, size }) => { try { const newPane = await tmux.splitPane(paneId, direction || 'vertical', size); return { content: [{ type: "text", text: newPane ? `Pane split successfully. New pane: ${JSON.stringify(newPane, null, 2)}` : `Failed to split pane ${paneId}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error splitting pane: ${error}` }], isError: true }; } } ); // Execute command in pane - Tool server.tool( "execute-command", "Execute a command in a tmux pane and get results. For interactive applications (REPLs, editors), use `rawMode=true`. IMPORTANT: When `rawMode=false` (default), avoid heredoc syntax (cat << EOF) and other multi-line constructs as they conflict with command wrapping. For file writing, prefer: printf 'content\\n' > file, echo statements, or write to temp files instead", { paneId: z.string().describe("ID of the tmux pane"), command: z.string().describe("Command to execute"), rawMode: z.boolean().optional().describe("Execute command without wrapper markers for REPL/interactive compatibility. Disables get-command-result status tracking. Use capture-pane after execution to verify command outcome."), noEnter: z.boolean().optional().describe("Send keystrokes without pressing Enter. For TUI navigation in apps like btop, vim, less. Supports special keys (Up, Down, Escape, Tab, etc.) and strings (sent char-by-char for proper filtering). Automatically applies rawMode. Use capture-pane after to see results.") }, async ({ paneId, command, rawMode, noEnter }) => { try { // If noEnter is true, automatically apply rawMode const effectiveRawMode = noEnter || rawMode; const commandId = await tmux.executeCommand(paneId, command, effectiveRawMode, noEnter); if (effectiveRawMode) { const modeText = noEnter ? "Keys sent without Enter" : "Interactive command started (rawMode)"; return { content: [{ type: "text", text: `${modeText}.\n\nStatus tracking is disabled.\nUse 'capture-pane' with paneId '${paneId}' to verify the command outcome.\n\nCommand ID: ${commandId}` }] }; } // 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') { if (command.result) { resultText = `Status: ${command.status}\nCommand: ${command.command}\n\n--- Message ---\n${command.result}`; } else { 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; // Default to no colors for resources to maintain clean programmatic access const content = await tmux.capturePaneContent(paneIdStr, 200, false); 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') { // For rawMode commands, we set a result message while status remains 'pending' // since we can't track their actual completion if (command.result) { resultText = `Status: ${command.status}\nCommand: ${command.command}\n\n--- Message ---\n${command.result}`; } else { 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 }); // 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); });

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/nickgnd/tmux-mcp'

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