@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.
- package/dist/client.d.ts +15 -0
- package/dist/client.js +45 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.js +12 -0
- package/dist/exec.d.ts +47 -0
- package/dist/exec.js +107 -0
- package/dist/files.d.ts +124 -0
- package/dist/files.js +436 -0
- package/dist/fingerprint.d.ts +67 -0
- package/dist/index.d.ts +17 -0
- package/dist/pool.d.ts +143 -0
- package/dist/pool.js +554 -0
- package/dist/pty.d.ts +59 -0
- package/dist/scp.d.ts +30 -0
- package/dist/scp.js +74 -0
- package/dist/sessions.d.ts +98 -0
- package/dist/tmux-exec.d.ts +50 -0
- package/dist/tmux.d.ts +213 -0
- package/dist/tmux.js +528 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +5 -0
- package/ebowwa-terminal-0.2.0.tgz +0 -0
- package/mcp/README.md +181 -0
- package/mcp/package.json +34 -0
- package/mcp/test-fix.sh +273 -0
- package/package.json +118 -0
- package/src/api.ts +752 -0
- package/src/client.ts +55 -0
- package/src/config.ts +489 -0
- package/src/error.ts +13 -0
- package/src/exec.ts +128 -0
- package/src/files.ts +636 -0
- package/src/fingerprint.ts +263 -0
- package/src/index.ts +144 -0
- package/src/manager.ts +319 -0
- package/src/mcp/index.ts +467 -0
- package/src/mcp/stdio.ts +708 -0
- package/src/network-error-detector.ts +121 -0
- package/src/pool.ts +662 -0
- package/src/pty.ts +285 -0
- package/src/scp.ts +109 -0
- package/src/sessions.ts +861 -0
- package/src/tmux-exec.ts +96 -0
- package/src/tmux-local.ts +839 -0
- package/src/tmux-manager.ts +962 -0
- package/src/tmux.ts +711 -0
- package/src/types.ts +19 -0
package/src/mcp/index.ts
ADDED
|
@@ -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`);
|