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