#
tokens: 10874/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE.md
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── tmux.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | .DS_Store
4 | *.log
5 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Tmux MCP Server
 2 | 
 3 | Model Context Protocol server that enables Claude Desktop to interact with and view tmux session content. This integration allows AI assistants to read from, control, and observe your terminal sessions.
 4 | 
 5 | ## Features
 6 | 
 7 | - List and search tmux sessions
 8 | - View and navigate tmux windows and panes
 9 | - Capture and expose terminal content from any pane
10 | - Execute commands in tmux panes and retrieve results (use it at your own risk ⚠️)
11 | - Create new tmux sessions and windows
12 | - Split panes horizontally or vertically with customizable sizes
13 | - Kill tmux sessions, windows, and panes
14 | 
15 | Check out this short video to get excited!
16 | 
17 | </br>
18 | 
19 | [![youtube video](http://i.ytimg.com/vi/3W0pqRF1RS0/hqdefault.jpg)](https://www.youtube.com/watch?v=3W0pqRF1RS0)
20 | 
21 | ## Prerequisites
22 | 
23 | - Node.js
24 | - tmux installed and running
25 | 
26 | ## Usage
27 | 
28 | ### Configure Claude Desktop
29 | 
30 | Add this MCP server to your Claude Desktop configuration:
31 | 
32 | ```json
33 | "mcpServers": {
34 |   "tmux": {
35 |     "command": "npx",
36 |     "args": ["-y", "tmux-mcp"]
37 |   }
38 | }
39 | ```
40 | 
41 | ### MCP server options
42 | 
43 | You can optionally specify the command line shell you are using, if unspecified it defaults to `bash`
44 | 
45 | ```json
46 | "mcpServers": {
47 |   "tmux": {
48 |     "command": "npx",
49 |     "args": ["-y", "tmux-mcp", "--shell-type=fish"]
50 |   }
51 | }
52 | ```
53 | 
54 | The MCP server needs to know the shell only when executing commands, to properly read its exit status.
55 | 
56 | ## Available Resources
57 | 
58 | - `tmux://sessions` - List all tmux sessions
59 | - `tmux://pane/{paneId}` - View content of a specific tmux pane
60 | - `tmux://command/{commandId}/result` - Results from executed commands
61 | 
62 | ## Available Tools
63 | 
64 | - `list-sessions` - List all active tmux sessions
65 | - `find-session` - Find a tmux session by name
66 | - `list-windows` - List windows in a tmux session
67 | - `list-panes` - List panes in a tmux window
68 | - `capture-pane` - Capture content from a tmux pane
69 | - `create-session` - Create a new tmux session
70 | - `create-window` - Create a new window in a tmux session
71 | - `split-pane` - Split a tmux pane horizontally or vertically with optional size
72 | - `kill-session` - Kill a tmux session by ID
73 | - `kill-window` - Kill a tmux window by ID
74 | - `kill-pane` - Kill a tmux pane by ID
75 | - `execute-command` - Execute a command in a tmux pane
76 | - `get-command-result` - Get the result of an executed command
77 | 
78 | 
```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
1 | Copyright 2025 Nicolò Gnudi
2 | 
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | 
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | 
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 | 
9 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "esModuleInterop": true,
 7 |     "outDir": "./build",
 8 |     "rootDir": "./src",
 9 |     "strict": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "tmux-mcp",
 3 |   "version": "0.2.2",
 4 |   "description": "MCP Server for interfacing with tmux sessions",
 5 |   "type": "module",
 6 |   "main": "build/index.js",
 7 |   "scripts": {
 8 |     "build": "tsc",
 9 |     "start": "node build/index.js",
10 |     "dev": "tsc -w",
11 |     "check-release": "npm run build && npm publish --dry-run",
12 |     "release": "npm run build && npm publish"
13 |   },
14 |   "bin": {
15 |     "tmux-mcp": "build/index.js"
16 |   },
17 |   "files": [
18 |     "build"
19 |   ],
20 |   "keywords": [
21 |     "mcp",
22 |     "tmux",
23 |     "claude"
24 |   ],
25 |   "author": "nickgnd",
26 |   "license": "MIT",
27 |   "dependencies": {
28 |     "@modelcontextprotocol/sdk": "^1.0.2",
29 |     "uuid": "^11.1.0",
30 |     "zod": "^3.22.4"
31 |   },
32 |   "devDependencies": {
33 |     "@types/node": "^20.10.5",
34 |     "@types/uuid": "^10.0.0",
35 |     "typescript": "^5.3.3"
36 |   },
37 |   "repository": {
38 |     "type": "git",
39 |     "url": "git+https://github.com/nickgnd/tmux-mcp.git"
40 |   },
41 |   "bugs": {
42 |     "url": "https://github.com/nickgnd/tmux-mcp/issues"
43 |   },
44 |   "homepage": "https://github.com/nickgnd/tmux-mcp#readme"
45 | }
46 | 
```

--------------------------------------------------------------------------------
/src/tmux.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { exec as execCallback } from "child_process";
  2 | import { promisify } from "util";
  3 | import { v4 as uuidv4 } from 'uuid';
  4 | 
  5 | const exec = promisify(execCallback);
  6 | 
  7 | // Basic interfaces for tmux objects
  8 | export interface TmuxSession {
  9 |   id: string;
 10 |   name: string;
 11 |   attached: boolean;
 12 |   windows: number;
 13 | }
 14 | 
 15 | export interface TmuxWindow {
 16 |   id: string;
 17 |   name: string;
 18 |   active: boolean;
 19 |   sessionId: string;
 20 | }
 21 | 
 22 | export interface TmuxPane {
 23 |   id: string;
 24 |   windowId: string;
 25 |   active: boolean;
 26 |   title: string;
 27 | }
 28 | 
 29 | interface CommandExecution {
 30 |   id: string;
 31 |   paneId: string;
 32 |   command: string;
 33 |   status: 'pending' | 'completed' | 'error';
 34 |   startTime: Date;
 35 |   result?: string;
 36 |   exitCode?: number;
 37 |   rawMode?: boolean;
 38 | }
 39 | 
 40 | export type ShellType = 'bash' | 'zsh' | 'fish';
 41 | 
 42 | let shellConfig: { type: ShellType } = { type: 'bash' };
 43 | 
 44 | export function setShellConfig(config: { type: string }): void {
 45 |   // Validate shell type
 46 |   const validShells: ShellType[] = ['bash', 'zsh', 'fish'];
 47 | 
 48 |   if (validShells.includes(config.type as ShellType)) {
 49 |     shellConfig = { type: config.type as ShellType };
 50 |   } else {
 51 |     shellConfig = { type: 'bash' };
 52 |   }
 53 | }
 54 | 
 55 | /**
 56 |  * Execute a tmux command and return the result
 57 |  */
 58 | export async function executeTmux(tmuxCommand: string): Promise<string> {
 59 |   try {
 60 |     const { stdout } = await exec(`tmux ${tmuxCommand}`);
 61 |     return stdout.trim();
 62 |   } catch (error: any) {
 63 |     throw new Error(`Failed to execute tmux command: ${error.message}`);
 64 |   }
 65 | }
 66 | 
 67 | /**
 68 |  * Check if tmux server is running
 69 |  */
 70 | export async function isTmuxRunning(): Promise<boolean> {
 71 |   try {
 72 |     await executeTmux("list-sessions -F '#{session_name}'");
 73 |     return true;
 74 |   } catch (error) {
 75 |     return false;
 76 |   }
 77 | }
 78 | 
 79 | /**
 80 |  * List all tmux sessions
 81 |  */
 82 | export async function listSessions(): Promise<TmuxSession[]> {
 83 |   const format = "#{session_id}:#{session_name}:#{?session_attached,1,0}:#{session_windows}";
 84 |   const output = await executeTmux(`list-sessions -F '${format}'`);
 85 | 
 86 |   if (!output) return [];
 87 | 
 88 |   return output.split('\n').map(line => {
 89 |     const [id, name, attached, windows] = line.split(':');
 90 |     return {
 91 |       id,
 92 |       name,
 93 |       attached: attached === '1',
 94 |       windows: parseInt(windows, 10)
 95 |     };
 96 |   });
 97 | }
 98 | 
 99 | /**
100 |  * Find a session by name
101 |  */
102 | export async function findSessionByName(name: string): Promise<TmuxSession | null> {
103 |   try {
104 |     const sessions = await listSessions();
105 |     return sessions.find(session => session.name === name) || null;
106 |   } catch (error) {
107 |     return null;
108 |   }
109 | }
110 | 
111 | /**
112 |  * List windows in a session
113 |  */
114 | export async function listWindows(sessionId: string): Promise<TmuxWindow[]> {
115 |   const format = "#{window_id}:#{window_name}:#{?window_active,1,0}";
116 |   const output = await executeTmux(`list-windows -t '${sessionId}' -F '${format}'`);
117 | 
118 |   if (!output) return [];
119 | 
120 |   return output.split('\n').map(line => {
121 |     const [id, name, active] = line.split(':');
122 |     return {
123 |       id,
124 |       name,
125 |       active: active === '1',
126 |       sessionId
127 |     };
128 |   });
129 | }
130 | 
131 | /**
132 |  * List panes in a window
133 |  */
134 | export async function listPanes(windowId: string): Promise<TmuxPane[]> {
135 |   const format = "#{pane_id}:#{pane_title}:#{?pane_active,1,0}";
136 |   const output = await executeTmux(`list-panes -t '${windowId}' -F '${format}'`);
137 | 
138 |   if (!output) return [];
139 | 
140 |   return output.split('\n').map(line => {
141 |     const [id, title, active] = line.split(':');
142 |     return {
143 |       id,
144 |       windowId,
145 |       title: title,
146 |       active: active === '1'
147 |     };
148 |   });
149 | }
150 | 
151 | /**
152 |  * Capture content from a specific pane, by default the latest 200 lines.
153 |  */
154 | export async function capturePaneContent(paneId: string, lines: number = 200, includeColors: boolean = false): Promise<string> {
155 |   const colorFlag = includeColors ? '-e' : '';
156 |   return executeTmux(`capture-pane -p ${colorFlag} -t '${paneId}' -S -${lines} -E -`);
157 | }
158 | 
159 | /**
160 |  * Create a new tmux session
161 |  */
162 | export async function createSession(name: string): Promise<TmuxSession | null> {
163 |   await executeTmux(`new-session -d -s "${name}"`);
164 |   return findSessionByName(name);
165 | }
166 | 
167 | /**
168 |  * Create a new window in a session
169 |  */
170 | export async function createWindow(sessionId: string, name: string): Promise<TmuxWindow | null> {
171 |   const output = await executeTmux(`new-window -t '${sessionId}' -n '${name}'`);
172 |   const windows = await listWindows(sessionId);
173 |   return windows.find(window => window.name === name) || null;
174 | }
175 | 
176 | /**
177 |  * Kill a tmux session by ID
178 |  */
179 | export async function killSession(sessionId: string): Promise<void> {
180 |   await executeTmux(`kill-session -t '${sessionId}'`);
181 | }
182 | 
183 | /**
184 |  * Kill a tmux window by ID
185 |  */
186 | export async function killWindow(windowId: string): Promise<void> {
187 |   await executeTmux(`kill-window -t '${windowId}'`);
188 | }
189 | 
190 | /**
191 |  * Kill a tmux pane by ID
192 |  */
193 | export async function killPane(paneId: string): Promise<void> {
194 |   await executeTmux(`kill-pane -t '${paneId}'`);
195 | }
196 | 
197 | /**
198 |  * Split a tmux pane horizontally or vertically
199 |  */
200 | export async function splitPane(
201 |   targetPaneId: string,
202 |   direction: 'horizontal' | 'vertical' = 'vertical',
203 |   size?: number
204 | ): Promise<TmuxPane | null> {
205 |   // Build the split-window command
206 |   let splitCommand = 'split-window';
207 | 
208 |   // Add direction flag (-h for horizontal, -v for vertical)
209 |   if (direction === 'horizontal') {
210 |     splitCommand += ' -h';
211 |   } else {
212 |     splitCommand += ' -v';
213 |   }
214 | 
215 |   // Add target pane
216 |   splitCommand += ` -t '${targetPaneId}'`;
217 | 
218 |   // Add size if specified (as percentage)
219 |   if (size !== undefined && size > 0 && size < 100) {
220 |     splitCommand += ` -p ${size}`;
221 |   }
222 | 
223 |   // Execute the split command
224 |   await executeTmux(splitCommand);
225 | 
226 |   // Get the window ID from the target pane to list all panes
227 |   const windowInfo = await executeTmux(`display-message -p -t '${targetPaneId}' '#{window_id}'`);
228 | 
229 |   // List all panes in the window to find the newly created one
230 |   const panes = await listPanes(windowInfo);
231 | 
232 |   // The newest pane is typically the last one in the list
233 |   return panes.length > 0 ? panes[panes.length - 1] : null;
234 | }
235 | 
236 | // Map to track ongoing command executions
237 | const activeCommands = new Map<string, CommandExecution>();
238 | 
239 | const startMarkerText = 'TMUX_MCP_START';
240 | const endMarkerPrefix = "TMUX_MCP_DONE_";
241 | 
242 | // Execute a command in a tmux pane and track its execution
243 | export async function executeCommand(paneId: string, command: string, rawMode?: boolean, noEnter?: boolean): Promise<string> {
244 |   // Generate unique ID for this command execution
245 |   const commandId = uuidv4();
246 | 
247 |   let fullCommand: string;
248 |   if (rawMode || noEnter) {
249 |     fullCommand = command;
250 |   } else {
251 |     const endMarkerText = getEndMarkerText();
252 |     fullCommand = `echo "${startMarkerText}"; ${command}; echo "${endMarkerText}"`;
253 |   }
254 | 
255 |   // Store command in tracking map
256 |   activeCommands.set(commandId, {
257 |     id: commandId,
258 |     paneId,
259 |     command,
260 |     status: 'pending',
261 |     startTime: new Date(),
262 |     rawMode: rawMode || noEnter
263 |   });
264 | 
265 |   // Send the command to the tmux pane
266 |   if (noEnter) {
267 |     // Check if this is a special key (e.g., Up, Down, Left, Right, Escape, Tab, etc.)
268 |     // Special keys in tmux are typically capitalized or have special names
269 |     const specialKeys = ['Up', 'Down', 'Left', 'Right', 'Escape', 'Tab', 'Enter', 'Space',
270 |       'BSpace', 'Delete', 'Home', 'End', 'PageUp', 'PageDown',
271 |       'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'];
272 | 
273 |     if (specialKeys.includes(fullCommand)) {
274 |       // Send special key as-is
275 |       await executeTmux(`send-keys -t '${paneId}' ${fullCommand}`);
276 |     } else {
277 |       // For regular text, send each character individually to ensure proper processing
278 |       // This handles both single characters (like 'q', 'f') and strings (like 'beam')
279 |       for (const char of fullCommand) {
280 |         await executeTmux(`send-keys -t '${paneId}' '${char.replace(/'/g, "'\\''")}'`);
281 |       }
282 |     }
283 |   } else {
284 |     await executeTmux(`send-keys -t '${paneId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`);
285 |   }
286 | 
287 |   return commandId;
288 | }
289 | 
290 | export async function checkCommandStatus(commandId: string): Promise<CommandExecution | null> {
291 |   const command = activeCommands.get(commandId);
292 |   if (!command) return null;
293 | 
294 |   if (command.status !== 'pending') return command;
295 | 
296 |   const content = await capturePaneContent(command.paneId, 1000);
297 | 
298 |   if (command.rawMode) {
299 |     command.result = 'Status tracking unavailable for rawMode commands. Use capture-pane to monitor interactive apps instead.';
300 |     return command;
301 |   }
302 | 
303 |   // Find the last occurrence of the markers
304 |   const startIndex = content.lastIndexOf(startMarkerText);
305 |   const endIndex = content.lastIndexOf(endMarkerPrefix);
306 | 
307 |   if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
308 |     command.result = "Command output could not be captured properly";
309 |     return command;
310 |   }
311 | 
312 |   // Extract exit code from the end marker line
313 |   const endLine = content.substring(endIndex).split('\n')[0];
314 |   const endMarkerRegex = new RegExp(`${endMarkerPrefix}(\\d+)`);
315 |   const exitCodeMatch = endLine.match(endMarkerRegex);
316 | 
317 |   if (exitCodeMatch) {
318 |     const exitCode = parseInt(exitCodeMatch[1], 10);
319 | 
320 |     command.status = exitCode === 0 ? 'completed' : 'error';
321 |     command.exitCode = exitCode;
322 | 
323 |     // Extract output between the start and end markers
324 |     const outputStart = startIndex + startMarkerText.length;
325 |     const outputContent = content.substring(outputStart, endIndex).trim();
326 | 
327 |     command.result = outputContent.substring(outputContent.indexOf('\n') + 1).trim();
328 | 
329 |     // Update in map
330 |     activeCommands.set(commandId, command);
331 |   }
332 | 
333 |   return command;
334 | }
335 | 
336 | // Get command by ID
337 | export function getCommand(commandId: string): CommandExecution | null {
338 |   return activeCommands.get(commandId) || null;
339 | }
340 | 
341 | // Get all active command IDs
342 | export function getActiveCommandIds(): string[] {
343 |   return Array.from(activeCommands.keys());
344 | }
345 | 
346 | // Clean up completed commands older than a certain time
347 | export function cleanupOldCommands(maxAgeMinutes: number = 60): void {
348 |   const now = new Date();
349 | 
350 |   for (const [id, command] of activeCommands.entries()) {
351 |     const ageMinutes = (now.getTime() - command.startTime.getTime()) / (1000 * 60);
352 | 
353 |     if (command.status !== 'pending' && ageMinutes > maxAgeMinutes) {
354 |       activeCommands.delete(id);
355 |     }
356 |   }
357 | }
358 | 
359 | function getEndMarkerText(): string {
360 |   return shellConfig.type === 'fish'
361 |     ? `${endMarkerPrefix}$status`
362 |     : `${endMarkerPrefix}$?`;
363 | }
364 | 
365 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { parseArgs } from 'node:util';
  4 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  6 | import { z } from "zod";
  7 | import * as tmux from "./tmux.js";
  8 | 
  9 | // Create MCP server
 10 | const server = new McpServer({
 11 |   name: "tmux-mcp",
 12 |   version: "0.2.2"
 13 | }, {
 14 |   capabilities: {
 15 |     resources: {
 16 |       subscribe: true,
 17 |       listChanged: true
 18 |     },
 19 |     tools: {
 20 |       listChanged: true
 21 |     },
 22 |     logging: {}
 23 |   }
 24 | });
 25 | 
 26 | // List all tmux sessions - Tool
 27 | server.tool(
 28 |   "list-sessions",
 29 |   "List all active tmux sessions",
 30 |   {},
 31 |   async () => {
 32 |     try {
 33 |       const sessions = await tmux.listSessions();
 34 |       return {
 35 |         content: [{
 36 |           type: "text",
 37 |           text: JSON.stringify(sessions, null, 2)
 38 |         }]
 39 |       };
 40 |     } catch (error) {
 41 |       return {
 42 |         content: [{
 43 |           type: "text",
 44 |           text: `Error listing tmux sessions: ${error}`
 45 |         }],
 46 |         isError: true
 47 |       };
 48 |     }
 49 |   }
 50 | );
 51 | 
 52 | // Find session by name - Tool
 53 | server.tool(
 54 |   "find-session",
 55 |   "Find a tmux session by name",
 56 |   {
 57 |     name: z.string().describe("Name of the tmux session to find")
 58 |   },
 59 |   async ({ name }) => {
 60 |     try {
 61 |       const session = await tmux.findSessionByName(name);
 62 |       return {
 63 |         content: [{
 64 |           type: "text",
 65 |           text: session ? JSON.stringify(session, null, 2) : `Session not found: ${name}`
 66 |         }]
 67 |       };
 68 |     } catch (error) {
 69 |       return {
 70 |         content: [{
 71 |           type: "text",
 72 |           text: `Error finding tmux session: ${error}`
 73 |         }],
 74 |         isError: true
 75 |       };
 76 |     }
 77 |   }
 78 | );
 79 | 
 80 | // List windows in a session - Tool
 81 | server.tool(
 82 |   "list-windows",
 83 |   "List windows in a tmux session",
 84 |   {
 85 |     sessionId: z.string().describe("ID of the tmux session")
 86 |   },
 87 |   async ({ sessionId }) => {
 88 |     try {
 89 |       const windows = await tmux.listWindows(sessionId);
 90 |       return {
 91 |         content: [{
 92 |           type: "text",
 93 |           text: JSON.stringify(windows, null, 2)
 94 |         }]
 95 |       };
 96 |     } catch (error) {
 97 |       return {
 98 |         content: [{
 99 |           type: "text",
100 |           text: `Error listing windows: ${error}`
101 |         }],
102 |         isError: true
103 |       };
104 |     }
105 |   }
106 | );
107 | 
108 | // List panes in a window - Tool
109 | server.tool(
110 |   "list-panes",
111 |   "List panes in a tmux window",
112 |   {
113 |     windowId: z.string().describe("ID of the tmux window")
114 |   },
115 |   async ({ windowId }) => {
116 |     try {
117 |       const panes = await tmux.listPanes(windowId);
118 |       return {
119 |         content: [{
120 |           type: "text",
121 |           text: JSON.stringify(panes, null, 2)
122 |         }]
123 |       };
124 |     } catch (error) {
125 |       return {
126 |         content: [{
127 |           type: "text",
128 |           text: `Error listing panes: ${error}`
129 |         }],
130 |         isError: true
131 |       };
132 |     }
133 |   }
134 | );
135 | 
136 | // Capture pane content - Tool
137 | server.tool(
138 |   "capture-pane",
139 |   "Capture content from a tmux pane with configurable lines count and optional color preservation",
140 |   {
141 |     paneId: z.string().describe("ID of the tmux pane"),
142 |     lines: z.string().optional().describe("Number of lines to capture"),
143 |     colors: z.boolean().optional().describe("Include color/escape sequences for text and background attributes in output")
144 |   },
145 |   async ({ paneId, lines, colors }) => {
146 |     try {
147 |       // Parse lines parameter if provided
148 |       const linesCount = lines ? parseInt(lines, 10) : undefined;
149 |       const includeColors = colors || false;
150 |       const content = await tmux.capturePaneContent(paneId, linesCount, includeColors);
151 |       return {
152 |         content: [{
153 |           type: "text",
154 |           text: content || "No content captured"
155 |         }]
156 |       };
157 |     } catch (error) {
158 |       return {
159 |         content: [{
160 |           type: "text",
161 |           text: `Error capturing pane content: ${error}`
162 |         }],
163 |         isError: true
164 |       };
165 |     }
166 |   }
167 | );
168 | 
169 | // Create new session - Tool
170 | server.tool(
171 |   "create-session",
172 |   "Create a new tmux session",
173 |   {
174 |     name: z.string().describe("Name for the new tmux session")
175 |   },
176 |   async ({ name }) => {
177 |     try {
178 |       const session = await tmux.createSession(name);
179 |       return {
180 |         content: [{
181 |           type: "text",
182 |           text: session
183 |             ? `Session created: ${JSON.stringify(session, null, 2)}`
184 |             : `Failed to create session: ${name}`
185 |         }]
186 |       };
187 |     } catch (error) {
188 |       return {
189 |         content: [{
190 |           type: "text",
191 |           text: `Error creating session: ${error}`
192 |         }],
193 |         isError: true
194 |       };
195 |     }
196 |   }
197 | );
198 | 
199 | // Create new window - Tool
200 | server.tool(
201 |   "create-window",
202 |   "Create a new window in a tmux session",
203 |   {
204 |     sessionId: z.string().describe("ID of the tmux session"),
205 |     name: z.string().describe("Name for the new window")
206 |   },
207 |   async ({ sessionId, name }) => {
208 |     try {
209 |       const window = await tmux.createWindow(sessionId, name);
210 |       return {
211 |         content: [{
212 |           type: "text",
213 |           text: window
214 |             ? `Window created: ${JSON.stringify(window, null, 2)}`
215 |             : `Failed to create window: ${name}`
216 |         }]
217 |       };
218 |     } catch (error) {
219 |       return {
220 |         content: [{
221 |           type: "text",
222 |           text: `Error creating window: ${error}`
223 |         }],
224 |         isError: true
225 |       };
226 |     }
227 |   }
228 | );
229 | 
230 | // Kill session - Tool
231 | server.tool(
232 |   "kill-session",
233 |   "Kill a tmux session by ID",
234 |   {
235 |     sessionId: z.string().describe("ID of the tmux session to kill")
236 |   },
237 |   async ({ sessionId }) => {
238 |     try {
239 |       await tmux.killSession(sessionId);
240 |       return {
241 |         content: [{
242 |           type: "text",
243 |           text: `Session ${sessionId} has been killed`
244 |         }]
245 |       };
246 |     } catch (error) {
247 |       return {
248 |         content: [{
249 |           type: "text",
250 |           text: `Error killing session: ${error}`
251 |         }],
252 |         isError: true
253 |       };
254 |     }
255 |   }
256 | );
257 | 
258 | // Kill window - Tool
259 | server.tool(
260 |   "kill-window",
261 |   "Kill a tmux window by ID",
262 |   {
263 |     windowId: z.string().describe("ID of the tmux window to kill")
264 |   },
265 |   async ({ windowId }) => {
266 |     try {
267 |       await tmux.killWindow(windowId);
268 |       return {
269 |         content: [{
270 |           type: "text",
271 |           text: `Window ${windowId} has been killed`
272 |         }]
273 |       };
274 |     } catch (error) {
275 |       return {
276 |         content: [{
277 |           type: "text",
278 |           text: `Error killing window: ${error}`
279 |         }],
280 |         isError: true
281 |       };
282 |     }
283 |   }
284 | );
285 | 
286 | // Kill pane - Tool
287 | server.tool(
288 |   "kill-pane",
289 |   "Kill a tmux pane by ID",
290 |   {
291 |     paneId: z.string().describe("ID of the tmux pane to kill")
292 |   },
293 |   async ({ paneId }) => {
294 |     try {
295 |       await tmux.killPane(paneId);
296 |       return {
297 |         content: [{
298 |           type: "text",
299 |           text: `Pane ${paneId} has been killed`
300 |         }]
301 |       };
302 |     } catch (error) {
303 |       return {
304 |         content: [{
305 |           type: "text",
306 |           text: `Error killing pane: ${error}`
307 |         }],
308 |         isError: true
309 |       };
310 |     }
311 |   }
312 | );
313 | 
314 | // Split pane - Tool
315 | server.tool(
316 |   "split-pane",
317 |   "Split a tmux pane horizontally or vertically",
318 |   {
319 |     paneId: z.string().describe("ID of the tmux pane to split"),
320 |     direction: z.enum(["horizontal", "vertical"]).optional().describe("Split direction: 'horizontal' (side by side) or 'vertical' (top/bottom). Default is 'vertical'"),
321 |     size: z.number().min(1).max(99).optional().describe("Size of the new pane as percentage (1-99). Default is 50%")
322 |   },
323 |   async ({ paneId, direction, size }) => {
324 |     try {
325 |       const newPane = await tmux.splitPane(paneId, direction || 'vertical', size);
326 |       return {
327 |         content: [{
328 |           type: "text",
329 |           text: newPane
330 |             ? `Pane split successfully. New pane: ${JSON.stringify(newPane, null, 2)}`
331 |             : `Failed to split pane ${paneId}`
332 |         }]
333 |       };
334 |     } catch (error) {
335 |       return {
336 |         content: [{
337 |           type: "text",
338 |           text: `Error splitting pane: ${error}`
339 |         }],
340 |         isError: true
341 |       };
342 |     }
343 |   }
344 | );
345 | 
346 | // Execute command in pane - Tool
347 | server.tool(
348 |   "execute-command",
349 |   "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",
350 |   {
351 |     paneId: z.string().describe("ID of the tmux pane"),
352 |     command: z.string().describe("Command to execute"),
353 |     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."),
354 |     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.")
355 |   },
356 |   async ({ paneId, command, rawMode, noEnter }) => {
357 |     try {
358 |       // If noEnter is true, automatically apply rawMode
359 |       const effectiveRawMode = noEnter || rawMode;
360 |       const commandId = await tmux.executeCommand(paneId, command, effectiveRawMode, noEnter);
361 | 
362 |       if (effectiveRawMode) {
363 |         const modeText = noEnter ? "Keys sent without Enter" : "Interactive command started (rawMode)";
364 |         return {
365 |           content: [{
366 |             type: "text",
367 |             text: `${modeText}.\n\nStatus tracking is disabled.\nUse 'capture-pane' with paneId '${paneId}' to verify the command outcome.\n\nCommand ID: ${commandId}`
368 |           }]
369 |         };
370 |       }
371 | 
372 |       // Create the resource URI for this command's results
373 |       const resourceUri = `tmux://command/${commandId}/result`;
374 | 
375 |       return {
376 |         content: [{
377 |           type: "text",
378 |           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.`
379 |         }]
380 |       };
381 |     } catch (error) {
382 |       return {
383 |         content: [{
384 |           type: "text",
385 |           text: `Error executing command: ${error}`
386 |         }],
387 |         isError: true
388 |       };
389 |     }
390 |   }
391 | );
392 | 
393 | // Get command result - Tool
394 | server.tool(
395 |   "get-command-result",
396 |   "Get the result of an executed command",
397 |   {
398 |     commandId: z.string().describe("ID of the executed command")
399 |   },
400 |   async ({ commandId }) => {
401 |     try {
402 |       // Check and update command status
403 |       const command = await tmux.checkCommandStatus(commandId);
404 | 
405 |       if (!command) {
406 |         return {
407 |           content: [{
408 |             type: "text",
409 |             text: `Command not found: ${commandId}`
410 |           }],
411 |           isError: true
412 |         };
413 |       }
414 | 
415 |       // Format the response based on command status
416 |       let resultText;
417 |       if (command.status === 'pending') {
418 |         if (command.result) {
419 |           resultText = `Status: ${command.status}\nCommand: ${command.command}\n\n--- Message ---\n${command.result}`;
420 |         } else {
421 |           resultText = `Command still executing...\nStarted: ${command.startTime.toISOString()}\nCommand: ${command.command}`;
422 |         }
423 |       } else {
424 |         resultText = `Status: ${command.status}\nExit code: ${command.exitCode}\nCommand: ${command.command}\n\n--- Output ---\n${command.result}`;
425 |       }
426 | 
427 |       return {
428 |         content: [{
429 |           type: "text",
430 |           text: resultText
431 |         }]
432 |       };
433 |     } catch (error) {
434 |       return {
435 |         content: [{
436 |           type: "text",
437 |           text: `Error retrieving command result: ${error}`
438 |         }],
439 |         isError: true
440 |       };
441 |     }
442 |   }
443 | );
444 | 
445 | // Expose tmux session list as a resource
446 | server.resource(
447 |   "Tmux Sessions",
448 |   "tmux://sessions",
449 |   async () => {
450 |     try {
451 |       const sessions = await tmux.listSessions();
452 |       return {
453 |         contents: [{
454 |           uri: "tmux://sessions",
455 |           text: JSON.stringify(sessions.map(session => ({
456 |             id: session.id,
457 |             name: session.name,
458 |             attached: session.attached,
459 |             windows: session.windows
460 |           })), null, 2)
461 |         }]
462 |       };
463 |     } catch (error) {
464 |       return {
465 |         contents: [{
466 |           uri: "tmux://sessions",
467 |           text: `Error listing tmux sessions: ${error}`
468 |         }]
469 |       };
470 |     }
471 |   }
472 | );
473 | 
474 | // Expose pane content as a resource
475 | server.resource(
476 |   "Tmux Pane Content",
477 |   new ResourceTemplate("tmux://pane/{paneId}", {
478 |     list: async () => {
479 |       try {
480 |         // Get all sessions
481 |         const sessions = await tmux.listSessions();
482 |         const paneResources = [];
483 | 
484 |         // For each session, get all windows
485 |         for (const session of sessions) {
486 |           const windows = await tmux.listWindows(session.id);
487 | 
488 |           // For each window, get all panes
489 |           for (const window of windows) {
490 |             const panes = await tmux.listPanes(window.id);
491 | 
492 |             // For each pane, create a resource with descriptive name
493 |             for (const pane of panes) {
494 |               paneResources.push({
495 |                 name: `Pane: ${session.name} - ${pane.id} - ${pane.title} ${pane.active ? "(active)" : ""}`,
496 |                 uri: `tmux://pane/${pane.id}`,
497 |                 description: `Content from pane ${pane.id} - ${pane.title} in session ${session.name}`
498 |               });
499 |             }
500 |           }
501 |         }
502 | 
503 |         return {
504 |           resources: paneResources
505 |         };
506 |       } catch (error) {
507 |         server.server.sendLoggingMessage({
508 |           level: 'error',
509 |           data: `Error listing panes: ${error}`
510 |         });
511 | 
512 |         return { resources: [] };
513 |       }
514 |     }
515 |   }),
516 |   async (uri, { paneId }) => {
517 |     try {
518 |       // Ensure paneId is a string
519 |       const paneIdStr = Array.isArray(paneId) ? paneId[0] : paneId;
520 |       // Default to no colors for resources to maintain clean programmatic access
521 |       const content = await tmux.capturePaneContent(paneIdStr, 200, false);
522 |       return {
523 |         contents: [{
524 |           uri: uri.href,
525 |           text: content || "No content captured"
526 |         }]
527 |       };
528 |     } catch (error) {
529 |       return {
530 |         contents: [{
531 |           uri: uri.href,
532 |           text: `Error capturing pane content: ${error}`
533 |         }]
534 |       };
535 |     }
536 |   }
537 | );
538 | 
539 | // Create dynamic resource for command executions
540 | server.resource(
541 |   "Command Execution Result",
542 |   new ResourceTemplate("tmux://command/{commandId}/result", {
543 |     list: async () => {
544 |       // Only list active commands that aren't too old
545 |       tmux.cleanupOldCommands(10); // Clean commands older than 10 minutes
546 | 
547 |       const resources = [];
548 |       for (const id of tmux.getActiveCommandIds()) {
549 |         const command = tmux.getCommand(id);
550 |         if (command) {
551 |           resources.push({
552 |             name: `Command: ${command.command.substring(0, 30)}${command.command.length > 30 ? '...' : ''}`,
553 |             uri: `tmux://command/${id}/result`,
554 |             description: `Execution status: ${command.status}`
555 |           });
556 |         }
557 |       }
558 | 
559 |       return { resources };
560 |     }
561 |   }),
562 |   async (uri, { commandId }) => {
563 |     try {
564 |       // Ensure commandId is a string
565 |       const commandIdStr = Array.isArray(commandId) ? commandId[0] : commandId;
566 | 
567 |       // Check command status
568 |       const command = await tmux.checkCommandStatus(commandIdStr);
569 | 
570 |       if (!command) {
571 |         return {
572 |           contents: [{
573 |             uri: uri.href,
574 |             text: `Command not found: ${commandIdStr}`
575 |           }]
576 |         };
577 |       }
578 | 
579 |       // Format the response based on command status
580 |       let resultText;
581 |       if (command.status === 'pending') {
582 |         // For rawMode commands, we set a result message while status remains 'pending'
583 |         // since we can't track their actual completion
584 |         if (command.result) {
585 |           resultText = `Status: ${command.status}\nCommand: ${command.command}\n\n--- Message ---\n${command.result}`;
586 |         } else {
587 |           resultText = `Command still executing...\nStarted: ${command.startTime.toISOString()}\nCommand: ${command.command}`;
588 |         }
589 |       } else {
590 |         resultText = `Status: ${command.status}\nExit code: ${command.exitCode}\nCommand: ${command.command}\n\n--- Output ---\n${command.result}`;
591 |       }
592 | 
593 |       return {
594 |         contents: [{
595 |           uri: uri.href,
596 |           text: resultText
597 |         }]
598 |       };
599 |     } catch (error) {
600 |       return {
601 |         contents: [{
602 |           uri: uri.href,
603 |           text: `Error retrieving command result: ${error}`
604 |         }]
605 |       };
606 |     }
607 |   }
608 | );
609 | 
610 | async function main() {
611 |   try {
612 |     const { values } = parseArgs({
613 |       options: {
614 |         'shell-type': { type: 'string', default: 'bash', short: 's' }
615 |       }
616 |     });
617 | 
618 |     // Set shell configuration
619 |     tmux.setShellConfig({
620 |       type: values['shell-type'] as string
621 |     });
622 | 
623 |     // Start the MCP server
624 |     const transport = new StdioServerTransport();
625 |     await server.connect(transport);
626 |   } catch (error) {
627 |     console.error("Failed to start MCP server:", error);
628 |     process.exit(1);
629 |   }
630 | }
631 | 
632 | main().catch(error => {
633 |   console.error("Fatal error:", error);
634 |   process.exit(1);
635 | });
636 | 
```