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.
Files changed (3) hide show
  1. package/README.md +27 -54
  2. package/dist/index.js +176 -318
  3. 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` wraps any MCP server and operates in three modes:
5
+ `run-mcp` operates in two modes:
6
6
 
7
- | Mode | Audience | Purpose |
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
- ## Why?
10
+ ### Interception Rules (Agent Server & REPL)
14
11
 
15
- MCP servers often return large base64-encoded images (screenshots, charts) or massive JSON payloads that can blow up an AI agent's context window. `run-mcp` sits between the agent and the server, transparently:
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
- -h, --help Show help
79
- ```
80
-
81
- ### REPL Command
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
- Options:
87
- -s, --script <file> Read commands from a file instead of stdin
88
- -o, --out-dir <path> Directory to save intercepted images (default: $TMPDIR/run-mcp)
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
- ### Server Command
78
+ ## Agent Use Cases
103
79
 
104
- ```
105
- run-mcp server [options]
80
+ ### Dynamic Testing
106
81
 
107
- Options:
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
- Add to your MCP client configuration:
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", "server"]
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 in the REPL, these commands are available:
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/proxy.ts
7
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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 = 6e4;
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 result = await Promise.race([target.callTool(name, args), this._timeout(timeout, name)]);
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.0" }, { capabilities: {} });
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 result = await this.client.callTool({ name, arguments: args });
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 as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
993
- import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
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 McpServer2(
1003
- { name: "run-mcp", version: "1.3.0" },
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 StdioServerTransport2();
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").enablePositionalOptions().description(
1395
- "A smart proxy, interactive REPL, and live test harness for MCP servers.\n\nOperates in three modes:\n repl - Human-friendly CLI for testing MCP servers interactively\n proxy - Transparent MCP proxy that intercepts images, enforces timeouts,\n and truncates large payloads to protect an AI agent's context window\n server - MCP server that lets AI agents dynamically test local MCP servers"
1396
- ).version("1.3.0").addHelpText(
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 repl node my-server.js # Interactive testing (human)
1401
- $ 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
- $ run-mcp server # Test harness (agent)
1404
- $ run-mcp repl npx -y some-mcp-server # Test an npx server
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
- Run 'run-mcp <command> --help' for detailed options.`
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", "server"]
1313
+ "args": ["-y", "run-mcp"]
1470
1314
  }
1471
1315
  }
1472
1316
  }
1473
1317
 
1474
- Then use these tools from your agent:
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
- ).action(async (opts) => {
1480
- await startServer({
1481
- outDir: opts.outDir,
1482
- timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
1483
- maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.4",
4
4
  "description": "A smart proxy and interactive REPL for Model Context Protocol (MCP) servers",
5
5
  "homepage": "https://github.com/funkyfunc/run-mcp#readme",
6
6
  "bugs": {