run-mcp 1.3.0 → 1.3.4
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 +27 -54
- package/dist/index.js +176 -318
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
|
|
3
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) | Transparent MCP proxy that intercepts responses to save images to disk, enforce timeouts, and truncate massive payloads |
|
|
11
|
-
| **`server`** | AI agents (explicit) | MCP server that lets agents dynamically connect to, inspect, and test local MCP servers |
|
|
7
|
+
1. **Agent MCP Server** (`run-mcp`) — 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. This is the **default mode** when you run `npx -y run-mcp`.
|
|
8
|
+
2. **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.).
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
### Interception Rules (Agent Server & REPL)
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
To protect the CLI and parent agents from large payloads, `run-mcp` automatically applies the following rules:
|
|
16
13
|
|
|
17
14
|
- **Saving images to disk** instead of passing multi-MB base64 strings through
|
|
18
15
|
- **Enforcing timeouts** so a hung tool call doesn't block forever
|
|
@@ -56,68 +53,44 @@ You'll see an interactive prompt:
|
|
|
56
53
|
>
|
|
57
54
|
```
|
|
58
55
|
|
|
59
|
-
### Proxy Mode — Protect your agent's context
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
run-mcp proxy node path/to/my-mcp-server.js --out-dir ./captured-images
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Then point your AI agent at `run-mcp` as the MCP server command. It transparently forwards all tools while sanitizing responses.
|
|
66
|
-
|
|
67
56
|
## Usage
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
run-mcp <command> [options]
|
|
71
|
-
|
|
72
|
-
Commands:
|
|
73
|
-
repl <target_command...> Start an interactive REPL session
|
|
74
|
-
proxy <target_command...> Start as a transparent MCP proxy
|
|
58
|
+
run-mcp [options] [target_command...]
|
|
75
59
|
|
|
76
60
|
Options:
|
|
77
61
|
-V, --version Show version number
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
run-mcp repl <target_command...> [options]
|
|
62
|
+
-o, --out-dir <path> Directory to save intercepted images and audio
|
|
63
|
+
-t, --timeout <ms> Default tool call timeout in milliseconds (default: 300000) (Agent Mode only)
|
|
64
|
+
--max-text <chars> Max text response length before truncation (default: 50000) (Agent Mode only)
|
|
65
|
+
-s, --script <file> Read commands from a file instead of stdin (REPL Mode only)
|
|
66
|
+
-h, --help Display help for command
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
Examples:
|
|
69
|
+
$ run-mcp # Test harness (agent mode)
|
|
70
|
+
$ run-mcp node my-server.js # Interactive testing (human REPL mode)
|
|
71
|
+
$ run-mcp node my-server.js -s test.txt # Run a script in REPL mode
|
|
72
|
+
$ run-mcp npx -y some-mcp-server # Test an npx server
|
|
73
|
+
$ run-mcp --out-dir ./test-output # Agent mode with options
|
|
74
|
+
$ run-mcp --out-dir ./screenshots node srv.js # REPL mode with options
|
|
90
75
|
|
|
91
|
-
### Proxy Command
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
run-mcp proxy <target_command...> [options]
|
|
95
76
|
|
|
96
|
-
Options:
|
|
97
|
-
-o, --out-dir <path> Directory to save intercepted images and audio (default: $TMPDIR/run-mcp)
|
|
98
|
-
-t, --timeout <ms> Default tool call timeout in milliseconds (default: 60000)
|
|
99
|
-
--max-text <chars> Max text response length before truncation (default: 50000)
|
|
100
|
-
```
|
|
101
77
|
|
|
102
|
-
|
|
78
|
+
## Agent Use Cases
|
|
103
79
|
|
|
104
|
-
|
|
105
|
-
run-mcp server [options]
|
|
80
|
+
### Dynamic Testing
|
|
106
81
|
|
|
107
|
-
|
|
108
|
-
-o, --out-dir <path> Directory to save intercepted images and audio (default: $TMPDIR/run-mcp)
|
|
109
|
-
-t, --timeout <ms> Default tool call timeout in milliseconds (default: 60000)
|
|
110
|
-
--max-text <chars> Max text response length before truncation (default: 50000)
|
|
111
|
-
```
|
|
82
|
+
When an AI agent is actively *developing* an MCP server, it needs to test it. Standard MCP clients require updating a configuration file (`mcp.json`) and restarting the agent session entirely.
|
|
112
83
|
|
|
113
|
-
|
|
84
|
+
`run-mcp` solves this by giving the agent a suite of tools to dynamically spawn, inspect, and test local MCP servers on the fly.
|
|
114
85
|
|
|
86
|
+
**How to use:**
|
|
87
|
+
Add `run-mcp` to your agent's MCP configuration using `npx`:
|
|
115
88
|
```json
|
|
116
89
|
{
|
|
117
90
|
"mcpServers": {
|
|
118
91
|
"run-mcp": {
|
|
119
92
|
"command": "npx",
|
|
120
|
-
"args": ["-y", "run-mcp"
|
|
93
|
+
"args": ["-y", "run-mcp"]
|
|
121
94
|
}
|
|
122
95
|
}
|
|
123
96
|
}
|
|
@@ -138,9 +111,9 @@ Then use these tools from your agent:
|
|
|
138
111
|
| `get_mcp_prompt` | Get a prompt by name |
|
|
139
112
|
| `get_mcp_server_stderr` | View target server stderr output |
|
|
140
113
|
|
|
141
|
-
## REPL Commands
|
|
114
|
+
## REPL Mode Commands
|
|
142
115
|
|
|
143
|
-
Once
|
|
116
|
+
Once connected via `run-mcp <command>`, the following shorthand commands are available:
|
|
144
117
|
|
|
145
118
|
| Command | Description |
|
|
146
119
|
|---------|-------------|
|
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";
|
|
@@ -204,7 +252,7 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
204
252
|
this.emit("stderr", text);
|
|
205
253
|
}
|
|
206
254
|
});
|
|
207
|
-
this.client = new Client({ name: "run-mcp", version: "1.3.
|
|
255
|
+
this.client = new Client({ name: "run-mcp", version: "1.3.1" }, { capabilities: {} });
|
|
208
256
|
this.client.onclose = () => {
|
|
209
257
|
this._connected = false;
|
|
210
258
|
this._clearStableTimer();
|
|
@@ -262,10 +310,19 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
262
310
|
}
|
|
263
311
|
/**
|
|
264
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.
|
|
265
317
|
*/
|
|
266
|
-
async callTool(name, args = {}) {
|
|
318
|
+
async callTool(name, args = {}, _timeoutMs) {
|
|
267
319
|
this._assertConnected();
|
|
268
|
-
const
|
|
320
|
+
const requestOptions = { timeout: 36e5 * 10 };
|
|
321
|
+
const result = await this.client.callTool(
|
|
322
|
+
{ name, arguments: args },
|
|
323
|
+
void 0,
|
|
324
|
+
requestOptions
|
|
325
|
+
);
|
|
269
326
|
this.recordResponse();
|
|
270
327
|
return result;
|
|
271
328
|
}
|
|
@@ -511,217 +568,6 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
511
568
|
}
|
|
512
569
|
};
|
|
513
570
|
|
|
514
|
-
// src/proxy.ts
|
|
515
|
-
async function startProxy(targetCommand, opts) {
|
|
516
|
-
const [command, ...args] = targetCommand;
|
|
517
|
-
const target = new TargetManager(command, args);
|
|
518
|
-
const interceptor = new ResponseInterceptor({
|
|
519
|
-
outDir: opts.outDir,
|
|
520
|
-
defaultTimeoutMs: opts.timeoutMs,
|
|
521
|
-
maxTextLength: opts.maxTextLength
|
|
522
|
-
});
|
|
523
|
-
target.on("stderr", (text) => {
|
|
524
|
-
process.stderr.write(`[target] ${text}
|
|
525
|
-
`);
|
|
526
|
-
});
|
|
527
|
-
process.stderr.write("[proxy] Connecting to target MCP server...\n");
|
|
528
|
-
try {
|
|
529
|
-
await target.connect();
|
|
530
|
-
} catch (err) {
|
|
531
|
-
process.stderr.write(`[proxy] Failed to connect to target: ${err.message}
|
|
532
|
-
`);
|
|
533
|
-
process.exit(1);
|
|
534
|
-
}
|
|
535
|
-
const status = target.getStatus();
|
|
536
|
-
process.stderr.write(`[proxy] Connected to target (PID: ${status.pid})
|
|
537
|
-
`);
|
|
538
|
-
const targetCaps = target.getServerCapabilities() ?? {};
|
|
539
|
-
const proxyCaps = {};
|
|
540
|
-
proxyCaps.tools = targetCaps.tools ?? {};
|
|
541
|
-
if (targetCaps.resources) proxyCaps.resources = targetCaps.resources;
|
|
542
|
-
if (targetCaps.prompts) proxyCaps.prompts = targetCaps.prompts;
|
|
543
|
-
if (targetCaps.logging) proxyCaps.logging = targetCaps.logging;
|
|
544
|
-
if (targetCaps.completions) proxyCaps.completions = targetCaps.completions;
|
|
545
|
-
process.stderr.write(`[proxy] Mirroring capabilities: ${Object.keys(proxyCaps).join(", ")}
|
|
546
|
-
`);
|
|
547
|
-
const instructions = target.getInstructions();
|
|
548
|
-
if (instructions) {
|
|
549
|
-
process.stderr.write(
|
|
550
|
-
`[proxy] Target instructions: ${instructions.slice(0, 200)}${instructions.length > 200 ? "..." : ""}
|
|
551
|
-
`
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
const mcpServer = new McpServer(
|
|
555
|
-
{
|
|
556
|
-
name: "run-mcp-proxy",
|
|
557
|
-
version: "1.3.0"
|
|
558
|
-
},
|
|
559
|
-
{ capabilities: proxyCaps }
|
|
560
|
-
);
|
|
561
|
-
const server = mcpServer.server;
|
|
562
|
-
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
563
|
-
const result = await target.listTools(request.params);
|
|
564
|
-
return result;
|
|
565
|
-
});
|
|
566
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
567
|
-
const { name, arguments: toolArgs } = request.params;
|
|
568
|
-
try {
|
|
569
|
-
const result = await interceptor.callTool(
|
|
570
|
-
target,
|
|
571
|
-
name,
|
|
572
|
-
toolArgs ?? {}
|
|
573
|
-
);
|
|
574
|
-
return result;
|
|
575
|
-
} catch (err) {
|
|
576
|
-
return {
|
|
577
|
-
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
578
|
-
isError: true
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
if (targetCaps.resources) {
|
|
583
|
-
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
584
|
-
return await target.listResources(request.params);
|
|
585
|
-
});
|
|
586
|
-
server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => {
|
|
587
|
-
return await target.listResourceTemplates(request.params);
|
|
588
|
-
});
|
|
589
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
590
|
-
return await target.readResource(request.params);
|
|
591
|
-
});
|
|
592
|
-
if (targetCaps.resources.subscribe) {
|
|
593
|
-
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
594
|
-
return await target.subscribeResource(request.params);
|
|
595
|
-
});
|
|
596
|
-
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
597
|
-
return await target.unsubscribeResource(request.params);
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (targetCaps.prompts) {
|
|
602
|
-
server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
|
|
603
|
-
return await target.listPrompts(request.params);
|
|
604
|
-
});
|
|
605
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
606
|
-
return await target.getPrompt(request.params);
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
if (targetCaps.logging) {
|
|
610
|
-
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
611
|
-
return await target.setLoggingLevel(request.params.level);
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
if (targetCaps.completions) {
|
|
615
|
-
server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
616
|
-
return await target.complete(request.params);
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
const rawClient = target.getRawClient();
|
|
620
|
-
if (rawClient) {
|
|
621
|
-
if (targetCaps.tools && targetCaps.tools.listChanged) {
|
|
622
|
-
rawClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
623
|
-
server.notification({ method: "notifications/tools/list_changed" });
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
if (targetCaps.resources && targetCaps.resources.listChanged) {
|
|
627
|
-
rawClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
|
|
628
|
-
server.notification({ method: "notifications/resources/list_changed" });
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
if (targetCaps.prompts && targetCaps.prompts.listChanged) {
|
|
632
|
-
rawClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
|
|
633
|
-
server.notification({ method: "notifications/prompts/list_changed" });
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
if (targetCaps.logging) {
|
|
637
|
-
rawClient.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
|
638
|
-
server.notification({
|
|
639
|
-
method: "notifications/message",
|
|
640
|
-
params: notification.params
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
const transport = new StdioServerTransport();
|
|
646
|
-
server.onclose = async () => {
|
|
647
|
-
process.stderr.write("[proxy] Parent disconnected, shutting down...\n");
|
|
648
|
-
await target.close();
|
|
649
|
-
process.exit(0);
|
|
650
|
-
};
|
|
651
|
-
await mcpServer.connect(transport);
|
|
652
|
-
process.stderr.write("[proxy] Proxy server running on stdio.\n");
|
|
653
|
-
target.on("disconnected", () => {
|
|
654
|
-
process.stderr.write("[proxy] Target server disconnected.\n");
|
|
655
|
-
process.exit(1);
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// src/repl.ts
|
|
660
|
-
import { readFile } from "fs/promises";
|
|
661
|
-
import { createInterface } from "readline";
|
|
662
|
-
import pc from "picocolors";
|
|
663
|
-
|
|
664
|
-
// src/parsing.ts
|
|
665
|
-
function parseCommandLine(input) {
|
|
666
|
-
const spaceIdx = input.indexOf(" ");
|
|
667
|
-
if (spaceIdx === -1) {
|
|
668
|
-
return { cmd: input.toLowerCase(), rest: "" };
|
|
669
|
-
}
|
|
670
|
-
return {
|
|
671
|
-
cmd: input.slice(0, spaceIdx).toLowerCase(),
|
|
672
|
-
rest: input.slice(spaceIdx + 1)
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
function parseCallArgs(rest) {
|
|
676
|
-
const trimmed = rest.trim();
|
|
677
|
-
if (!trimmed) return { toolName: "", jsonArgs: "" };
|
|
678
|
-
const spaceIdx = trimmed.indexOf(" ");
|
|
679
|
-
if (spaceIdx === -1) {
|
|
680
|
-
return { toolName: trimmed, jsonArgs: "" };
|
|
681
|
-
}
|
|
682
|
-
const toolName = trimmed.slice(0, spaceIdx);
|
|
683
|
-
let remainder = trimmed.slice(spaceIdx + 1).trim();
|
|
684
|
-
let timeoutMs;
|
|
685
|
-
const timeoutMatch = remainder.match(/\s--timeout\s+(\d+)\s*$/);
|
|
686
|
-
if (timeoutMatch) {
|
|
687
|
-
timeoutMs = parseInt(timeoutMatch[1], 10);
|
|
688
|
-
remainder = remainder.slice(0, timeoutMatch.index).trim();
|
|
689
|
-
}
|
|
690
|
-
return { toolName, jsonArgs: remainder, timeoutMs };
|
|
691
|
-
}
|
|
692
|
-
function formatJson(obj, indent = 2) {
|
|
693
|
-
const json = JSON.stringify(obj, null, indent);
|
|
694
|
-
return json.split("\n").map((line) => " ".repeat(indent) + line).join("\n");
|
|
695
|
-
}
|
|
696
|
-
function levenshtein(a, b) {
|
|
697
|
-
const m = a.length;
|
|
698
|
-
const n = b.length;
|
|
699
|
-
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
700
|
-
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
701
|
-
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
702
|
-
for (let i = 1; i <= m; i++) {
|
|
703
|
-
for (let j = 1; j <= n; j++) {
|
|
704
|
-
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]);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return dp[m][n];
|
|
708
|
-
}
|
|
709
|
-
function suggestCommand(input, commands, threshold = 0.4) {
|
|
710
|
-
let best = null;
|
|
711
|
-
let bestDist = Infinity;
|
|
712
|
-
for (const cmd of commands) {
|
|
713
|
-
const dist = levenshtein(input, cmd);
|
|
714
|
-
if (dist < bestDist) {
|
|
715
|
-
bestDist = dist;
|
|
716
|
-
best = cmd;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
if (best && bestDist <= Math.ceil(input.length * threshold)) {
|
|
720
|
-
return best;
|
|
721
|
-
}
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
571
|
// src/repl.ts
|
|
726
572
|
var KNOWN_COMMANDS = [
|
|
727
573
|
"tools/list",
|
|
@@ -989,8 +835,8 @@ async function readScriptLines(filepath) {
|
|
|
989
835
|
}
|
|
990
836
|
|
|
991
837
|
// src/server.ts
|
|
992
|
-
import { McpServer
|
|
993
|
-
import { StdioServerTransport
|
|
838
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
839
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
994
840
|
import { z } from "zod";
|
|
995
841
|
async function startServer(opts) {
|
|
996
842
|
let target = null;
|
|
@@ -999,8 +845,8 @@ async function startServer(opts) {
|
|
|
999
845
|
defaultTimeoutMs: opts.timeoutMs,
|
|
1000
846
|
maxTextLength: opts.maxTextLength
|
|
1001
847
|
});
|
|
1002
|
-
const mcpServer = new
|
|
1003
|
-
{ name: "run-mcp", version: "1.3.
|
|
848
|
+
const mcpServer = new McpServer(
|
|
849
|
+
{ name: "run-mcp", version: "1.3.1" },
|
|
1004
850
|
{
|
|
1005
851
|
capabilities: {
|
|
1006
852
|
tools: {}
|
|
@@ -1174,6 +1020,54 @@ Check that the command is correct and the server starts without errors. You can
|
|
|
1174
1020
|
}
|
|
1175
1021
|
}
|
|
1176
1022
|
);
|
|
1023
|
+
mcpServer.registerTool(
|
|
1024
|
+
"describe_mcp_tool",
|
|
1025
|
+
{
|
|
1026
|
+
title: "Describe MCP Tool",
|
|
1027
|
+
description: "Get the description and input schema for a specific tool on the connected server.",
|
|
1028
|
+
inputSchema: {
|
|
1029
|
+
name: z.string().describe("Name of the tool to describe")
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
async ({ name }) => {
|
|
1033
|
+
if (!target?.connected) {
|
|
1034
|
+
return {
|
|
1035
|
+
content: [
|
|
1036
|
+
{
|
|
1037
|
+
type: "text",
|
|
1038
|
+
text: "No target server connected. Use connect_to_mcp first."
|
|
1039
|
+
}
|
|
1040
|
+
],
|
|
1041
|
+
isError: true
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
const result = await target.listTools();
|
|
1046
|
+
const tool = result.tools.find((t) => t.name === name);
|
|
1047
|
+
if (!tool) {
|
|
1048
|
+
const available = result.tools.map((t) => t.name).join(", ");
|
|
1049
|
+
return {
|
|
1050
|
+
content: [
|
|
1051
|
+
{
|
|
1052
|
+
type: "text",
|
|
1053
|
+
text: `Tool "${name}" not found.
|
|
1054
|
+
Available tools: ${available}`
|
|
1055
|
+
}
|
|
1056
|
+
],
|
|
1057
|
+
isError: true
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
content: [{ type: "text", text: JSON.stringify(tool, null, 2) }]
|
|
1062
|
+
};
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
return {
|
|
1065
|
+
content: [{ type: "text", text: `Error describing tool: ${err.message}` }],
|
|
1066
|
+
isError: true
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
1177
1071
|
mcpServer.registerTool(
|
|
1178
1072
|
"call_mcp_tool",
|
|
1179
1073
|
{
|
|
@@ -1378,7 +1272,7 @@ Check that the command is correct and the server starts without errors. You can
|
|
|
1378
1272
|
};
|
|
1379
1273
|
}
|
|
1380
1274
|
);
|
|
1381
|
-
const transport = new
|
|
1275
|
+
const transport = new StdioServerTransport();
|
|
1382
1276
|
mcpServer.server.onclose = async () => {
|
|
1383
1277
|
if (target) {
|
|
1384
1278
|
await target.close();
|
|
@@ -1391,96 +1285,60 @@ Check that the command is correct and the server starts without errors. You can
|
|
|
1391
1285
|
}
|
|
1392
1286
|
|
|
1393
1287
|
// src/index.ts
|
|
1394
|
-
program.name("run-mcp").
|
|
1395
|
-
"
|
|
1396
|
-
)
|
|
1288
|
+
program.name("run-mcp").description("A smart interactive REPL and live test harness for MCP servers").version("1.3.4").passThroughOptions().allowUnknownOption().argument(
|
|
1289
|
+
"[target_command...]",
|
|
1290
|
+
"Command to spawn the target MCP server (starts REPL if provided, Agent server otherwise)"
|
|
1291
|
+
).option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option(
|
|
1292
|
+
"-t, --timeout <ms>",
|
|
1293
|
+
"Default tool call timeout in milliseconds (default: 300000) (Agent Mode only)"
|
|
1294
|
+
).option(
|
|
1295
|
+
"--max-text <chars>",
|
|
1296
|
+
"Max text response length before truncation (default: 50000) (Agent Mode only)"
|
|
1297
|
+
).option("-s, --script <file>", "Read commands from a file instead of stdin (REPL Mode only)").addHelpText(
|
|
1397
1298
|
"after",
|
|
1398
1299
|
`
|
|
1399
1300
|
Examples:
|
|
1400
|
-
$ run-mcp
|
|
1401
|
-
$ run-mcp
|
|
1402
|
-
$ run-mcp
|
|
1403
|
-
$ run-mcp server
|
|
1404
|
-
$ run-mcp
|
|
1301
|
+
$ run-mcp # Test harness (agent mode)
|
|
1302
|
+
$ run-mcp node my-server.js # Interactive testing (human REPL mode)
|
|
1303
|
+
$ run-mcp node my-server.js -s test.txt # Run a script in REPL mode
|
|
1304
|
+
$ run-mcp npx -y some-mcp-server # Test an npx server
|
|
1305
|
+
$ run-mcp --out-dir ./test-output # Agent mode with options
|
|
1306
|
+
$ run-mcp --out-dir ./screenshots node srv.js # REPL mode with options
|
|
1405
1307
|
|
|
1406
|
-
|
|
1407
|
-
);
|
|
1408
|
-
if (process.argv.length <= 2) {
|
|
1409
|
-
program.outputHelp();
|
|
1410
|
-
process.exit(0);
|
|
1411
|
-
}
|
|
1412
|
-
program.command("repl").description("Start an interactive REPL session with a target MCP server").passThroughOptions().allowUnknownOption().argument("<target_command...>", "Command to spawn the target MCP server").option("-s, --script <file>", "Read commands from a file instead of stdin").option("-o, --out-dir <path>", "Directory to save intercepted images").addHelpText(
|
|
1413
|
-
"after",
|
|
1414
|
-
`
|
|
1415
|
-
Examples:
|
|
1416
|
-
$ run-mcp repl node my-server.js
|
|
1417
|
-
$ run-mcp repl node my-server.js --script verify.txt
|
|
1418
|
-
$ run-mcp repl node my-server.js --out-dir ./screenshots
|
|
1419
|
-
|
|
1420
|
-
REPL Commands (once connected):
|
|
1421
|
-
tools/list List all available tools
|
|
1422
|
-
tools/describe <name> Show a tool's input schema
|
|
1423
|
-
tools/call <name> <json> [opts] Call a tool with JSON arguments
|
|
1424
|
-
status Show target server status
|
|
1425
|
-
help Show all commands`
|
|
1426
|
-
).action(async (targetCommand, opts) => {
|
|
1427
|
-
await startRepl(targetCommand, opts);
|
|
1428
|
-
});
|
|
1429
|
-
program.command("proxy").description("Start as a transparent MCP proxy between an AI agent and a target server").passThroughOptions().allowUnknownOption().argument("<target_command...>", "Command to spawn the target MCP server").option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option("-t, --timeout <ms>", "Default tool call timeout in milliseconds (default: 60000)").option("--max-text <chars>", "Max text response length before truncation (default: 50000)").addHelpText(
|
|
1430
|
-
"after",
|
|
1431
|
-
`
|
|
1432
|
-
Examples:
|
|
1433
|
-
$ run-mcp proxy node my-server.js
|
|
1434
|
-
$ run-mcp proxy node my-server.js --out-dir ./images
|
|
1435
|
-
$ run-mcp proxy node my-server.js --timeout 120000
|
|
1436
|
-
$ run-mcp proxy node my-server.js --max-text 100000
|
|
1437
|
-
|
|
1438
|
-
Use this in your MCP client configuration to wrap any MCP server:
|
|
1439
|
-
{
|
|
1440
|
-
"mcpServers": {
|
|
1441
|
-
"my-server": {
|
|
1442
|
-
"command": "run-mcp",
|
|
1443
|
-
"args": ["proxy", "node", "my-server.js"]
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}`
|
|
1447
|
-
).action(
|
|
1448
|
-
async (targetCommand, opts) => {
|
|
1449
|
-
await startProxy(targetCommand, {
|
|
1450
|
-
outDir: opts.outDir,
|
|
1451
|
-
timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
|
|
1452
|
-
maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
);
|
|
1456
|
-
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: 60000)").option("--max-text <chars>", "Max text response length before truncation (default: 50000)").addHelpText(
|
|
1457
|
-
"after",
|
|
1458
|
-
`
|
|
1459
|
-
Examples:
|
|
1460
|
-
$ run-mcp server
|
|
1461
|
-
$ run-mcp server --out-dir ./test-output
|
|
1462
|
-
$ run-mcp server --timeout 120000
|
|
1463
|
-
|
|
1464
|
-
Add to your MCP client configuration:
|
|
1308
|
+
Agent Mode Configuration (mcp.json):
|
|
1465
1309
|
{
|
|
1466
1310
|
"mcpServers": {
|
|
1467
1311
|
"run-mcp": {
|
|
1468
1312
|
"command": "npx",
|
|
1469
|
-
"args": ["-y", "run-mcp"
|
|
1313
|
+
"args": ["-y", "run-mcp"]
|
|
1470
1314
|
}
|
|
1471
1315
|
}
|
|
1472
1316
|
}
|
|
1473
1317
|
|
|
1474
|
-
|
|
1318
|
+
Agent Mode Tools:
|
|
1475
1319
|
connect_to_mcp \u2192 Spawn and connect to a local MCP server
|
|
1476
1320
|
list_mcp_tools \u2192 List tools on the connected server
|
|
1321
|
+
describe_mcp_tool \u2192 Show a tool's input schema
|
|
1477
1322
|
call_mcp_tool \u2192 Call a tool (with interception)
|
|
1478
|
-
disconnect_from_mcp \u2192 Tear down and reconnect after changes
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1323
|
+
disconnect_from_mcp \u2192 Tear down and reconnect after changes
|
|
1324
|
+
|
|
1325
|
+
REPL Mode Commands (once connected):
|
|
1326
|
+
tools/list List all available tools
|
|
1327
|
+
tools/describe <name> Show a tool's input schema
|
|
1328
|
+
tools/call <name> <json> [opts] Call a tool with JSON arguments
|
|
1329
|
+
status Show target server status
|
|
1330
|
+
help Show all commands`
|
|
1331
|
+
).action(
|
|
1332
|
+
async (targetCommand, opts) => {
|
|
1333
|
+
if (targetCommand && targetCommand.length > 0) {
|
|
1334
|
+
await startRepl(targetCommand, { script: opts.script, outDir: opts.outDir });
|
|
1335
|
+
} else {
|
|
1336
|
+
await startServer({
|
|
1337
|
+
outDir: opts.outDir,
|
|
1338
|
+
timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
|
|
1339
|
+
maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
);
|
|
1486
1344
|
program.parse();
|