@ebowwa/terminal 0.2.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.
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Terminal MCP Server
4
+ *
5
+ * Exposes terminal session management via Model Context Protocol
6
+ * Integrates with tmux, SSH, PTY, and file operations
7
+ */
8
+
9
+ import type {
10
+ TerminalSession,
11
+ SessionInfo,
12
+ SSHOptions,
13
+ SCPOptions,
14
+ } from "../index.js";
15
+
16
+ // Re-export terminal functions
17
+ import {
18
+ // Session management
19
+ getOrCreateSession,
20
+ getSession,
21
+ getAllSessions,
22
+ getAllSessionInfo,
23
+ getSessionInfo,
24
+ closeSession,
25
+ cleanupStaleSessions,
26
+ getSessionsByHost,
27
+ writeToSession,
28
+ resizeSession,
29
+ getSessionCount,
30
+
31
+ // PTY operations
32
+ createPTYSession,
33
+ writeToPTY,
34
+ setPTYSize,
35
+ readFromPTY,
36
+ closePTYSession,
37
+ getPTYSession,
38
+ getActivePTYSessions,
39
+
40
+ // Tmux operations
41
+ generateSessionName,
42
+ isTmuxInstalled,
43
+ ensureTmux,
44
+ listTmuxSessions,
45
+ hasTmuxSession,
46
+ createOrAttachTmuxSession,
47
+ killTmuxSession,
48
+ getTmuxSessionInfo,
49
+ cleanupOldTmuxSessions,
50
+ getTmuxResourceUsage,
51
+ sendCommandToPane,
52
+ splitPane,
53
+ capturePane,
54
+ getPaneHistory,
55
+ switchWindow,
56
+ switchPane,
57
+ renameWindow,
58
+ killPane,
59
+ getDetailedSessionInfo,
60
+ listSessionWindows,
61
+ listWindowPanes,
62
+
63
+ // SSH operations
64
+ execSSH,
65
+ execSSHParallel,
66
+ testSSHConnection,
67
+
68
+ // SCP operations
69
+ scpUpload,
70
+ scpDownload,
71
+
72
+ // File operations
73
+ listFiles,
74
+ previewFile,
75
+ sanitizePath,
76
+
77
+ // Fingerprint operations
78
+ getSSHFingerprint,
79
+ getLocalKeyFingerprint,
80
+ testSSHKeyConnection,
81
+
82
+ // Connection pool
83
+ getSSHPool,
84
+ closeGlobalSSHPool,
85
+ getActiveSSHConnections,
86
+ } from "../index.js";
87
+
88
+ // ==============
89
+ // MCP Tools
90
+ //=============
91
+
92
+ /**
93
+ * List all active terminal sessions
94
+ */
95
+ async function listSessionsTool(): Promise<string> {
96
+ const sessions = getAllSessionInfo();
97
+ const lines = [
98
+ "🖥️ Active Terminal Sessions",
99
+ "=" .repeat(50),
100
+ "",
101
+ `Total sessions: ${sessions.length}`,
102
+ "",
103
+ ];
104
+
105
+ for (const session of sessions) {
106
+ const active = session.active ? "🟢" : "⚪";
107
+ lines.push(`${active} ${session.id}`);
108
+ lines.push(` Host: ${session.host}`);
109
+ lines.push(` Type: ${session.type}`);
110
+ lines.push(` Created: ${new Date(session.createdAt).toLocaleString()}`);
111
+ if (session.lastActivity) {
112
+ lines.push(` Last activity: ${new Date(session.lastActivity).toLocaleString()}`);
113
+ }
114
+ lines.push("");
115
+ }
116
+
117
+ return lines.join("\n");
118
+ }
119
+
120
+ /**
121
+ * Get detailed info about a specific session
122
+ */
123
+ async function getSessionInfoTool(sessionId: string): Promise<string> {
124
+ try {
125
+ const info = await getSessionInfo(sessionId);
126
+ const lines = [
127
+ `📋 Session: ${sessionId}`,
128
+ "=" .repeat(50),
129
+ "",
130
+ `Host: ${info.host}`,
131
+ `Type: ${info.type}`,
132
+ `Active: ${info.active ? "Yes" : "No"}`,
133
+ `Created: ${new Date(info.createdAt).toLocaleString()}`,
134
+ ];
135
+
136
+ if (info.lastActivity) {
137
+ lines.push(`Last activity: ${new Date(info.lastActivity).toLocaleString()}`);
138
+ }
139
+
140
+ const session = getSession(sessionId);
141
+ if (session) {
142
+ lines.push(``);
143
+ lines.push(`PTY session: ${session.ptyId || "N/A"}`);
144
+ lines.push(`WebSocket attached: ${session.wsAttached ? "Yes" : "No"}`);
145
+ }
146
+
147
+ return lines.join("\n");
148
+ } catch (error) {
149
+ throw new Error(`Failed to get session info: ${error}`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Create a new terminal session
155
+ */
156
+ async function createSessionTool(host: string, type: "ssh" | "pty" = "ssh", command?: string): Promise<string> {
157
+ try {
158
+ const session = await getOrCreateSession(host, {
159
+ type,
160
+ command,
161
+ });
162
+
163
+ return `✓ Created session ${session.id} for ${host} (${type})`;
164
+ } catch (error) {
165
+ throw new Error(`Failed to create session: ${error}`);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Write a command to a session
171
+ */
172
+ async function writeCommandTool(sessionId: string, command: string): Promise<string> {
173
+ try {
174
+ const session = getSession(sessionId);
175
+ if (!session) {
176
+ throw new Error(`Session ${sessionId} not found`);
177
+ }
178
+
179
+ await writeToSession(sessionId, command);
180
+ return `✓ Command sent to session ${sessionId}`;
181
+ } catch (error) {
182
+ throw new Error(`Failed to write command: ${error}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Resize a session
188
+ */
189
+ async function resizeSessionTool(sessionId: string, rows: number, cols: number): Promise<string> {
190
+ try {
191
+ await resizeSession(sessionId, rows, cols);
192
+ return `✓ Resized session ${sessionId} to ${rows}x${cols}`;
193
+ } catch (error) {
194
+ throw new Error(`Failed to resize session: ${error}`);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Close a session
200
+ */
201
+ async function closeSessionTool(sessionId: string): Promise<string> {
202
+ try {
203
+ await closeSession(sessionId);
204
+ return `✓ Closed session ${sessionId}`;
205
+ } catch (error) {
206
+ throw new Error(`Failed to close session: ${error}`);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Execute a command via SSH
212
+ */
213
+ async function execSSHTool(host: string, command: string, options?: Partial<SSHOptions>): Promise<string> {
214
+ try {
215
+ const result = await execSSH(host, command, options);
216
+ return result.stdout || result.stderr || "Command executed";
217
+ } catch (error) {
218
+ throw new Error(`SSH command failed: ${error}`);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Test SSH connection
224
+ */
225
+ async function testConnectionTool(host: string): Promise<string> {
226
+ try {
227
+ const result = await testSSHConnection(host);
228
+ if (result.success) {
229
+ return `✓ Connection to ${host} successful\nLatency: ${result.latency}ms`;
230
+ } else {
231
+ throw new Error(result.error || "Connection failed");
232
+ }
233
+ } catch (error) {
234
+ throw new Error(`Connection test failed: ${error}`);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * List tmux sessions
240
+ */
241
+ async function listTmuxSessionsTool(): Promise<string> {
242
+ try {
243
+ await ensureTmux();
244
+ const sessions = await listTmuxSessions();
245
+ const lines = [
246
+ "🎬 Tmux Sessions",
247
+ "=" .repeat(50),
248
+ "",
249
+ ];
250
+
251
+ if (sessions.length === 0) {
252
+ lines.push("No active tmux sessions");
253
+ } else {
254
+ for (const session of sessions) {
255
+ lines.push(`${session.name}`);
256
+ lines.push(` Windows: ${session.windows}`);
257
+ lines.push(` Created: ${new Date(session.created).toLocaleString()}`);
258
+ lines.push("");
259
+ }
260
+ }
261
+
262
+ return lines.join("\n");
263
+ } catch (error) {
264
+ throw new Error(`Failed to list tmux sessions: ${error}`);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * List files on remote host
270
+ */
271
+ async function listFilesTool(host: string, path: string = "."): Promise<string> {
272
+ try {
273
+ const files = await listFiles(host, path);
274
+ const lines = [
275
+ `📁 Files on ${host}:${path}`,
276
+ "=" .repeat(50),
277
+ "",
278
+ ];
279
+
280
+ for (const file of files) {
281
+ const icon = file.type === "directory" ? "📁" : "📄";
282
+ const size = file.size ? ` (${file.size} bytes)` : "";
283
+ lines.push(`${icon} ${file.name}${size}`);
284
+ }
285
+
286
+ return lines.join("\n");
287
+ } catch (error) {
288
+ throw new Error(`Failed to list files: ${error}`);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Upload file via SCP
294
+ */
295
+ async function uploadFileTool(host: string, localPath: string, remotePath: string): Promise<string> {
296
+ try {
297
+ await scpUpload(host, { localPath, remotePath });
298
+ return `✓ Uploaded ${localPath} to ${host}:${remotePath}`;
299
+ } catch (error) {
300
+ throw new Error(`SCP upload failed: ${error}`);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Download file via SCP
306
+ */
307
+ async function downloadFileTool(host: string, remotePath: string, localPath: string): Promise<string> {
308
+ try {
309
+ await scpDownload(host, { remotePath, localPath });
310
+ return `✓ Downloaded ${host}:${remotePath} to ${localPath}`;
311
+ } catch (error) {
312
+ throw new Error(`SCP download failed: ${error}`);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Get active SSH connections
318
+ */
319
+ async function getActiveConnectionsTool(): Promise<string> {
320
+ try {
321
+ const pool = getSSHPool();
322
+ const connections = getActiveSSHConnections();
323
+ const lines = [
324
+ "🔗 Active SSH Connections",
325
+ "=" .repeat(50),
326
+ "",
327
+ `Total connections: ${connections.length}`,
328
+ "",
329
+ ];
330
+
331
+ for (const conn of connections) {
332
+ lines.push(`${conn.host}`);
333
+ lines.push(` Connected: ${conn.connected ? "Yes" : "No"}`);
334
+ lines.push(` Port: ${conn.port || 22}`);
335
+ lines.push("");
336
+ }
337
+
338
+ return lines.join("\n");
339
+ } catch (error) {
340
+ throw new Error(`Failed to get connections: ${error}`);
341
+ }
342
+ }
343
+
344
+ // ==============
345
+ // MCP Server
346
+ //=============
347
+
348
+ const MCP_PORT = parseInt(process.env.MCP_PORT || "8913");
349
+
350
+ Bun.serve({
351
+ port: MCP_PORT,
352
+ fetch: async (req) => {
353
+ const url = new URL(req.url);
354
+
355
+ // Health check
356
+ if (url.pathname === "/health") {
357
+ return Response.json({ status: "ok", port: MCP_PORT, service: "terminal-mcp" });
358
+ }
359
+
360
+ // MCP endpoint
361
+ if (url.pathname === "/mcp") {
362
+ if (req.method === "POST") {
363
+ const body = await req.json();
364
+ const { tool, args } = body;
365
+
366
+ try {
367
+ let result;
368
+
369
+ switch (tool) {
370
+ // Session management
371
+ case "list_sessions":
372
+ result = await listSessionsTool();
373
+ break;
374
+ case "get_session_info":
375
+ result = await getSessionInfoTool(args?.session_id);
376
+ break;
377
+ case "create_session":
378
+ result = await createSessionTool(args?.host, args?.type, args?.command);
379
+ break;
380
+ case "write_command":
381
+ result = await writeCommandTool(args?.session_id, args?.command);
382
+ break;
383
+ case "resize_session":
384
+ result = await resizeSessionTool(args?.session_id, args?.rows, args?.cols);
385
+ break;
386
+ case "close_session":
387
+ result = await closeSessionTool(args?.session_id);
388
+ break;
389
+
390
+ // SSH operations
391
+ case "exec_ssh":
392
+ result = await execSSHTool(args?.host, args?.command, args?.options);
393
+ break;
394
+ case "test_connection":
395
+ result = await testConnectionTool(args?.host);
396
+ break;
397
+
398
+ // Tmux operations
399
+ case "list_tmux_sessions":
400
+ result = await listTmuxSessionsTool();
401
+ break;
402
+
403
+ // File operations
404
+ case "list_files":
405
+ result = await listFilesTool(args?.host, args?.path);
406
+ break;
407
+ case "upload_file":
408
+ result = await uploadFileTool(args?.host, args?.local_path, args?.remote_path);
409
+ break;
410
+ case "download_file":
411
+ result = await downloadFileTool(args?.host, args?.remote_path, args?.local_path);
412
+ break;
413
+
414
+ // Connection management
415
+ case "get_active_connections":
416
+ result = await getActiveConnectionsTool();
417
+ break;
418
+
419
+ default:
420
+ return Response.json({ error: `Unknown tool: ${tool}` }, { status: 400 });
421
+ }
422
+
423
+ return Response.json({ result });
424
+ } catch (error) {
425
+ return Response.json({ error: String(error) }, { status: 500 });
426
+ }
427
+ }
428
+
429
+ // GET returns available tools
430
+ return Response.json({
431
+ name: "terminal-mcp",
432
+ version: "1.0.0",
433
+ description: "Terminal session management with tmux, SSH, PTY, and file operations",
434
+ tools: [
435
+ // Session management
436
+ { name: "list_sessions", description: "List all active terminal sessions" },
437
+ { name: "get_session_info", description: "Get detailed info about a session", args: ["session_id"] },
438
+ { name: "create_session", description: "Create a new terminal session", args: ["host", "type?", "command?"] },
439
+ { name: "write_command", description: "Write a command to a session", args: ["session_id", "command"] },
440
+ { name: "resize_session", description: "Resize a session terminal", args: ["session_id", "rows", "cols"] },
441
+ { name: "close_session", description: "Close a terminal session", args: ["session_id"] },
442
+
443
+ // SSH operations
444
+ { name: "exec_ssh", description: "Execute command via SSH", args: ["host", "command", "options?"] },
445
+ { name: "test_connection", description: "Test SSH connection", args: ["host"] },
446
+
447
+ // Tmux operations
448
+ { name: "list_tmux_sessions", description: "List all tmux sessions" },
449
+
450
+ // File operations
451
+ { name: "list_files", description: "List files on remote host", args: ["host", "path?"] },
452
+ { name: "upload_file", description: "Upload file via SCP", args: ["host", "local_path", "remote_path"] },
453
+ { name: "download_file", description: "Download file via SCP", args: ["host", "remote_path", "local_path"] },
454
+
455
+ // Connection management
456
+ { name: "get_active_connections", description: "Get active SSH connections" },
457
+ ]
458
+ });
459
+ }
460
+
461
+ return Response.json({ error: "Not found" }, { status: 404 });
462
+ },
463
+ });
464
+
465
+ console.log(`🚀 Terminal MCP Server running on port ${MCP_PORT}`);
466
+ console.log(` Health: http://localhost:${MCP_PORT}/health`);
467
+ console.log(` MCP: http://localhost:${MCP_PORT}/mcp`);