run-mcp 1.3.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 +6 -28
- package/dist/index.js +130 -272
- 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. **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.
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
### Interception Rules (Server Mode & 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,14 +53,6 @@ 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
58
|
```
|
|
@@ -71,7 +60,7 @@ run-mcp <command> [options]
|
|
|
71
60
|
|
|
72
61
|
Commands:
|
|
73
62
|
repl <target_command...> Start an interactive REPL session
|
|
74
|
-
|
|
63
|
+
server Start as an MCP server for agents
|
|
75
64
|
|
|
76
65
|
Options:
|
|
77
66
|
-V, --version Show version number
|
|
@@ -88,17 +77,6 @@ Options:
|
|
|
88
77
|
-o, --out-dir <path> Directory to save intercepted images (default: $TMPDIR/run-mcp)
|
|
89
78
|
```
|
|
90
79
|
|
|
91
|
-
### Proxy Command
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
run-mcp proxy <target_command...> [options]
|
|
95
|
-
|
|
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
|
-
|
|
102
80
|
### Server Command
|
|
103
81
|
|
|
104
82
|
```
|
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,15 @@ 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({ name, arguments: args }, void 0, requestOptions);
|
|
269
322
|
this.recordResponse();
|
|
270
323
|
return result;
|
|
271
324
|
}
|
|
@@ -511,217 +564,6 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
511
564
|
}
|
|
512
565
|
};
|
|
513
566
|
|
|
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
567
|
// src/repl.ts
|
|
726
568
|
var KNOWN_COMMANDS = [
|
|
727
569
|
"tools/list",
|
|
@@ -989,8 +831,8 @@ async function readScriptLines(filepath) {
|
|
|
989
831
|
}
|
|
990
832
|
|
|
991
833
|
// src/server.ts
|
|
992
|
-
import { McpServer
|
|
993
|
-
import { StdioServerTransport
|
|
834
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
835
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
994
836
|
import { z } from "zod";
|
|
995
837
|
async function startServer(opts) {
|
|
996
838
|
let target = null;
|
|
@@ -999,8 +841,8 @@ async function startServer(opts) {
|
|
|
999
841
|
defaultTimeoutMs: opts.timeoutMs,
|
|
1000
842
|
maxTextLength: opts.maxTextLength
|
|
1001
843
|
});
|
|
1002
|
-
const mcpServer = new
|
|
1003
|
-
{ name: "run-mcp", version: "1.3.
|
|
844
|
+
const mcpServer = new McpServer(
|
|
845
|
+
{ name: "run-mcp", version: "1.3.1" },
|
|
1004
846
|
{
|
|
1005
847
|
capabilities: {
|
|
1006
848
|
tools: {}
|
|
@@ -1174,6 +1016,50 @@ Check that the command is correct and the server starts without errors. You can
|
|
|
1174
1016
|
}
|
|
1175
1017
|
}
|
|
1176
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
|
+
);
|
|
1177
1063
|
mcpServer.registerTool(
|
|
1178
1064
|
"call_mcp_tool",
|
|
1179
1065
|
{
|
|
@@ -1378,7 +1264,7 @@ Check that the command is correct and the server starts without errors. You can
|
|
|
1378
1264
|
};
|
|
1379
1265
|
}
|
|
1380
1266
|
);
|
|
1381
|
-
const transport = new
|
|
1267
|
+
const transport = new StdioServerTransport();
|
|
1382
1268
|
mcpServer.server.onclose = async () => {
|
|
1383
1269
|
if (target) {
|
|
1384
1270
|
await target.close();
|
|
@@ -1392,14 +1278,13 @@ Check that the command is correct and the server starts without errors. You can
|
|
|
1392
1278
|
|
|
1393
1279
|
// src/index.ts
|
|
1394
1280
|
program.name("run-mcp").enablePositionalOptions().description(
|
|
1395
|
-
"A smart
|
|
1396
|
-
).version("1.3.
|
|
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(
|
|
1397
1283
|
"after",
|
|
1398
1284
|
`
|
|
1399
1285
|
Examples:
|
|
1400
1286
|
$ run-mcp repl node my-server.js # Interactive testing (human)
|
|
1401
1287
|
$ run-mcp repl node my-server.js -s test.txt # Run a script
|
|
1402
|
-
$ run-mcp proxy node my-server.js # Transparent proxy (agent)
|
|
1403
1288
|
$ run-mcp server # Test harness (agent)
|
|
1404
1289
|
$ run-mcp repl npx -y some-mcp-server # Test an npx server
|
|
1405
1290
|
|
|
@@ -1426,34 +1311,7 @@ REPL Commands (once connected):
|
|
|
1426
1311
|
).action(async (targetCommand, opts) => {
|
|
1427
1312
|
await startRepl(targetCommand, opts);
|
|
1428
1313
|
});
|
|
1429
|
-
program.command("
|
|
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(
|
|
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(
|
|
1457
1315
|
"after",
|
|
1458
1316
|
`
|
|
1459
1317
|
Examples:
|