run-mcp 1.2.0 → 1.3.2
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 +37 -19
- package/dist/index.js +565 -259
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
# run-mcp
|
|
2
2
|
|
|
3
|
-
A smart proxy
|
|
3
|
+
A smart proxy, interactive REPL, and live test harness for [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers.
|
|
4
4
|
|
|
5
|
-
`run-mcp`
|
|
5
|
+
`run-mcp` operates in two modes:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
| **`repl`** | Humans / developers | Interactive CLI for testing and exploring MCP servers with shorthand commands |
|
|
10
|
-
| **`proxy`** | AI agents | Transparent MCP proxy that intercepts responses to save images to disk, enforce timeouts, and truncate massive payloads |
|
|
7
|
+
1. **Interactive REPL** (`run-mcp repl`) — A headless CLI for human developers to manually test and explore MCP servers using short, memorable commands (`tools/call`, `status`, etc.).
|
|
8
|
+
2. **Server Mode** (`run-mcp server`) — An MCP server that exposes tools (`connect_to_mcp`, `call_mcp_tool`) so AI agents can dynamically connect to and test local MCP projects without hardcoding them in configuration files.
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
### Interception Rules (Server Mode & REPL)
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
To protect the CLI and parent agents from large payloads, `run-mcp` automatically applies the following rules:
|
|
15
13
|
|
|
16
14
|
- **Saving images to disk** instead of passing multi-MB base64 strings through
|
|
17
15
|
- **Enforcing timeouts** so a hung tool call doesn't block forever
|
|
@@ -55,14 +53,6 @@ You'll see an interactive prompt:
|
|
|
55
53
|
>
|
|
56
54
|
```
|
|
57
55
|
|
|
58
|
-
### Proxy Mode — Protect your agent's context
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
run-mcp proxy node path/to/my-mcp-server.js --out-dir ./captured-images
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Then point your AI agent at `run-mcp` as the MCP server command. It transparently forwards all tools while sanitizing responses.
|
|
65
|
-
|
|
66
56
|
## Usage
|
|
67
57
|
|
|
68
58
|
```
|
|
@@ -70,7 +60,7 @@ run-mcp <command> [options]
|
|
|
70
60
|
|
|
71
61
|
Commands:
|
|
72
62
|
repl <target_command...> Start an interactive REPL session
|
|
73
|
-
|
|
63
|
+
server Start as an MCP server for agents
|
|
74
64
|
|
|
75
65
|
Options:
|
|
76
66
|
-V, --version Show version number
|
|
@@ -87,10 +77,10 @@ Options:
|
|
|
87
77
|
-o, --out-dir <path> Directory to save intercepted images (default: $TMPDIR/run-mcp)
|
|
88
78
|
```
|
|
89
79
|
|
|
90
|
-
###
|
|
80
|
+
### Server Command
|
|
91
81
|
|
|
92
82
|
```
|
|
93
|
-
run-mcp
|
|
83
|
+
run-mcp server [options]
|
|
94
84
|
|
|
95
85
|
Options:
|
|
96
86
|
-o, --out-dir <path> Directory to save intercepted images and audio (default: $TMPDIR/run-mcp)
|
|
@@ -98,6 +88,34 @@ Options:
|
|
|
98
88
|
--max-text <chars> Max text response length before truncation (default: 50000)
|
|
99
89
|
```
|
|
100
90
|
|
|
91
|
+
Add to your MCP client configuration:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"run-mcp": {
|
|
97
|
+
"command": "npx",
|
|
98
|
+
"args": ["-y", "run-mcp", "server"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then use these tools from your agent:
|
|
105
|
+
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
|------|-------------|
|
|
108
|
+
| `connect_to_mcp` | Spawn and connect to a local MCP server |
|
|
109
|
+
| `disconnect_from_mcp` | Tear down the connection |
|
|
110
|
+
| `mcp_server_status` | Check connection status |
|
|
111
|
+
| `list_mcp_tools` | List tools on the connected server |
|
|
112
|
+
| `call_mcp_tool` | Call a tool (with interception) |
|
|
113
|
+
| `list_mcp_resources` | List resources |
|
|
114
|
+
| `read_mcp_resource` | Read a resource by URI |
|
|
115
|
+
| `list_mcp_prompts` | List prompts |
|
|
116
|
+
| `get_mcp_prompt` | Get a prompt by name |
|
|
117
|
+
| `get_mcp_server_stderr` | View target server stderr output |
|
|
118
|
+
|
|
101
119
|
## REPL Commands
|
|
102
120
|
|
|
103
121
|
Once in the REPL, these commands are available:
|
package/dist/index.js
CHANGED
|
@@ -3,33 +3,17 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
CallToolRequestSchema,
|
|
11
|
-
CompleteRequestSchema,
|
|
12
|
-
GetPromptRequestSchema,
|
|
13
|
-
ListPromptsRequestSchema,
|
|
14
|
-
ListResourcesRequestSchema,
|
|
15
|
-
ListResourceTemplatesRequestSchema,
|
|
16
|
-
ListToolsRequestSchema,
|
|
17
|
-
LoggingMessageNotificationSchema,
|
|
18
|
-
PromptListChangedNotificationSchema,
|
|
19
|
-
ReadResourceRequestSchema,
|
|
20
|
-
ResourceListChangedNotificationSchema,
|
|
21
|
-
SetLevelRequestSchema,
|
|
22
|
-
SubscribeRequestSchema,
|
|
23
|
-
ToolListChangedNotificationSchema,
|
|
24
|
-
UnsubscribeRequestSchema
|
|
25
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
// src/repl.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { createInterface } from "readline";
|
|
9
|
+
import pc from "picocolors";
|
|
26
10
|
|
|
27
11
|
// src/interceptor.ts
|
|
28
12
|
import { mkdir, writeFile } from "fs/promises";
|
|
29
13
|
import { tmpdir } from "os";
|
|
30
14
|
import { join } from "path";
|
|
31
15
|
var BASE64_PATTERN = /^[A-Za-z0-9+/]{1000,}={0,2}$/;
|
|
32
|
-
var DEFAULT_TIMEOUT_MS =
|
|
16
|
+
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
33
17
|
var DEFAULT_MAX_TEXT_LENGTH = 5e4;
|
|
34
18
|
var ResponseInterceptor = class {
|
|
35
19
|
outDir;
|
|
@@ -49,7 +33,10 @@ var ResponseInterceptor = class {
|
|
|
49
33
|
*/
|
|
50
34
|
async callTool(target, name, args = {}, timeoutMs) {
|
|
51
35
|
const timeout = timeoutMs ?? this.defaultTimeoutMs;
|
|
52
|
-
const
|
|
36
|
+
const targetCall = target.callTool(name, args);
|
|
37
|
+
targetCall.catch(() => {
|
|
38
|
+
});
|
|
39
|
+
const result = await Promise.race([targetCall, this._timeout(timeout, name)]);
|
|
53
40
|
const content = result.content;
|
|
54
41
|
if (Array.isArray(content)) {
|
|
55
42
|
for (let i = 0; i < content.length; i++) {
|
|
@@ -146,6 +133,67 @@ var ResponseInterceptor = class {
|
|
|
146
133
|
}
|
|
147
134
|
};
|
|
148
135
|
|
|
136
|
+
// src/parsing.ts
|
|
137
|
+
function parseCommandLine(input) {
|
|
138
|
+
const spaceIdx = input.indexOf(" ");
|
|
139
|
+
if (spaceIdx === -1) {
|
|
140
|
+
return { cmd: input.toLowerCase(), rest: "" };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
cmd: input.slice(0, spaceIdx).toLowerCase(),
|
|
144
|
+
rest: input.slice(spaceIdx + 1)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseCallArgs(rest) {
|
|
148
|
+
const trimmed = rest.trim();
|
|
149
|
+
if (!trimmed) return { toolName: "", jsonArgs: "" };
|
|
150
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
151
|
+
if (spaceIdx === -1) {
|
|
152
|
+
return { toolName: trimmed, jsonArgs: "" };
|
|
153
|
+
}
|
|
154
|
+
const toolName = trimmed.slice(0, spaceIdx);
|
|
155
|
+
let remainder = trimmed.slice(spaceIdx + 1).trim();
|
|
156
|
+
let timeoutMs;
|
|
157
|
+
const timeoutMatch = remainder.match(/\s--timeout\s+(\d+)\s*$/);
|
|
158
|
+
if (timeoutMatch) {
|
|
159
|
+
timeoutMs = parseInt(timeoutMatch[1], 10);
|
|
160
|
+
remainder = remainder.slice(0, timeoutMatch.index).trim();
|
|
161
|
+
}
|
|
162
|
+
return { toolName, jsonArgs: remainder, timeoutMs };
|
|
163
|
+
}
|
|
164
|
+
function formatJson(obj, indent = 2) {
|
|
165
|
+
const json = JSON.stringify(obj, null, indent);
|
|
166
|
+
return json.split("\n").map((line) => " ".repeat(indent) + line).join("\n");
|
|
167
|
+
}
|
|
168
|
+
function levenshtein(a, b) {
|
|
169
|
+
const m = a.length;
|
|
170
|
+
const n = b.length;
|
|
171
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
172
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
173
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
174
|
+
for (let i = 1; i <= m; i++) {
|
|
175
|
+
for (let j = 1; j <= n; j++) {
|
|
176
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return dp[m][n];
|
|
180
|
+
}
|
|
181
|
+
function suggestCommand(input, commands, threshold = 0.4) {
|
|
182
|
+
let best = null;
|
|
183
|
+
let bestDist = Infinity;
|
|
184
|
+
for (const cmd of commands) {
|
|
185
|
+
const dist = levenshtein(input, cmd);
|
|
186
|
+
if (dist < bestDist) {
|
|
187
|
+
bestDist = dist;
|
|
188
|
+
best = cmd;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (best && bestDist <= Math.ceil(input.length * threshold)) {
|
|
192
|
+
return best;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
// src/target-manager.ts
|
|
150
198
|
import { EventEmitter } from "events";
|
|
151
199
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -167,6 +215,8 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
167
215
|
// Enhanced status tracking
|
|
168
216
|
_lastResponseTime = null;
|
|
169
217
|
_stderrLineCount = 0;
|
|
218
|
+
_stderrLines = [];
|
|
219
|
+
static MAX_STDERR_LINES = 200;
|
|
170
220
|
// Auto-reconnect state
|
|
171
221
|
_reconnectAttempts = 0;
|
|
172
222
|
_stableTimer = null;
|
|
@@ -193,11 +243,16 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
193
243
|
this.transport.stderr?.on("data", (chunk) => {
|
|
194
244
|
const text = chunk.toString().trimEnd();
|
|
195
245
|
if (text) {
|
|
196
|
-
|
|
246
|
+
const lines = text.split("\n");
|
|
247
|
+
this._stderrLineCount += lines.length;
|
|
248
|
+
this._stderrLines.push(...lines);
|
|
249
|
+
if (this._stderrLines.length > _TargetManager.MAX_STDERR_LINES) {
|
|
250
|
+
this._stderrLines = this._stderrLines.slice(-_TargetManager.MAX_STDERR_LINES);
|
|
251
|
+
}
|
|
197
252
|
this.emit("stderr", text);
|
|
198
253
|
}
|
|
199
254
|
});
|
|
200
|
-
this.client = new Client({ name: "run-mcp", version: "1.
|
|
255
|
+
this.client = new Client({ name: "run-mcp", version: "1.3.1" }, { capabilities: {} });
|
|
201
256
|
this.client.onclose = () => {
|
|
202
257
|
this._connected = false;
|
|
203
258
|
this._clearStableTimer();
|
|
@@ -255,10 +310,15 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
255
310
|
}
|
|
256
311
|
/**
|
|
257
312
|
* Call a tool on the target MCP server.
|
|
313
|
+
* We apply a massive SDK-level timeout (e.g. 10 hours) because we want to handle
|
|
314
|
+
* timeouts in the interceptor via Promise.race, and we DO NOT want to send
|
|
315
|
+
* protocol-level cancellation requests to the target server if the agent gives up.
|
|
316
|
+
* This allows long-running builds (like mobile app compiling) to finish in the background.
|
|
258
317
|
*/
|
|
259
|
-
async callTool(name, args = {}) {
|
|
318
|
+
async callTool(name, args = {}, _timeoutMs) {
|
|
260
319
|
this._assertConnected();
|
|
261
|
-
const
|
|
320
|
+
const requestOptions = { timeout: 36e5 * 10 };
|
|
321
|
+
const result = await this.client.callTool({ name, arguments: args }, void 0, requestOptions);
|
|
262
322
|
this.recordResponse();
|
|
263
323
|
return result;
|
|
264
324
|
}
|
|
@@ -360,6 +420,14 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
360
420
|
return this.client;
|
|
361
421
|
}
|
|
362
422
|
// ─── Status & lifecycle ─────────────────────────────────────────────────────
|
|
423
|
+
/**
|
|
424
|
+
* Returns the last N lines of stderr output from the target server.
|
|
425
|
+
* Useful for debugging crashes or unexpected behavior.
|
|
426
|
+
*/
|
|
427
|
+
getStderrLines(count) {
|
|
428
|
+
if (!count || count >= this._stderrLines.length) return [...this._stderrLines];
|
|
429
|
+
return this._stderrLines.slice(-count);
|
|
430
|
+
}
|
|
363
431
|
/**
|
|
364
432
|
* Returns current connection status, PID, uptime, and diagnostics.
|
|
365
433
|
*/
|
|
@@ -496,217 +564,6 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
496
564
|
}
|
|
497
565
|
};
|
|
498
566
|
|
|
499
|
-
// src/proxy.ts
|
|
500
|
-
async function startProxy(targetCommand, opts) {
|
|
501
|
-
const [command, ...args] = targetCommand;
|
|
502
|
-
const target = new TargetManager(command, args);
|
|
503
|
-
const interceptor = new ResponseInterceptor({
|
|
504
|
-
outDir: opts.outDir,
|
|
505
|
-
defaultTimeoutMs: opts.timeoutMs,
|
|
506
|
-
maxTextLength: opts.maxTextLength
|
|
507
|
-
});
|
|
508
|
-
target.on("stderr", (text) => {
|
|
509
|
-
process.stderr.write(`[target] ${text}
|
|
510
|
-
`);
|
|
511
|
-
});
|
|
512
|
-
process.stderr.write("[proxy] Connecting to target MCP server...\n");
|
|
513
|
-
try {
|
|
514
|
-
await target.connect();
|
|
515
|
-
} catch (err) {
|
|
516
|
-
process.stderr.write(`[proxy] Failed to connect to target: ${err.message}
|
|
517
|
-
`);
|
|
518
|
-
process.exit(1);
|
|
519
|
-
}
|
|
520
|
-
const status = target.getStatus();
|
|
521
|
-
process.stderr.write(`[proxy] Connected to target (PID: ${status.pid})
|
|
522
|
-
`);
|
|
523
|
-
const targetCaps = target.getServerCapabilities() ?? {};
|
|
524
|
-
const proxyCaps = {};
|
|
525
|
-
proxyCaps.tools = targetCaps.tools ?? {};
|
|
526
|
-
if (targetCaps.resources) proxyCaps.resources = targetCaps.resources;
|
|
527
|
-
if (targetCaps.prompts) proxyCaps.prompts = targetCaps.prompts;
|
|
528
|
-
if (targetCaps.logging) proxyCaps.logging = targetCaps.logging;
|
|
529
|
-
if (targetCaps.completions) proxyCaps.completions = targetCaps.completions;
|
|
530
|
-
process.stderr.write(`[proxy] Mirroring capabilities: ${Object.keys(proxyCaps).join(", ")}
|
|
531
|
-
`);
|
|
532
|
-
const instructions = target.getInstructions();
|
|
533
|
-
if (instructions) {
|
|
534
|
-
process.stderr.write(
|
|
535
|
-
`[proxy] Target instructions: ${instructions.slice(0, 200)}${instructions.length > 200 ? "..." : ""}
|
|
536
|
-
`
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
const mcpServer = new McpServer(
|
|
540
|
-
{
|
|
541
|
-
name: "run-mcp-proxy",
|
|
542
|
-
version: "1.2.0"
|
|
543
|
-
},
|
|
544
|
-
{ capabilities: proxyCaps }
|
|
545
|
-
);
|
|
546
|
-
const server = mcpServer.server;
|
|
547
|
-
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
548
|
-
const result = await target.listTools(request.params);
|
|
549
|
-
return result;
|
|
550
|
-
});
|
|
551
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
552
|
-
const { name, arguments: toolArgs } = request.params;
|
|
553
|
-
try {
|
|
554
|
-
const result = await interceptor.callTool(
|
|
555
|
-
target,
|
|
556
|
-
name,
|
|
557
|
-
toolArgs ?? {}
|
|
558
|
-
);
|
|
559
|
-
return result;
|
|
560
|
-
} catch (err) {
|
|
561
|
-
return {
|
|
562
|
-
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
563
|
-
isError: true
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
if (targetCaps.resources) {
|
|
568
|
-
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
569
|
-
return await target.listResources(request.params);
|
|
570
|
-
});
|
|
571
|
-
server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => {
|
|
572
|
-
return await target.listResourceTemplates(request.params);
|
|
573
|
-
});
|
|
574
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
575
|
-
return await target.readResource(request.params);
|
|
576
|
-
});
|
|
577
|
-
if (targetCaps.resources.subscribe) {
|
|
578
|
-
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
579
|
-
return await target.subscribeResource(request.params);
|
|
580
|
-
});
|
|
581
|
-
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
582
|
-
return await target.unsubscribeResource(request.params);
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (targetCaps.prompts) {
|
|
587
|
-
server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
|
|
588
|
-
return await target.listPrompts(request.params);
|
|
589
|
-
});
|
|
590
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
591
|
-
return await target.getPrompt(request.params);
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
if (targetCaps.logging) {
|
|
595
|
-
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
596
|
-
return await target.setLoggingLevel(request.params.level);
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
if (targetCaps.completions) {
|
|
600
|
-
server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
601
|
-
return await target.complete(request.params);
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
const rawClient = target.getRawClient();
|
|
605
|
-
if (rawClient) {
|
|
606
|
-
if (targetCaps.tools && targetCaps.tools.listChanged) {
|
|
607
|
-
rawClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
608
|
-
server.notification({ method: "notifications/tools/list_changed" });
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
if (targetCaps.resources && targetCaps.resources.listChanged) {
|
|
612
|
-
rawClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
|
|
613
|
-
server.notification({ method: "notifications/resources/list_changed" });
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
if (targetCaps.prompts && targetCaps.prompts.listChanged) {
|
|
617
|
-
rawClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
|
|
618
|
-
server.notification({ method: "notifications/prompts/list_changed" });
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
if (targetCaps.logging) {
|
|
622
|
-
rawClient.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
|
623
|
-
server.notification({
|
|
624
|
-
method: "notifications/message",
|
|
625
|
-
params: notification.params
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
const transport = new StdioServerTransport();
|
|
631
|
-
server.onclose = async () => {
|
|
632
|
-
process.stderr.write("[proxy] Parent disconnected, shutting down...\n");
|
|
633
|
-
await target.close();
|
|
634
|
-
process.exit(0);
|
|
635
|
-
};
|
|
636
|
-
await mcpServer.connect(transport);
|
|
637
|
-
process.stderr.write("[proxy] Proxy server running on stdio.\n");
|
|
638
|
-
target.on("disconnected", () => {
|
|
639
|
-
process.stderr.write("[proxy] Target server disconnected.\n");
|
|
640
|
-
process.exit(1);
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// src/repl.ts
|
|
645
|
-
import { readFile } from "fs/promises";
|
|
646
|
-
import { createInterface } from "readline";
|
|
647
|
-
import pc from "picocolors";
|
|
648
|
-
|
|
649
|
-
// src/parsing.ts
|
|
650
|
-
function parseCommandLine(input) {
|
|
651
|
-
const spaceIdx = input.indexOf(" ");
|
|
652
|
-
if (spaceIdx === -1) {
|
|
653
|
-
return { cmd: input.toLowerCase(), rest: "" };
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
cmd: input.slice(0, spaceIdx).toLowerCase(),
|
|
657
|
-
rest: input.slice(spaceIdx + 1)
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
function parseCallArgs(rest) {
|
|
661
|
-
const trimmed = rest.trim();
|
|
662
|
-
if (!trimmed) return { toolName: "", jsonArgs: "" };
|
|
663
|
-
const spaceIdx = trimmed.indexOf(" ");
|
|
664
|
-
if (spaceIdx === -1) {
|
|
665
|
-
return { toolName: trimmed, jsonArgs: "" };
|
|
666
|
-
}
|
|
667
|
-
const toolName = trimmed.slice(0, spaceIdx);
|
|
668
|
-
let remainder = trimmed.slice(spaceIdx + 1).trim();
|
|
669
|
-
let timeoutMs;
|
|
670
|
-
const timeoutMatch = remainder.match(/\s--timeout\s+(\d+)\s*$/);
|
|
671
|
-
if (timeoutMatch) {
|
|
672
|
-
timeoutMs = parseInt(timeoutMatch[1], 10);
|
|
673
|
-
remainder = remainder.slice(0, timeoutMatch.index).trim();
|
|
674
|
-
}
|
|
675
|
-
return { toolName, jsonArgs: remainder, timeoutMs };
|
|
676
|
-
}
|
|
677
|
-
function formatJson(obj, indent = 2) {
|
|
678
|
-
const json = JSON.stringify(obj, null, indent);
|
|
679
|
-
return json.split("\n").map((line) => " ".repeat(indent) + line).join("\n");
|
|
680
|
-
}
|
|
681
|
-
function levenshtein(a, b) {
|
|
682
|
-
const m = a.length;
|
|
683
|
-
const n = b.length;
|
|
684
|
-
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
685
|
-
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
686
|
-
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
687
|
-
for (let i = 1; i <= m; i++) {
|
|
688
|
-
for (let j = 1; j <= n; j++) {
|
|
689
|
-
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return dp[m][n];
|
|
693
|
-
}
|
|
694
|
-
function suggestCommand(input, commands, threshold = 0.4) {
|
|
695
|
-
let best = null;
|
|
696
|
-
let bestDist = Infinity;
|
|
697
|
-
for (const cmd of commands) {
|
|
698
|
-
const dist = levenshtein(input, cmd);
|
|
699
|
-
if (dist < bestDist) {
|
|
700
|
-
bestDist = dist;
|
|
701
|
-
best = cmd;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
if (best && bestDist <= Math.ceil(input.length * threshold)) {
|
|
705
|
-
return best;
|
|
706
|
-
}
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
567
|
// src/repl.ts
|
|
711
568
|
var KNOWN_COMMANDS = [
|
|
712
569
|
"tools/list",
|
|
@@ -973,16 +830,462 @@ async function readScriptLines(filepath) {
|
|
|
973
830
|
return content.split("\n");
|
|
974
831
|
}
|
|
975
832
|
|
|
833
|
+
// src/server.ts
|
|
834
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
835
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
836
|
+
import { z } from "zod";
|
|
837
|
+
async function startServer(opts) {
|
|
838
|
+
let target = null;
|
|
839
|
+
const interceptor = new ResponseInterceptor({
|
|
840
|
+
outDir: opts.outDir,
|
|
841
|
+
defaultTimeoutMs: opts.timeoutMs,
|
|
842
|
+
maxTextLength: opts.maxTextLength
|
|
843
|
+
});
|
|
844
|
+
const mcpServer = new McpServer(
|
|
845
|
+
{ name: "run-mcp", version: "1.3.1" },
|
|
846
|
+
{
|
|
847
|
+
capabilities: {
|
|
848
|
+
tools: {}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
mcpServer.registerTool(
|
|
853
|
+
"connect_to_mcp",
|
|
854
|
+
{
|
|
855
|
+
title: "Connect to MCP Server",
|
|
856
|
+
description: "Spawn and connect to a local MCP server process. Use this to test an MCP server you're building. Only one connection at a time \u2014 call disconnect_from_mcp first if already connected.",
|
|
857
|
+
inputSchema: {
|
|
858
|
+
command: z.string().describe("Command to run (e.g. 'node', 'python', 'npx')"),
|
|
859
|
+
args: z.array(z.string()).optional().describe("Arguments to pass (e.g. ['src/index.js'] or ['-y', 'some-server'])"),
|
|
860
|
+
env: z.record(z.string()).optional().describe("Extra environment variables for the child process")
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
async ({ command, args, env }) => {
|
|
864
|
+
if (target?.connected) {
|
|
865
|
+
return {
|
|
866
|
+
content: [
|
|
867
|
+
{
|
|
868
|
+
type: "text",
|
|
869
|
+
text: "Already connected to a target server. Call disconnect_from_mcp first, then connect again."
|
|
870
|
+
}
|
|
871
|
+
],
|
|
872
|
+
isError: true
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (target) {
|
|
876
|
+
await target.close();
|
|
877
|
+
target = null;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
if (env) {
|
|
881
|
+
for (const [key, value] of Object.entries(env)) {
|
|
882
|
+
process.env[key] = value;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
target = new TargetManager(command, args ?? []);
|
|
886
|
+
await target.connect();
|
|
887
|
+
const status = target.getStatus();
|
|
888
|
+
const caps = target.getServerCapabilities() ?? {};
|
|
889
|
+
const capSummary = [];
|
|
890
|
+
if (caps.tools) capSummary.push("tools");
|
|
891
|
+
if (caps.resources) capSummary.push("resources");
|
|
892
|
+
if (caps.prompts) capSummary.push("prompts");
|
|
893
|
+
if (caps.logging) capSummary.push("logging");
|
|
894
|
+
let toolCount = 0;
|
|
895
|
+
try {
|
|
896
|
+
const tools = await target.listTools();
|
|
897
|
+
toolCount = tools.tools.length;
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
const lines = [
|
|
901
|
+
`Connected to MCP server (PID: ${status.pid})`,
|
|
902
|
+
`Command: ${command} ${(args ?? []).join(" ")}`,
|
|
903
|
+
`Capabilities: ${capSummary.join(", ") || "none"}`,
|
|
904
|
+
`Tools available: ${toolCount}`,
|
|
905
|
+
"",
|
|
906
|
+
"Use list_mcp_tools, call_mcp_tool, list_mcp_resources, etc. to interact with it.",
|
|
907
|
+
"Use disconnect_from_mcp when done, or to reconnect after code changes."
|
|
908
|
+
];
|
|
909
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
910
|
+
} catch (err) {
|
|
911
|
+
target = null;
|
|
912
|
+
return {
|
|
913
|
+
content: [
|
|
914
|
+
{
|
|
915
|
+
type: "text",
|
|
916
|
+
text: `Failed to connect: ${err.message}
|
|
917
|
+
|
|
918
|
+
Check that the command is correct and the server starts without errors. You can also check get_mcp_server_stderr after a failed connect for more details.`
|
|
919
|
+
}
|
|
920
|
+
],
|
|
921
|
+
isError: true
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
mcpServer.registerTool(
|
|
927
|
+
"disconnect_from_mcp",
|
|
928
|
+
{
|
|
929
|
+
title: "Disconnect from MCP Server",
|
|
930
|
+
description: "Tear down the current MCP server connection. Call this before reconnecting after code changes."
|
|
931
|
+
},
|
|
932
|
+
async () => {
|
|
933
|
+
if (!target) {
|
|
934
|
+
return {
|
|
935
|
+
content: [{ type: "text", text: "No target server is connected." }],
|
|
936
|
+
isError: true
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const status = target.getStatus();
|
|
940
|
+
await target.close();
|
|
941
|
+
target = null;
|
|
942
|
+
return {
|
|
943
|
+
content: [
|
|
944
|
+
{
|
|
945
|
+
type: "text",
|
|
946
|
+
text: `Disconnected from MCP server (was PID: ${status.pid}, uptime: ${status.uptime.toFixed(1)}s).`
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
mcpServer.registerTool(
|
|
953
|
+
"mcp_server_status",
|
|
954
|
+
{
|
|
955
|
+
title: "MCP Server Status",
|
|
956
|
+
description: "Check the current target server connection status, PID, uptime, and capabilities."
|
|
957
|
+
},
|
|
958
|
+
async () => {
|
|
959
|
+
if (!target) {
|
|
960
|
+
return {
|
|
961
|
+
content: [
|
|
962
|
+
{
|
|
963
|
+
type: "text",
|
|
964
|
+
text: "No target server connected. Use connect_to_mcp to connect to one."
|
|
965
|
+
}
|
|
966
|
+
]
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const status = target.getStatus();
|
|
970
|
+
const caps = target.getServerCapabilities() ?? {};
|
|
971
|
+
const lines = [
|
|
972
|
+
`Connected: ${status.connected}`,
|
|
973
|
+
`PID: ${status.pid}`,
|
|
974
|
+
`Uptime: ${status.uptime.toFixed(1)}s`,
|
|
975
|
+
`Command: ${status.command} ${status.args.join(" ")}`,
|
|
976
|
+
`Capabilities: ${Object.keys(caps).join(", ") || "none"}`,
|
|
977
|
+
`Stderr lines: ${status.stderrLineCount}`,
|
|
978
|
+
`Last response: ${status.lastResponseTime ? new Date(status.lastResponseTime).toISOString() : "none"}`
|
|
979
|
+
];
|
|
980
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
981
|
+
}
|
|
982
|
+
);
|
|
983
|
+
mcpServer.registerTool(
|
|
984
|
+
"list_mcp_tools",
|
|
985
|
+
{
|
|
986
|
+
title: "List MCP Tools",
|
|
987
|
+
description: "List all tools exposed by the connected MCP server, including descriptions, input schemas, and annotations."
|
|
988
|
+
},
|
|
989
|
+
async () => {
|
|
990
|
+
if (!target?.connected) {
|
|
991
|
+
return {
|
|
992
|
+
content: [
|
|
993
|
+
{
|
|
994
|
+
type: "text",
|
|
995
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
996
|
+
}
|
|
997
|
+
],
|
|
998
|
+
isError: true
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await target.listTools();
|
|
1003
|
+
return {
|
|
1004
|
+
content: [
|
|
1005
|
+
{
|
|
1006
|
+
type: "text",
|
|
1007
|
+
text: JSON.stringify(result.tools, null, 2)
|
|
1008
|
+
}
|
|
1009
|
+
]
|
|
1010
|
+
};
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
return {
|
|
1013
|
+
content: [{ type: "text", text: `Error listing tools: ${err.message}` }],
|
|
1014
|
+
isError: true
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
mcpServer.registerTool(
|
|
1020
|
+
"describe_mcp_tool",
|
|
1021
|
+
{
|
|
1022
|
+
title: "Describe MCP Tool",
|
|
1023
|
+
description: "Get the description and input schema for a specific tool on the connected server.",
|
|
1024
|
+
inputSchema: {
|
|
1025
|
+
name: z.string().describe("Name of the tool to describe")
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
async ({ name }) => {
|
|
1029
|
+
if (!target?.connected) {
|
|
1030
|
+
return {
|
|
1031
|
+
content: [
|
|
1032
|
+
{ type: "text", text: "No target server connected. Use connect_to_mcp first." }
|
|
1033
|
+
],
|
|
1034
|
+
isError: true
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
const result = await target.listTools();
|
|
1039
|
+
const tool = result.tools.find((t) => t.name === name);
|
|
1040
|
+
if (!tool) {
|
|
1041
|
+
const available = result.tools.map((t) => t.name).join(", ");
|
|
1042
|
+
return {
|
|
1043
|
+
content: [
|
|
1044
|
+
{ type: "text", text: `Tool "${name}" not found.
|
|
1045
|
+
Available tools: ${available}` }
|
|
1046
|
+
],
|
|
1047
|
+
isError: true
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
content: [
|
|
1052
|
+
{ type: "text", text: JSON.stringify(tool, null, 2) }
|
|
1053
|
+
]
|
|
1054
|
+
};
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
return {
|
|
1057
|
+
content: [{ type: "text", text: `Error describing tool: ${err.message}` }],
|
|
1058
|
+
isError: true
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
);
|
|
1063
|
+
mcpServer.registerTool(
|
|
1064
|
+
"call_mcp_tool",
|
|
1065
|
+
{
|
|
1066
|
+
title: "Call MCP Tool",
|
|
1067
|
+
description: "Call a tool on the connected MCP server. Responses go through the interceptor: images/audio are saved to disk, timeouts are enforced, and oversized text is truncated.",
|
|
1068
|
+
inputSchema: {
|
|
1069
|
+
name: z.string().describe("Name of the tool to call"),
|
|
1070
|
+
arguments: z.record(z.unknown()).optional().describe("Arguments to pass to the tool (as a JSON object)"),
|
|
1071
|
+
timeout_ms: z.number().optional().describe("Timeout for this specific call in milliseconds (overrides default)")
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
1074
|
+
async ({ name, arguments: toolArgs, timeout_ms }) => {
|
|
1075
|
+
if (!target?.connected) {
|
|
1076
|
+
return {
|
|
1077
|
+
content: [
|
|
1078
|
+
{
|
|
1079
|
+
type: "text",
|
|
1080
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
1081
|
+
}
|
|
1082
|
+
],
|
|
1083
|
+
isError: true
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const result = await interceptor.callTool(
|
|
1088
|
+
target,
|
|
1089
|
+
name,
|
|
1090
|
+
toolArgs ?? {},
|
|
1091
|
+
timeout_ms
|
|
1092
|
+
);
|
|
1093
|
+
return result;
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
return {
|
|
1096
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
1097
|
+
isError: true
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
);
|
|
1102
|
+
mcpServer.registerTool(
|
|
1103
|
+
"list_mcp_resources",
|
|
1104
|
+
{
|
|
1105
|
+
title: "List MCP Resources",
|
|
1106
|
+
description: "List all resources exposed by the connected MCP server."
|
|
1107
|
+
},
|
|
1108
|
+
async () => {
|
|
1109
|
+
if (!target?.connected) {
|
|
1110
|
+
return {
|
|
1111
|
+
content: [
|
|
1112
|
+
{
|
|
1113
|
+
type: "text",
|
|
1114
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
1115
|
+
}
|
|
1116
|
+
],
|
|
1117
|
+
isError: true
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const result = await target.listResources();
|
|
1122
|
+
return {
|
|
1123
|
+
content: [{ type: "text", text: JSON.stringify(result.resources, null, 2) }]
|
|
1124
|
+
};
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
return {
|
|
1127
|
+
content: [{ type: "text", text: `Error listing resources: ${err.message}` }],
|
|
1128
|
+
isError: true
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
mcpServer.registerTool(
|
|
1134
|
+
"read_mcp_resource",
|
|
1135
|
+
{
|
|
1136
|
+
title: "Read MCP Resource",
|
|
1137
|
+
description: "Read a specific resource by URI from the connected MCP server.",
|
|
1138
|
+
inputSchema: {
|
|
1139
|
+
uri: z.string().describe("URI of the resource to read (e.g. 'docs://readme')")
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
async ({ uri }) => {
|
|
1143
|
+
if (!target?.connected) {
|
|
1144
|
+
return {
|
|
1145
|
+
content: [
|
|
1146
|
+
{
|
|
1147
|
+
type: "text",
|
|
1148
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
1149
|
+
}
|
|
1150
|
+
],
|
|
1151
|
+
isError: true
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
const result = await target.readResource({ uri });
|
|
1156
|
+
return {
|
|
1157
|
+
content: [{ type: "text", text: JSON.stringify(result.contents, null, 2) }]
|
|
1158
|
+
};
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
return {
|
|
1161
|
+
content: [{ type: "text", text: `Error reading resource: ${err.message}` }],
|
|
1162
|
+
isError: true
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
mcpServer.registerTool(
|
|
1168
|
+
"list_mcp_prompts",
|
|
1169
|
+
{
|
|
1170
|
+
title: "List MCP Prompts",
|
|
1171
|
+
description: "List all prompts exposed by the connected MCP server."
|
|
1172
|
+
},
|
|
1173
|
+
async () => {
|
|
1174
|
+
if (!target?.connected) {
|
|
1175
|
+
return {
|
|
1176
|
+
content: [
|
|
1177
|
+
{
|
|
1178
|
+
type: "text",
|
|
1179
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
1180
|
+
}
|
|
1181
|
+
],
|
|
1182
|
+
isError: true
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
const result = await target.listPrompts();
|
|
1187
|
+
return {
|
|
1188
|
+
content: [{ type: "text", text: JSON.stringify(result.prompts, null, 2) }]
|
|
1189
|
+
};
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
return {
|
|
1192
|
+
content: [{ type: "text", text: `Error listing prompts: ${err.message}` }],
|
|
1193
|
+
isError: true
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
mcpServer.registerTool(
|
|
1199
|
+
"get_mcp_prompt",
|
|
1200
|
+
{
|
|
1201
|
+
title: "Get MCP Prompt",
|
|
1202
|
+
description: "Get a specific prompt by name from the connected MCP server.",
|
|
1203
|
+
inputSchema: {
|
|
1204
|
+
name: z.string().describe("Name of the prompt"),
|
|
1205
|
+
arguments: z.record(z.string()).optional().describe("Arguments to pass to the prompt")
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
async ({ name, arguments: promptArgs }) => {
|
|
1209
|
+
if (!target?.connected) {
|
|
1210
|
+
return {
|
|
1211
|
+
content: [
|
|
1212
|
+
{
|
|
1213
|
+
type: "text",
|
|
1214
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
1215
|
+
}
|
|
1216
|
+
],
|
|
1217
|
+
isError: true
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
const result = await target.getPrompt({
|
|
1222
|
+
name,
|
|
1223
|
+
arguments: promptArgs ?? {}
|
|
1224
|
+
});
|
|
1225
|
+
return {
|
|
1226
|
+
content: [{ type: "text", text: JSON.stringify(result.messages, null, 2) }]
|
|
1227
|
+
};
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
return {
|
|
1230
|
+
content: [{ type: "text", text: `Error getting prompt: ${err.message}` }],
|
|
1231
|
+
isError: true
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
);
|
|
1236
|
+
mcpServer.registerTool(
|
|
1237
|
+
"get_mcp_server_stderr",
|
|
1238
|
+
{
|
|
1239
|
+
title: "Get MCP Server Stderr",
|
|
1240
|
+
description: "Get recent stderr output from the target MCP server. Useful for debugging crashes, startup failures, or unexpected behavior.",
|
|
1241
|
+
inputSchema: {
|
|
1242
|
+
lines: z.number().optional().describe("Number of recent lines to return (default: all, max 200)")
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
async ({ lines }) => {
|
|
1246
|
+
if (!target) {
|
|
1247
|
+
return {
|
|
1248
|
+
content: [
|
|
1249
|
+
{
|
|
1250
|
+
type: "text",
|
|
1251
|
+
text: "No target server (current or previous). Nothing to show."
|
|
1252
|
+
}
|
|
1253
|
+
]
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const stderrLines = target.getStderrLines(lines);
|
|
1257
|
+
if (stderrLines.length === 0) {
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: "text", text: "No stderr output captured." }]
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
content: [{ type: "text", text: stderrLines.join("\n") }]
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
);
|
|
1267
|
+
const transport = new StdioServerTransport();
|
|
1268
|
+
mcpServer.server.onclose = async () => {
|
|
1269
|
+
if (target) {
|
|
1270
|
+
await target.close();
|
|
1271
|
+
}
|
|
1272
|
+
process.exit(0);
|
|
1273
|
+
};
|
|
1274
|
+
await mcpServer.connect(transport);
|
|
1275
|
+
process.stderr.write("[server] run-mcp test harness running on stdio.\n");
|
|
1276
|
+
process.stderr.write("[server] Waiting for connect_to_mcp call...\n");
|
|
1277
|
+
}
|
|
1278
|
+
|
|
976
1279
|
// src/index.ts
|
|
977
1280
|
program.name("run-mcp").enablePositionalOptions().description(
|
|
978
|
-
"A smart
|
|
979
|
-
).version("1.
|
|
1281
|
+
"A smart interactive REPL and live test harness for MCP servers.\n\nOperates in two modes:\n repl - Human-friendly CLI for testing MCP servers interactively\n server - MCP server that lets AI agents dynamically test local MCP servers"
|
|
1282
|
+
).version("1.3.1").addHelpText(
|
|
980
1283
|
"after",
|
|
981
1284
|
`
|
|
982
1285
|
Examples:
|
|
983
|
-
$ run-mcp repl node my-server.js # Interactive testing
|
|
1286
|
+
$ run-mcp repl node my-server.js # Interactive testing (human)
|
|
984
1287
|
$ run-mcp repl node my-server.js -s test.txt # Run a script
|
|
985
|
-
$ run-mcp
|
|
1288
|
+
$ run-mcp server # Test harness (agent)
|
|
986
1289
|
$ run-mcp repl npx -y some-mcp-server # Test an npx server
|
|
987
1290
|
|
|
988
1291
|
Run 'run-mcp <command> --help' for detailed options.`
|
|
@@ -1008,31 +1311,34 @@ REPL Commands (once connected):
|
|
|
1008
1311
|
).action(async (targetCommand, opts) => {
|
|
1009
1312
|
await startRepl(targetCommand, opts);
|
|
1010
1313
|
});
|
|
1011
|
-
program.command("
|
|
1314
|
+
program.command("server").description("Start as an MCP server that lets AI agents dynamically test local MCP servers").option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option("-t, --timeout <ms>", "Default tool call timeout in milliseconds (default: 300000)").option("--max-text <chars>", "Max text response length before truncation (default: 50000)").addHelpText(
|
|
1012
1315
|
"after",
|
|
1013
1316
|
`
|
|
1014
1317
|
Examples:
|
|
1015
|
-
$ run-mcp
|
|
1016
|
-
$ run-mcp
|
|
1017
|
-
$ run-mcp
|
|
1018
|
-
$ run-mcp proxy node my-server.js --max-text 100000
|
|
1318
|
+
$ run-mcp server
|
|
1319
|
+
$ run-mcp server --out-dir ./test-output
|
|
1320
|
+
$ run-mcp server --timeout 120000
|
|
1019
1321
|
|
|
1020
|
-
|
|
1322
|
+
Add to your MCP client configuration:
|
|
1021
1323
|
{
|
|
1022
1324
|
"mcpServers": {
|
|
1023
|
-
"
|
|
1024
|
-
"command": "
|
|
1025
|
-
"args": ["
|
|
1325
|
+
"run-mcp": {
|
|
1326
|
+
"command": "npx",
|
|
1327
|
+
"args": ["-y", "run-mcp", "server"]
|
|
1026
1328
|
}
|
|
1027
1329
|
}
|
|
1028
|
-
}`
|
|
1029
|
-
).action(
|
|
1030
|
-
async (targetCommand, opts) => {
|
|
1031
|
-
await startProxy(targetCommand, {
|
|
1032
|
-
outDir: opts.outDir,
|
|
1033
|
-
timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
|
|
1034
|
-
maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
|
|
1035
|
-
});
|
|
1036
1330
|
}
|
|
1037
|
-
|
|
1331
|
+
|
|
1332
|
+
Then use these tools from your agent:
|
|
1333
|
+
connect_to_mcp \u2192 Spawn and connect to a local MCP server
|
|
1334
|
+
list_mcp_tools \u2192 List tools on the connected server
|
|
1335
|
+
call_mcp_tool \u2192 Call a tool (with interception)
|
|
1336
|
+
disconnect_from_mcp \u2192 Tear down and reconnect after changes`
|
|
1337
|
+
).action(async (opts) => {
|
|
1338
|
+
await startServer({
|
|
1339
|
+
outDir: opts.outDir,
|
|
1340
|
+
timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
|
|
1341
|
+
maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1038
1344
|
program.parse();
|