@arearseth/tmux-mcp 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/build/index.js +25 -8
- package/build/tmux.js +25 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Model Context Protocol server that enables Claude Desktop to interact with and v
|
|
|
7
7
|
- List and search tmux sessions
|
|
8
8
|
- View and navigate tmux windows and panes
|
|
9
9
|
- Capture and expose terminal content from any pane
|
|
10
|
-
- Execute commands in tmux panes and retrieve results (use it at your own risk ⚠️)
|
|
10
|
+
- Execute commands in tmux panes and retrieve results across bash, zsh, fish, and tclsh shells (use it at your own risk ⚠️)
|
|
11
11
|
- Create new tmux sessions and windows
|
|
12
12
|
- Split panes horizontally or vertically with customizable sizes
|
|
13
13
|
- Kill tmux sessions, windows, and panes
|
|
@@ -40,7 +40,7 @@ Add this MCP server to your Claude Desktop configuration:
|
|
|
40
40
|
|
|
41
41
|
### MCP server options
|
|
42
42
|
|
|
43
|
-
You can optionally specify the
|
|
43
|
+
You can optionally specify the default shell the server should assume when wrapping commands. If unspecified it defaults to `bash`.
|
|
44
44
|
|
|
45
45
|
```json
|
|
46
46
|
"mcpServers": {
|
|
@@ -51,7 +51,7 @@ You can optionally specify the command line shell you are using, if unspecified
|
|
|
51
51
|
}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
The MCP server needs to know the shell
|
|
54
|
+
The CLI flag only sets the server-wide default. You can still override individual panes (or change the default at runtime) with the `set-shell-type` tool described below. The MCP server needs to know the shell when executing commands so it can wrap and read exit statuses correctly.
|
|
55
55
|
|
|
56
56
|
## Available Resources
|
|
57
57
|
|
|
@@ -72,6 +72,6 @@ The MCP server needs to know the shell only when executing commands, to properly
|
|
|
72
72
|
- `kill-session` - Kill a tmux session by ID
|
|
73
73
|
- `kill-window` - Kill a tmux window by ID
|
|
74
74
|
- `kill-pane` - Kill a tmux pane by ID
|
|
75
|
+
- `set-shell-type` - Configure the shell used for command execution (supports bash, zsh, fish, tclsh). Provide a paneId to override a single pane, or omit to adjust the default.
|
|
75
76
|
- `execute-command` - Execute a command in a tmux pane
|
|
76
77
|
- `get-command-result` - Get the result of an executed command
|
|
77
|
-
|
package/build/index.js
CHANGED
|
@@ -112,16 +112,30 @@ server.tool("list-panes", "List panes in a tmux window", {
|
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
// Capture pane content - Tool
|
|
115
|
-
server.tool("capture-pane", "Capture content from a tmux pane
|
|
115
|
+
server.tool("capture-pane", "Capture content from a tmux pane. Defaults to the last N lines, but you can provide tmux-style start/end offsets (like 0 and -) to walk the full scrollback.", {
|
|
116
116
|
paneId: z.string().describe("ID of the tmux pane"),
|
|
117
|
-
lines: z.string().optional().describe("Number of lines to capture"),
|
|
117
|
+
lines: z.string().optional().describe("Number of trailing lines to capture when start/end offsets are omitted (defaults to 200)"),
|
|
118
|
+
start: z.string().optional().describe("tmux -S offset; use 0 for the oldest line or a negative value to offset from the bottom"),
|
|
119
|
+
end: z.string().optional().describe("tmux -E offset; use - for the newest line or 0 for the active cursor line"),
|
|
118
120
|
colors: z.boolean().optional().describe("Include color/escape sequences for text and background attributes in output")
|
|
119
|
-
}, async ({ paneId, lines, colors }) => {
|
|
121
|
+
}, async ({ paneId, lines, start, end, colors }) => {
|
|
120
122
|
try {
|
|
121
123
|
// Parse lines parameter if provided
|
|
122
|
-
const
|
|
123
|
-
const includeColors = colors
|
|
124
|
-
const
|
|
124
|
+
const parsedLines = lines !== undefined ? parseInt(lines, 10) : undefined;
|
|
125
|
+
const includeColors = colors ?? false;
|
|
126
|
+
const options = {
|
|
127
|
+
includeColors
|
|
128
|
+
};
|
|
129
|
+
if (parsedLines !== undefined && !Number.isNaN(parsedLines) && parsedLines > 0) {
|
|
130
|
+
options.lines = parsedLines;
|
|
131
|
+
}
|
|
132
|
+
if (start !== undefined && start !== '') {
|
|
133
|
+
options.start = start;
|
|
134
|
+
}
|
|
135
|
+
if (end !== undefined && end !== '') {
|
|
136
|
+
options.end = end;
|
|
137
|
+
}
|
|
138
|
+
const content = await tmux.capturePaneContent(paneId, options);
|
|
125
139
|
return {
|
|
126
140
|
content: [{
|
|
127
141
|
type: "text",
|
|
@@ -287,7 +301,7 @@ server.tool("split-pane", "Split a tmux pane horizontally or vertically", {
|
|
|
287
301
|
}
|
|
288
302
|
});
|
|
289
303
|
// Configure shell type - Tool
|
|
290
|
-
server.tool("set-shell-type", "Configure the shell
|
|
304
|
+
server.tool("set-shell-type", "Configure the shell for command execution (bash, zsh, fish, tclsh). Provide paneId to override a specific pane.", {
|
|
291
305
|
type: shellTypeSchema,
|
|
292
306
|
paneId: z.string().optional().describe("ID of the tmux pane to override. Omit to change the default shell type.")
|
|
293
307
|
}, async ({ type, paneId }) => {
|
|
@@ -461,7 +475,10 @@ server.resource("Tmux Pane Content", new ResourceTemplate("tmux://pane/{paneId}"
|
|
|
461
475
|
// Ensure paneId is a string
|
|
462
476
|
const paneIdStr = Array.isArray(paneId) ? paneId[0] : paneId;
|
|
463
477
|
// Default to no colors for resources to maintain clean programmatic access
|
|
464
|
-
const content = await tmux.capturePaneContent(paneIdStr,
|
|
478
|
+
const content = await tmux.capturePaneContent(paneIdStr, {
|
|
479
|
+
lines: 200,
|
|
480
|
+
includeColors: false
|
|
481
|
+
});
|
|
465
482
|
return {
|
|
466
483
|
contents: [{
|
|
467
484
|
uri: uri.href,
|
package/build/tmux.js
CHANGED
|
@@ -2,7 +2,7 @@ import { exec as execCallback } from "child_process";
|
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
const exec = promisify(execCallback);
|
|
5
|
-
export const supportedShellTypes = ['bash', 'zsh', 'fish', '
|
|
5
|
+
export const supportedShellTypes = ['bash', 'zsh', 'fish', 'tclsh'];
|
|
6
6
|
const shellConfig = {
|
|
7
7
|
defaultType: 'bash',
|
|
8
8
|
paneOverrides: new Map()
|
|
@@ -17,12 +17,12 @@ export function setShellConfig(config) {
|
|
|
17
17
|
if (config.paneId) {
|
|
18
18
|
shellConfig.paneOverrides.set(config.paneId, normalized);
|
|
19
19
|
// Reset cached initialization so the helper can be installed on demand
|
|
20
|
-
|
|
20
|
+
tclshInitializedPanes.delete(config.paneId);
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
shellConfig.defaultType = normalized;
|
|
24
|
-
if (normalized !== '
|
|
25
|
-
|
|
24
|
+
if (normalized !== 'tclsh') {
|
|
25
|
+
tclshInitializedPanes.clear();
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
function resolveShellType(paneId) {
|
|
@@ -121,9 +121,16 @@ export async function listPanes(windowId) {
|
|
|
121
121
|
/**
|
|
122
122
|
* Capture content from a specific pane, by default the latest 200 lines.
|
|
123
123
|
*/
|
|
124
|
-
export async function capturePaneContent(paneId,
|
|
125
|
-
const
|
|
126
|
-
|
|
124
|
+
export async function capturePaneContent(paneId, options = {}) {
|
|
125
|
+
const { lines = 200, start, end, includeColors = false } = options;
|
|
126
|
+
const startValue = start !== undefined ? String(start) : `-${lines}`;
|
|
127
|
+
const endValue = end !== undefined ? String(end) : '-';
|
|
128
|
+
const commandParts = ['capture-pane', '-p'];
|
|
129
|
+
if (includeColors) {
|
|
130
|
+
commandParts.push('-e');
|
|
131
|
+
}
|
|
132
|
+
commandParts.push('-t', `'${paneId}'`, '-S', startValue, '-E', endValue);
|
|
133
|
+
return executeTmux(commandParts.join(' '));
|
|
127
134
|
}
|
|
128
135
|
/**
|
|
129
136
|
* Create a new tmux session
|
|
@@ -190,8 +197,8 @@ export async function splitPane(targetPaneId, direction = 'vertical', size) {
|
|
|
190
197
|
const activeCommands = new Map();
|
|
191
198
|
const startMarkerText = 'TMUX_MCP_START';
|
|
192
199
|
const endMarkerPrefix = "TMUX_MCP_DONE_";
|
|
193
|
-
// Track
|
|
194
|
-
const
|
|
200
|
+
// Track tclsh initialization per pane to keep terminal output minimal
|
|
201
|
+
const tclshInitializedPanes = new Set();
|
|
195
202
|
// Execute a command in a tmux pane and track its execution
|
|
196
203
|
export async function executeCommand(paneId, command, rawMode, noEnter) {
|
|
197
204
|
// Generate unique ID for this command execution
|
|
@@ -202,9 +209,9 @@ export async function executeCommand(paneId, command, rawMode, noEnter) {
|
|
|
202
209
|
fullCommand = command;
|
|
203
210
|
}
|
|
204
211
|
else {
|
|
205
|
-
if (shellType === '
|
|
206
|
-
await
|
|
207
|
-
fullCommand =
|
|
212
|
+
if (shellType === 'tclsh') {
|
|
213
|
+
await ensureTclshInitialized(paneId);
|
|
214
|
+
fullCommand = buildTclshCommand(command);
|
|
208
215
|
}
|
|
209
216
|
else {
|
|
210
217
|
fullCommand = buildWrappedCommand(command, shellType);
|
|
@@ -249,7 +256,7 @@ export async function checkCommandStatus(commandId) {
|
|
|
249
256
|
return null;
|
|
250
257
|
if (command.status !== 'pending')
|
|
251
258
|
return command;
|
|
252
|
-
const content = await capturePaneContent(command.paneId, 1000);
|
|
259
|
+
const content = await capturePaneContent(command.paneId, { lines: 1000 });
|
|
253
260
|
if (command.rawMode) {
|
|
254
261
|
command.result = 'Status tracking unavailable for rawMode commands. Use capture-pane to monitor interactive apps instead.';
|
|
255
262
|
return command;
|
|
@@ -307,7 +314,7 @@ function getEndMarkerText(shellType) {
|
|
|
307
314
|
if (shellType === 'fish') {
|
|
308
315
|
return `${endMarkerPrefix}$status`;
|
|
309
316
|
}
|
|
310
|
-
if (shellType === '
|
|
317
|
+
if (shellType === 'tclsh') {
|
|
311
318
|
return `${endMarkerPrefix}$::tmux_mcp_status`;
|
|
312
319
|
}
|
|
313
320
|
return `${endMarkerPrefix}$?`;
|
|
@@ -316,7 +323,7 @@ function buildWrappedCommand(command, shellType) {
|
|
|
316
323
|
const endMarkerText = getEndMarkerText(shellType);
|
|
317
324
|
return `echo "${startMarkerText}"; ${command}; echo "${endMarkerText}"`;
|
|
318
325
|
}
|
|
319
|
-
function
|
|
326
|
+
function buildTclshCommand(command) {
|
|
320
327
|
const escaped = escapeForTcl(command);
|
|
321
328
|
return `::tmux_mcp::run {${escaped}}`;
|
|
322
329
|
}
|
|
@@ -328,8 +335,8 @@ function escapeForTcl(command) {
|
|
|
328
335
|
.replace(/\{/g, '\\{')
|
|
329
336
|
.replace(/\}/g, '\\}');
|
|
330
337
|
}
|
|
331
|
-
async function
|
|
332
|
-
if (
|
|
338
|
+
async function ensureTclshInitialized(paneId) {
|
|
339
|
+
if (tclshInitializedPanes.has(paneId)) {
|
|
333
340
|
return;
|
|
334
341
|
}
|
|
335
342
|
const definitionCommand = [
|
|
@@ -348,5 +355,5 @@ async function ensureFcShellInitialized(paneId) {
|
|
|
348
355
|
].join(' ');
|
|
349
356
|
const escapedCommand = definitionCommand.replace(/'/g, "'\\''");
|
|
350
357
|
await executeTmux(`send-keys -t '${paneId}' '${escapedCommand}' Enter`);
|
|
351
|
-
|
|
358
|
+
tclshInitializedPanes.add(paneId);
|
|
352
359
|
}
|