run-mcp 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +43 -3
  2. package/dist/index.js +455 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # run-mcp
2
2
 
3
- A smart proxy and interactive REPL for [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers.
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 two modes:
5
+ `run-mcp` wraps any MCP server and operates in three modes:
6
6
 
7
7
  | Mode | Audience | Purpose |
8
8
  |------|----------|---------|
9
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 |
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 |
11
12
 
12
13
  ## Why?
13
14
 
@@ -98,6 +99,45 @@ Options:
98
99
  --max-text <chars> Max text response length before truncation (default: 50000)
99
100
  ```
100
101
 
102
+ ### Server Command
103
+
104
+ ```
105
+ run-mcp server [options]
106
+
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
+ ```
112
+
113
+ Add to your MCP client configuration:
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "run-mcp": {
119
+ "command": "npx",
120
+ "args": ["-y", "run-mcp", "server"]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ Then use these tools from your agent:
127
+
128
+ | Tool | Description |
129
+ |------|-------------|
130
+ | `connect_to_mcp` | Spawn and connect to a local MCP server |
131
+ | `disconnect_from_mcp` | Tear down the connection |
132
+ | `mcp_server_status` | Check connection status |
133
+ | `list_mcp_tools` | List tools on the connected server |
134
+ | `call_mcp_tool` | Call a tool (with interception) |
135
+ | `list_mcp_resources` | List resources |
136
+ | `read_mcp_resource` | Read a resource by URI |
137
+ | `list_mcp_prompts` | List prompts |
138
+ | `get_mcp_prompt` | Get a prompt by name |
139
+ | `get_mcp_server_stderr` | View target server stderr output |
140
+
101
141
  ## REPL Commands
102
142
 
103
143
  Once in the REPL, these commands are available:
package/dist/index.js CHANGED
@@ -167,6 +167,8 @@ var TargetManager = class _TargetManager extends EventEmitter {
167
167
  // Enhanced status tracking
168
168
  _lastResponseTime = null;
169
169
  _stderrLineCount = 0;
170
+ _stderrLines = [];
171
+ static MAX_STDERR_LINES = 200;
170
172
  // Auto-reconnect state
171
173
  _reconnectAttempts = 0;
172
174
  _stableTimer = null;
@@ -193,11 +195,16 @@ var TargetManager = class _TargetManager extends EventEmitter {
193
195
  this.transport.stderr?.on("data", (chunk) => {
194
196
  const text = chunk.toString().trimEnd();
195
197
  if (text) {
196
- this._stderrLineCount += text.split("\n").length;
198
+ const lines = text.split("\n");
199
+ this._stderrLineCount += lines.length;
200
+ this._stderrLines.push(...lines);
201
+ if (this._stderrLines.length > _TargetManager.MAX_STDERR_LINES) {
202
+ this._stderrLines = this._stderrLines.slice(-_TargetManager.MAX_STDERR_LINES);
203
+ }
197
204
  this.emit("stderr", text);
198
205
  }
199
206
  });
200
- this.client = new Client({ name: "run-mcp", version: "1.2.0" }, { capabilities: {} });
207
+ this.client = new Client({ name: "run-mcp", version: "1.3.0" }, { capabilities: {} });
201
208
  this.client.onclose = () => {
202
209
  this._connected = false;
203
210
  this._clearStableTimer();
@@ -360,6 +367,14 @@ var TargetManager = class _TargetManager extends EventEmitter {
360
367
  return this.client;
361
368
  }
362
369
  // ─── Status & lifecycle ─────────────────────────────────────────────────────
370
+ /**
371
+ * Returns the last N lines of stderr output from the target server.
372
+ * Useful for debugging crashes or unexpected behavior.
373
+ */
374
+ getStderrLines(count) {
375
+ if (!count || count >= this._stderrLines.length) return [...this._stderrLines];
376
+ return this._stderrLines.slice(-count);
377
+ }
363
378
  /**
364
379
  * Returns current connection status, PID, uptime, and diagnostics.
365
380
  */
@@ -539,7 +554,7 @@ async function startProxy(targetCommand, opts) {
539
554
  const mcpServer = new McpServer(
540
555
  {
541
556
  name: "run-mcp-proxy",
542
- version: "1.2.0"
557
+ version: "1.3.0"
543
558
  },
544
559
  { capabilities: proxyCaps }
545
560
  );
@@ -973,16 +988,419 @@ async function readScriptLines(filepath) {
973
988
  return content.split("\n");
974
989
  }
975
990
 
991
+ // 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";
994
+ import { z } from "zod";
995
+ async function startServer(opts) {
996
+ let target = null;
997
+ const interceptor = new ResponseInterceptor({
998
+ outDir: opts.outDir,
999
+ defaultTimeoutMs: opts.timeoutMs,
1000
+ maxTextLength: opts.maxTextLength
1001
+ });
1002
+ const mcpServer = new McpServer2(
1003
+ { name: "run-mcp", version: "1.3.0" },
1004
+ {
1005
+ capabilities: {
1006
+ tools: {}
1007
+ }
1008
+ }
1009
+ );
1010
+ mcpServer.registerTool(
1011
+ "connect_to_mcp",
1012
+ {
1013
+ title: "Connect to MCP Server",
1014
+ 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.",
1015
+ inputSchema: {
1016
+ command: z.string().describe("Command to run (e.g. 'node', 'python', 'npx')"),
1017
+ args: z.array(z.string()).optional().describe("Arguments to pass (e.g. ['src/index.js'] or ['-y', 'some-server'])"),
1018
+ env: z.record(z.string()).optional().describe("Extra environment variables for the child process")
1019
+ }
1020
+ },
1021
+ async ({ command, args, env }) => {
1022
+ if (target?.connected) {
1023
+ return {
1024
+ content: [
1025
+ {
1026
+ type: "text",
1027
+ text: "Already connected to a target server. Call disconnect_from_mcp first, then connect again."
1028
+ }
1029
+ ],
1030
+ isError: true
1031
+ };
1032
+ }
1033
+ if (target) {
1034
+ await target.close();
1035
+ target = null;
1036
+ }
1037
+ try {
1038
+ if (env) {
1039
+ for (const [key, value] of Object.entries(env)) {
1040
+ process.env[key] = value;
1041
+ }
1042
+ }
1043
+ target = new TargetManager(command, args ?? []);
1044
+ await target.connect();
1045
+ const status = target.getStatus();
1046
+ const caps = target.getServerCapabilities() ?? {};
1047
+ const capSummary = [];
1048
+ if (caps.tools) capSummary.push("tools");
1049
+ if (caps.resources) capSummary.push("resources");
1050
+ if (caps.prompts) capSummary.push("prompts");
1051
+ if (caps.logging) capSummary.push("logging");
1052
+ let toolCount = 0;
1053
+ try {
1054
+ const tools = await target.listTools();
1055
+ toolCount = tools.tools.length;
1056
+ } catch {
1057
+ }
1058
+ const lines = [
1059
+ `Connected to MCP server (PID: ${status.pid})`,
1060
+ `Command: ${command} ${(args ?? []).join(" ")}`,
1061
+ `Capabilities: ${capSummary.join(", ") || "none"}`,
1062
+ `Tools available: ${toolCount}`,
1063
+ "",
1064
+ "Use list_mcp_tools, call_mcp_tool, list_mcp_resources, etc. to interact with it.",
1065
+ "Use disconnect_from_mcp when done, or to reconnect after code changes."
1066
+ ];
1067
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1068
+ } catch (err) {
1069
+ target = null;
1070
+ return {
1071
+ content: [
1072
+ {
1073
+ type: "text",
1074
+ text: `Failed to connect: ${err.message}
1075
+
1076
+ 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.`
1077
+ }
1078
+ ],
1079
+ isError: true
1080
+ };
1081
+ }
1082
+ }
1083
+ );
1084
+ mcpServer.registerTool(
1085
+ "disconnect_from_mcp",
1086
+ {
1087
+ title: "Disconnect from MCP Server",
1088
+ description: "Tear down the current MCP server connection. Call this before reconnecting after code changes."
1089
+ },
1090
+ async () => {
1091
+ if (!target) {
1092
+ return {
1093
+ content: [{ type: "text", text: "No target server is connected." }],
1094
+ isError: true
1095
+ };
1096
+ }
1097
+ const status = target.getStatus();
1098
+ await target.close();
1099
+ target = null;
1100
+ return {
1101
+ content: [
1102
+ {
1103
+ type: "text",
1104
+ text: `Disconnected from MCP server (was PID: ${status.pid}, uptime: ${status.uptime.toFixed(1)}s).`
1105
+ }
1106
+ ]
1107
+ };
1108
+ }
1109
+ );
1110
+ mcpServer.registerTool(
1111
+ "mcp_server_status",
1112
+ {
1113
+ title: "MCP Server Status",
1114
+ description: "Check the current target server connection status, PID, uptime, and capabilities."
1115
+ },
1116
+ async () => {
1117
+ if (!target) {
1118
+ return {
1119
+ content: [
1120
+ {
1121
+ type: "text",
1122
+ text: "No target server connected. Use connect_to_mcp to connect to one."
1123
+ }
1124
+ ]
1125
+ };
1126
+ }
1127
+ const status = target.getStatus();
1128
+ const caps = target.getServerCapabilities() ?? {};
1129
+ const lines = [
1130
+ `Connected: ${status.connected}`,
1131
+ `PID: ${status.pid}`,
1132
+ `Uptime: ${status.uptime.toFixed(1)}s`,
1133
+ `Command: ${status.command} ${status.args.join(" ")}`,
1134
+ `Capabilities: ${Object.keys(caps).join(", ") || "none"}`,
1135
+ `Stderr lines: ${status.stderrLineCount}`,
1136
+ `Last response: ${status.lastResponseTime ? new Date(status.lastResponseTime).toISOString() : "none"}`
1137
+ ];
1138
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1139
+ }
1140
+ );
1141
+ mcpServer.registerTool(
1142
+ "list_mcp_tools",
1143
+ {
1144
+ title: "List MCP Tools",
1145
+ description: "List all tools exposed by the connected MCP server, including descriptions, input schemas, and annotations."
1146
+ },
1147
+ async () => {
1148
+ if (!target?.connected) {
1149
+ return {
1150
+ content: [
1151
+ {
1152
+ type: "text",
1153
+ text: "No target server connected. Use connect_to_mcp first."
1154
+ }
1155
+ ],
1156
+ isError: true
1157
+ };
1158
+ }
1159
+ try {
1160
+ const result = await target.listTools();
1161
+ return {
1162
+ content: [
1163
+ {
1164
+ type: "text",
1165
+ text: JSON.stringify(result.tools, null, 2)
1166
+ }
1167
+ ]
1168
+ };
1169
+ } catch (err) {
1170
+ return {
1171
+ content: [{ type: "text", text: `Error listing tools: ${err.message}` }],
1172
+ isError: true
1173
+ };
1174
+ }
1175
+ }
1176
+ );
1177
+ mcpServer.registerTool(
1178
+ "call_mcp_tool",
1179
+ {
1180
+ title: "Call MCP Tool",
1181
+ 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.",
1182
+ inputSchema: {
1183
+ name: z.string().describe("Name of the tool to call"),
1184
+ arguments: z.record(z.unknown()).optional().describe("Arguments to pass to the tool (as a JSON object)"),
1185
+ timeout_ms: z.number().optional().describe("Timeout for this specific call in milliseconds (overrides default)")
1186
+ }
1187
+ },
1188
+ async ({ name, arguments: toolArgs, timeout_ms }) => {
1189
+ if (!target?.connected) {
1190
+ return {
1191
+ content: [
1192
+ {
1193
+ type: "text",
1194
+ text: "No target server connected. Use connect_to_mcp first."
1195
+ }
1196
+ ],
1197
+ isError: true
1198
+ };
1199
+ }
1200
+ try {
1201
+ const result = await interceptor.callTool(
1202
+ target,
1203
+ name,
1204
+ toolArgs ?? {},
1205
+ timeout_ms
1206
+ );
1207
+ return result;
1208
+ } catch (err) {
1209
+ return {
1210
+ content: [{ type: "text", text: `Error: ${err.message}` }],
1211
+ isError: true
1212
+ };
1213
+ }
1214
+ }
1215
+ );
1216
+ mcpServer.registerTool(
1217
+ "list_mcp_resources",
1218
+ {
1219
+ title: "List MCP Resources",
1220
+ description: "List all resources exposed by the connected MCP server."
1221
+ },
1222
+ async () => {
1223
+ if (!target?.connected) {
1224
+ return {
1225
+ content: [
1226
+ {
1227
+ type: "text",
1228
+ text: "No target server connected. Use connect_to_mcp first."
1229
+ }
1230
+ ],
1231
+ isError: true
1232
+ };
1233
+ }
1234
+ try {
1235
+ const result = await target.listResources();
1236
+ return {
1237
+ content: [{ type: "text", text: JSON.stringify(result.resources, null, 2) }]
1238
+ };
1239
+ } catch (err) {
1240
+ return {
1241
+ content: [{ type: "text", text: `Error listing resources: ${err.message}` }],
1242
+ isError: true
1243
+ };
1244
+ }
1245
+ }
1246
+ );
1247
+ mcpServer.registerTool(
1248
+ "read_mcp_resource",
1249
+ {
1250
+ title: "Read MCP Resource",
1251
+ description: "Read a specific resource by URI from the connected MCP server.",
1252
+ inputSchema: {
1253
+ uri: z.string().describe("URI of the resource to read (e.g. 'docs://readme')")
1254
+ }
1255
+ },
1256
+ async ({ uri }) => {
1257
+ if (!target?.connected) {
1258
+ return {
1259
+ content: [
1260
+ {
1261
+ type: "text",
1262
+ text: "No target server connected. Use connect_to_mcp first."
1263
+ }
1264
+ ],
1265
+ isError: true
1266
+ };
1267
+ }
1268
+ try {
1269
+ const result = await target.readResource({ uri });
1270
+ return {
1271
+ content: [{ type: "text", text: JSON.stringify(result.contents, null, 2) }]
1272
+ };
1273
+ } catch (err) {
1274
+ return {
1275
+ content: [{ type: "text", text: `Error reading resource: ${err.message}` }],
1276
+ isError: true
1277
+ };
1278
+ }
1279
+ }
1280
+ );
1281
+ mcpServer.registerTool(
1282
+ "list_mcp_prompts",
1283
+ {
1284
+ title: "List MCP Prompts",
1285
+ description: "List all prompts exposed by the connected MCP server."
1286
+ },
1287
+ async () => {
1288
+ if (!target?.connected) {
1289
+ return {
1290
+ content: [
1291
+ {
1292
+ type: "text",
1293
+ text: "No target server connected. Use connect_to_mcp first."
1294
+ }
1295
+ ],
1296
+ isError: true
1297
+ };
1298
+ }
1299
+ try {
1300
+ const result = await target.listPrompts();
1301
+ return {
1302
+ content: [{ type: "text", text: JSON.stringify(result.prompts, null, 2) }]
1303
+ };
1304
+ } catch (err) {
1305
+ return {
1306
+ content: [{ type: "text", text: `Error listing prompts: ${err.message}` }],
1307
+ isError: true
1308
+ };
1309
+ }
1310
+ }
1311
+ );
1312
+ mcpServer.registerTool(
1313
+ "get_mcp_prompt",
1314
+ {
1315
+ title: "Get MCP Prompt",
1316
+ description: "Get a specific prompt by name from the connected MCP server.",
1317
+ inputSchema: {
1318
+ name: z.string().describe("Name of the prompt"),
1319
+ arguments: z.record(z.string()).optional().describe("Arguments to pass to the prompt")
1320
+ }
1321
+ },
1322
+ async ({ name, arguments: promptArgs }) => {
1323
+ if (!target?.connected) {
1324
+ return {
1325
+ content: [
1326
+ {
1327
+ type: "text",
1328
+ text: "No target server connected. Use connect_to_mcp first."
1329
+ }
1330
+ ],
1331
+ isError: true
1332
+ };
1333
+ }
1334
+ try {
1335
+ const result = await target.getPrompt({
1336
+ name,
1337
+ arguments: promptArgs ?? {}
1338
+ });
1339
+ return {
1340
+ content: [{ type: "text", text: JSON.stringify(result.messages, null, 2) }]
1341
+ };
1342
+ } catch (err) {
1343
+ return {
1344
+ content: [{ type: "text", text: `Error getting prompt: ${err.message}` }],
1345
+ isError: true
1346
+ };
1347
+ }
1348
+ }
1349
+ );
1350
+ mcpServer.registerTool(
1351
+ "get_mcp_server_stderr",
1352
+ {
1353
+ title: "Get MCP Server Stderr",
1354
+ description: "Get recent stderr output from the target MCP server. Useful for debugging crashes, startup failures, or unexpected behavior.",
1355
+ inputSchema: {
1356
+ lines: z.number().optional().describe("Number of recent lines to return (default: all, max 200)")
1357
+ }
1358
+ },
1359
+ async ({ lines }) => {
1360
+ if (!target) {
1361
+ return {
1362
+ content: [
1363
+ {
1364
+ type: "text",
1365
+ text: "No target server (current or previous). Nothing to show."
1366
+ }
1367
+ ]
1368
+ };
1369
+ }
1370
+ const stderrLines = target.getStderrLines(lines);
1371
+ if (stderrLines.length === 0) {
1372
+ return {
1373
+ content: [{ type: "text", text: "No stderr output captured." }]
1374
+ };
1375
+ }
1376
+ return {
1377
+ content: [{ type: "text", text: stderrLines.join("\n") }]
1378
+ };
1379
+ }
1380
+ );
1381
+ const transport = new StdioServerTransport2();
1382
+ mcpServer.server.onclose = async () => {
1383
+ if (target) {
1384
+ await target.close();
1385
+ }
1386
+ process.exit(0);
1387
+ };
1388
+ await mcpServer.connect(transport);
1389
+ process.stderr.write("[server] run-mcp test harness running on stdio.\n");
1390
+ process.stderr.write("[server] Waiting for connect_to_mcp call...\n");
1391
+ }
1392
+
976
1393
  // src/index.ts
977
1394
  program.name("run-mcp").enablePositionalOptions().description(
978
- "A smart proxy and interactive REPL for Model Context Protocol (MCP) servers.\n\nOperates in two 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"
979
- ).version("1.2.0").addHelpText(
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(
980
1397
  "after",
981
1398
  `
982
1399
  Examples:
983
- $ run-mcp repl node my-server.js # Interactive testing
1400
+ $ run-mcp repl node my-server.js # Interactive testing (human)
984
1401
  $ run-mcp repl node my-server.js -s test.txt # Run a script
985
- $ run-mcp proxy node my-server.js # Proxy for AI agents
1402
+ $ run-mcp proxy node my-server.js # Transparent proxy (agent)
1403
+ $ run-mcp server # Test harness (agent)
986
1404
  $ run-mcp repl npx -y some-mcp-server # Test an npx server
987
1405
 
988
1406
  Run 'run-mcp <command> --help' for detailed options.`
@@ -1035,4 +1453,34 @@ Use this in your MCP client configuration to wrap any MCP server:
1035
1453
  });
1036
1454
  }
1037
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:
1465
+ {
1466
+ "mcpServers": {
1467
+ "run-mcp": {
1468
+ "command": "npx",
1469
+ "args": ["-y", "run-mcp", "server"]
1470
+ }
1471
+ }
1472
+ }
1473
+
1474
+ Then use these tools from your agent:
1475
+ connect_to_mcp \u2192 Spawn and connect to a local MCP server
1476
+ list_mcp_tools \u2192 List tools on the connected server
1477
+ 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
+ });
1038
1486
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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": {