Skip to main content
Glama
index.ts27.9 kB
#!/usr/bin/env node /** * MCP Server for local tmux session management. * * This server provides tools to interact with tmux, including session management, * window/pane operations, and command execution. */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); // Constants const CHARACTER_LIMIT = 50000; // Types interface TmuxSession { name: string; windows: number; created: string; attached: boolean; } interface TmuxWindow { index: number; name: string; active: boolean; panes: number; } interface TmuxPane { index: number; active: boolean; width: number; height: number; currentCommand: string; currentPath: string; } // Utility functions async function runTmux(args: string): Promise<string> { try { const { stdout } = await execAsync(`tmux ${args}`); return stdout.trim(); } catch (error: unknown) { if (error instanceof Error && "stderr" in error) { const stderr = (error as { stderr: string }).stderr; if (stderr.includes("no server running")) { throw new Error("tmux server is not running. Start a session first with tmux_create_session."); } if (stderr.includes("session not found")) { throw new Error("Session not found. Use tmux_list_sessions to see available sessions."); } if (stderr.includes("window not found")) { throw new Error("Window not found. Use tmux_list_windows to see available windows."); } if (stderr.includes("can't find pane")) { throw new Error("Pane not found. Use tmux_list_panes to see available panes."); } throw new Error(`tmux error: ${stderr}`); } throw error; } } function formatTarget(session?: string, window?: number | string, pane?: number): string { let target = ""; if (session) { target = session; if (window !== undefined) { target += `:${window}`; if (pane !== undefined) { target += `.${pane}`; } } } return target; } function truncateIfNeeded(text: string, limit: number = CHARACTER_LIMIT): { text: string; truncated: boolean } { if (text.length <= limit) { return { text, truncated: false }; } return { text: text.slice(0, limit) + `\n\n[Output truncated. ${text.length - limit} characters omitted.]`, truncated: true, }; } // Create MCP server instance const server = new McpServer({ name: "tmux-mcp-server", version: "1.0.0", }); // ============================================================================ // Session Tools // ============================================================================ server.registerTool( "tmux_list_sessions", { title: "List tmux Sessions", description: `List all active tmux sessions. Returns information about each session including: - Session name - Number of windows - Creation time - Whether the session is currently attached Use this tool to discover available sessions before operating on them.`, inputSchema: z.object({}).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async () => { try { const output = await runTmux('list-sessions -F "#{session_name}|#{session_windows}|#{session_created}|#{session_attached}"'); if (!output) { return { content: [{ type: "text", text: "No tmux sessions found." }], }; } const sessions: TmuxSession[] = output.split("\n").map((line) => { const [name, windows, created, attached] = line.split("|"); return { name, windows: parseInt(windows, 10), created: new Date(parseInt(created, 10) * 1000).toISOString(), attached: attached === "1", }; }); const result = { count: sessions.length, sessions, }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_create_session", { title: "Create tmux Session", description: `Create a new tmux session. Args: - name (string, required): Name for the new session - window_name (string, optional): Name for the initial window - start_directory (string, optional): Starting directory for the session The session is created in detached mode. Use this to start new working environments.`, inputSchema: z .object({ name: z.string().min(1).describe("Name for the new session"), window_name: z.string().optional().describe("Name for the initial window"), start_directory: z.string().optional().describe("Starting directory for the session"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ name, window_name, start_directory }) => { try { let cmd = `new-session -d -s "${name}"`; if (window_name) { cmd += ` -n "${window_name}"`; } if (start_directory) { cmd += ` -c "${start_directory}"`; } await runTmux(cmd); return { content: [{ type: "text", text: `Session '${name}' created successfully.` }], structuredContent: { success: true, session: name }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_kill_session", { title: "Kill tmux Session", description: `Kill (terminate) a tmux session and all its windows/panes. Args: - name (string, required): Name of the session to kill WARNING: This will terminate all processes running in the session.`, inputSchema: z .object({ name: z.string().min(1).describe("Name of the session to kill"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, }, }, async ({ name }) => { try { await runTmux(`kill-session -t "${name}"`); return { content: [{ type: "text", text: `Session '${name}' killed successfully.` }], structuredContent: { success: true, session: name }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); // ============================================================================ // Window Tools // ============================================================================ server.registerTool( "tmux_list_windows", { title: "List tmux Windows", description: `List all windows in a tmux session. Args: - session (string, required): Name of the session Returns information about each window including index, name, active status, and pane count.`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), }) .strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ session }) => { try { const output = await runTmux( `list-windows -t "${session}" -F "#{window_index}|#{window_name}|#{window_active}|#{window_panes}"` ); if (!output) { return { content: [{ type: "text", text: `No windows found in session '${session}'.` }], }; } const windows: TmuxWindow[] = output.split("\n").map((line) => { const [index, name, active, panes] = line.split("|"); return { index: parseInt(index, 10), name, active: active === "1", panes: parseInt(panes, 10), }; }); const result = { session, count: windows.length, windows }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_create_window", { title: "Create tmux Window", description: `Create a new window in a tmux session. Args: - session (string, required): Name of the session - name (string, optional): Name for the new window - start_directory (string, optional): Starting directory for the window`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), name: z.string().optional().describe("Name for the new window"), start_directory: z.string().optional().describe("Starting directory for the window"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ session, name, start_directory }) => { try { let cmd = `new-window -t "${session}"`; if (name) { cmd += ` -n "${name}"`; } if (start_directory) { cmd += ` -c "${start_directory}"`; } await runTmux(cmd); return { content: [{ type: "text", text: `Window${name ? ` '${name}'` : ""} created in session '${session}'.` }], structuredContent: { success: true, session, window: name }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_kill_window", { title: "Kill tmux Window", description: `Kill (close) a window in a tmux session. Args: - session (string, required): Name of the session - window (string or number, required): Window index or name WARNING: This will terminate all processes running in the window.`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).describe("Window index or name"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window }) => { try { const target = formatTarget(session, window); await runTmux(`kill-window -t "${target}"`); return { content: [{ type: "text", text: `Window '${window}' in session '${session}' killed successfully.` }], structuredContent: { success: true, session, window }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); // ============================================================================ // Pane Tools // ============================================================================ server.registerTool( "tmux_list_panes", { title: "List tmux Panes", description: `List all panes in a tmux window. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name (defaults to current/active window) Returns information about each pane including index, dimensions, and current command.`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), }) .strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window }) => { try { const target = formatTarget(session, window); const output = await runTmux( `list-panes -t "${target}" -F "#{pane_index}|#{pane_active}|#{pane_width}|#{pane_height}|#{pane_current_command}|#{pane_current_path}"` ); if (!output) { return { content: [{ type: "text", text: "No panes found." }], }; } const panes: TmuxPane[] = output.split("\n").map((line) => { const [index, active, width, height, currentCommand, currentPath] = line.split("|"); return { index: parseInt(index, 10), active: active === "1", width: parseInt(width, 10), height: parseInt(height, 10), currentCommand, currentPath, }; }); const result = { session, window, count: panes.length, panes }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_split_window", { title: "Split tmux Window", description: `Split a window into panes. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name - horizontal (boolean, optional): Split horizontally (default: false = vertical split) - start_directory (string, optional): Starting directory for the new pane - percentage (number, optional): Size of new pane as percentage (1-99)`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), horizontal: z.boolean().default(false).describe("Split horizontally (default: vertical)"), start_directory: z.string().optional().describe("Starting directory for the new pane"), percentage: z.number().min(1).max(99).optional().describe("Size of new pane as percentage"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ session, window, horizontal, start_directory, percentage }) => { try { const target = formatTarget(session, window); let cmd = `split-window -t "${target}"`; if (horizontal) { cmd += " -h"; } if (start_directory) { cmd += ` -c "${start_directory}"`; } if (percentage) { cmd += ` -p ${percentage}`; } await runTmux(cmd); return { content: [{ type: "text", text: `Window split ${horizontal ? "horizontally" : "vertically"} successfully.` }], structuredContent: { success: true, session, window, horizontal }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_kill_pane", { title: "Kill tmux Pane", description: `Kill (close) a pane in a tmux window. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name - pane (number, required): Pane index WARNING: This will terminate the process running in the pane.`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), pane: z.number().int().min(0).describe("Pane index"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window, pane }) => { try { const target = formatTarget(session, window, pane); await runTmux(`kill-pane -t "${target}"`); return { content: [{ type: "text", text: `Pane ${pane} killed successfully.` }], structuredContent: { success: true, session, window, pane }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); // ============================================================================ // Interaction Tools // ============================================================================ server.registerTool( "tmux_send_keys", { title: "Send Keys to tmux Pane", description: `Send keys or commands to a tmux pane. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name - pane (number, optional): Pane index - keys (string, required): Keys or command to send - enter (boolean, optional): Press Enter after sending keys (default: true) Examples: - Send a command: keys="ls -la", enter=true - Send text without executing: keys="echo hello", enter=false - Send special keys: keys="C-c" (Ctrl+C), keys="C-d" (Ctrl+D)`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), pane: z.number().int().min(0).optional().describe("Pane index"), keys: z.string().min(1).describe("Keys or command to send"), enter: z.boolean().default(true).describe("Press Enter after sending keys"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ session, window, pane, keys, enter }) => { try { const target = formatTarget(session, window, pane); // Escape double quotes in keys const escapedKeys = keys.replace(/"/g, '\\"'); let cmd = `send-keys -t "${target}" "${escapedKeys}"`; if (enter) { cmd += " Enter"; } await runTmux(cmd); return { content: [{ type: "text", text: `Keys sent to ${target || "current pane"}: "${keys}"${enter ? " [Enter]" : ""}` }], structuredContent: { success: true, target, keys, enter }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_capture_pane", { title: "Capture tmux Pane Content", description: `Capture the visible content or history of a tmux pane. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name - pane (number, optional): Pane index - start_line (number, optional): Start line (negative = history, 0 = top of visible) - end_line (number, optional): End line (use - for bottom of visible pane) - escape_sequences (boolean, optional): Include escape sequences (default: false) This tool is useful for reading command output or checking the state of a pane.`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), pane: z.number().int().min(0).optional().describe("Pane index"), start_line: z.number().int().optional().describe("Start line (negative = history)"), end_line: z.number().int().optional().describe("End line"), escape_sequences: z.boolean().default(false).describe("Include escape sequences"), }) .strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window, pane, start_line, end_line, escape_sequences }) => { try { const target = formatTarget(session, window, pane); let cmd = `capture-pane -t "${target}" -p`; if (start_line !== undefined) { cmd += ` -S ${start_line}`; } if (end_line !== undefined) { cmd += ` -E ${end_line}`; } if (escape_sequences) { cmd += " -e"; } const output = await runTmux(cmd); const { text, truncated } = truncateIfNeeded(output); return { content: [{ type: "text", text }], structuredContent: { target, content: text, truncated }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); // ============================================================================ // Navigation Tools // ============================================================================ server.registerTool( "tmux_select_window", { title: "Select tmux Window", description: `Switch to a specific window in a tmux session. Args: - session (string, required): Name of the session - window (string or number, required): Window index or name`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).describe("Window index or name"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window }) => { try { const target = formatTarget(session, window); await runTmux(`select-window -t "${target}"`); return { content: [{ type: "text", text: `Switched to window '${window}' in session '${session}'.` }], structuredContent: { success: true, session, window }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_select_pane", { title: "Select tmux Pane", description: `Switch to a specific pane in a tmux window. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name - pane (number, required): Pane index`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), pane: z.number().int().min(0).describe("Pane index"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window, pane }) => { try { const target = formatTarget(session, window, pane); await runTmux(`select-pane -t "${target}"`); return { content: [{ type: "text", text: `Switched to pane ${pane}.` }], structuredContent: { success: true, session, window, pane }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_rename_session", { title: "Rename tmux Session", description: `Rename an existing tmux session. Args: - old_name (string, required): Current session name - new_name (string, required): New session name`, inputSchema: z .object({ old_name: z.string().min(1).describe("Current session name"), new_name: z.string().min(1).describe("New session name"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ old_name, new_name }) => { try { await runTmux(`rename-session -t "${old_name}" "${new_name}"`); return { content: [{ type: "text", text: `Session renamed from '${old_name}' to '${new_name}'.` }], structuredContent: { success: true, old_name, new_name }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_rename_window", { title: "Rename tmux Window", description: `Rename a window in a tmux session. Args: - session (string, required): Name of the session - window (string or number, required): Window index or current name - new_name (string, required): New window name`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).describe("Window index or current name"), new_name: z.string().min(1).describe("New window name"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ session, window, new_name }) => { try { const target = formatTarget(session, window); await runTmux(`rename-window -t "${target}" "${new_name}"`); return { content: [{ type: "text", text: `Window '${window}' renamed to '${new_name}'.` }], structuredContent: { success: true, session, window, new_name }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); server.registerTool( "tmux_resize_pane", { title: "Resize tmux Pane", description: `Resize a pane in a tmux window. Args: - session (string, required): Name of the session - window (string or number, optional): Window index or name - pane (number, optional): Pane index - direction (string, required): Direction to resize: up, down, left, right - amount (number, optional): Number of cells to resize by (default: 5)`, inputSchema: z .object({ session: z.string().min(1).describe("Name of the session"), window: z.union([z.string(), z.number()]).optional().describe("Window index or name"), pane: z.number().int().min(0).optional().describe("Pane index"), direction: z.enum(["up", "down", "left", "right"]).describe("Direction to resize"), amount: z.number().int().min(1).default(5).describe("Number of cells to resize by"), }) .strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ session, window, pane, direction, amount }) => { try { const target = formatTarget(session, window, pane); const dirFlag = { up: "-U", down: "-D", left: "-L", right: "-R" }[direction]; await runTmux(`resize-pane -t "${target}" ${dirFlag} ${amount}`); return { content: [{ type: "text", text: `Pane resized ${direction} by ${amount} cells.` }], structuredContent: { success: true, direction, amount }, }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, }; } } ); // Main function async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("tmux MCP server running via stdio"); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });

Implementation Reference

Latest Blog Posts

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

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