run-mcp 1.0.0 → 1.2.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 +246 -1
  2. package/dist/index.js +1038 -0
  3. package/package.json +41 -5
package/README.md CHANGED
@@ -1 +1,246 @@
1
- # run-mcp
1
+ # run-mcp
2
+
3
+ A smart proxy and interactive REPL for [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers.
4
+
5
+ `run-mcp` wraps any MCP server and operates in two modes:
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 MCP proxy that intercepts responses to save images to disk, enforce timeouts, and truncate massive payloads |
11
+
12
+ ## Why?
13
+
14
+ 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:
15
+
16
+ - **Saving images to disk** instead of passing multi-MB base64 strings through
17
+ - **Enforcing timeouts** so a hung tool call doesn't block forever
18
+ - **Truncating huge text** responses to protect context budgets
19
+
20
+ For humans, the REPL mode provides a quick way to test any MCP server without writing client code.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install
26
+ npm run build
27
+ ```
28
+
29
+ To install globally (makes `run-mcp` available system-wide):
30
+
31
+ ```bash
32
+ npm install -g .
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### REPL Mode — Test an MCP server interactively
38
+
39
+ ```bash
40
+ # Start a REPL session with any MCP server
41
+ run-mcp repl node path/to/my-mcp-server.js
42
+
43
+ # Or use npx without installing globally
44
+ npx . repl node path/to/my-mcp-server.js
45
+ ```
46
+
47
+ You'll see an interactive prompt:
48
+
49
+ ```
50
+ ⟳ Connecting to target MCP server...
51
+ Command: node path/to/my-mcp-server.js
52
+ ✓ Connected (PID: 12345)
53
+ 5 tool(s) available. Type help for commands.
54
+
55
+ >
56
+ ```
57
+
58
+ ### Proxy Mode — Protect your agent's context
59
+
60
+ ```bash
61
+ run-mcp proxy node path/to/my-mcp-server.js --out-dir ./captured-images
62
+ ```
63
+
64
+ Then point your AI agent at `run-mcp` as the MCP server command. It transparently forwards all tools while sanitizing responses.
65
+
66
+ ## Usage
67
+
68
+ ```
69
+ run-mcp <command> [options]
70
+
71
+ Commands:
72
+ repl <target_command...> Start an interactive REPL session
73
+ proxy <target_command...> Start as a transparent MCP proxy
74
+
75
+ Options:
76
+ -V, --version Show version number
77
+ -h, --help Show help
78
+ ```
79
+
80
+ ### REPL Command
81
+
82
+ ```
83
+ run-mcp repl <target_command...> [options]
84
+
85
+ Options:
86
+ -s, --script <file> Read commands from a file instead of stdin
87
+ -o, --out-dir <path> Directory to save intercepted images (default: $TMPDIR/run-mcp)
88
+ ```
89
+
90
+ ### Proxy Command
91
+
92
+ ```
93
+ run-mcp proxy <target_command...> [options]
94
+
95
+ Options:
96
+ -o, --out-dir <path> Directory to save intercepted images and audio (default: $TMPDIR/run-mcp)
97
+ -t, --timeout <ms> Default tool call timeout in milliseconds (default: 60000)
98
+ --max-text <chars> Max text response length before truncation (default: 50000)
99
+ ```
100
+
101
+ ## REPL Commands
102
+
103
+ Once in the REPL, these commands are available:
104
+
105
+ | Command | Description |
106
+ |---------|-------------|
107
+ | `tools/list` | List all tools exposed by the target server |
108
+ | `tools/describe <name>` | Show a tool's full input schema |
109
+ | `tools/call <name> <json> [--timeout <ms>]` | Call a tool with JSON arguments |
110
+ | `status` | Show target server status (PID, uptime, connection) |
111
+ | `help` | Show available commands |
112
+ | `exit` / `quit` | Disconnect and exit |
113
+
114
+ ### Examples
115
+
116
+ ```bash
117
+ # List available tools
118
+ > tools/list
119
+
120
+ # Inspect a tool's schema
121
+ > tools/describe screenshot
122
+
123
+ # Call a tool with arguments
124
+ > tools/call screenshot {"target": "#loginBtn"}
125
+
126
+ # Call with a custom timeout (5 seconds)
127
+ > tools/call long_running_tool {} --timeout 5000
128
+
129
+ # Arguments with spaces work fine
130
+ > tools/call send_message {"text": "hello world", "channel": "general"}
131
+ ```
132
+
133
+ ### Script Mode
134
+
135
+ You can automate REPL commands by writing them to a file:
136
+
137
+ ```bash
138
+ # commands.txt
139
+ tools/list
140
+ tools/call get_status {}
141
+ tools/call screenshot {"save_path": "/tmp/test.png"}
142
+ ```
143
+
144
+ ```bash
145
+ run-mcp repl node my-server.js --script commands.txt
146
+ ```
147
+
148
+ - Lines starting with `#` are treated as comments
149
+ - Exits with code `0` on success, `1` on first error
150
+
151
+ ## Proxy Mode — How It Works
152
+
153
+ In proxy mode, `run-mcp` acts as an MCP server itself. Configure it as the command your AI agent spawns:
154
+
155
+ ```json
156
+ {
157
+ "mcpServers": {
158
+ "my-server": {
159
+ "command": "run-mcp",
160
+ "args": ["proxy", "node", "path/to/actual-server.js", "--out-dir", "./images"]
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### What the proxy forwards
167
+
168
+ The proxy dynamically mirrors the target server's capabilities. All MCP primitives that the target supports are forwarded transparently:
169
+
170
+ | Primitive | Forwarded? |
171
+ |-----------|------------|
172
+ | **Tools** (`tools/list`, `tools/call`) | ✅ Always (with interception) |
173
+ | **Resources** (`resources/list`, `resources/read`, `resources/templates/list`) | ✅ If target supports |
174
+ | **Prompts** (`prompts/list`, `prompts/get`) | ✅ If target supports |
175
+ | **Logging** (`logging/setLevel`) | ✅ If target supports |
176
+ | **Completion** (`completion/complete`) | ✅ If target supports |
177
+ | **Notifications** (list changes, logging) | ✅ Forwarded from target to agent |
178
+ | **Tool annotations** (`readOnlyHint`, `destructiveHint`, etc.) | ✅ Preserved as-is |
179
+ | **Pagination** (`nextCursor` / `cursor`) | ✅ Passed through |
180
+
181
+ ### What the proxy intercepts
182
+
183
+ Tool call responses are processed through the interceptor pipeline. All other primitives pass through untouched.
184
+
185
+ | Feature | Behavior |
186
+ |---------|----------|
187
+ | **Image extraction** | `type: "image"` responses with base64 data are saved to disk. Replaced with `[Image saved to /path/to/img.png (24KB)]` |
188
+ | **Audio extraction** | `type: "audio"` responses with base64 data are saved to disk. Replaced with `[Audio saved to /path/to/audio.wav (12KB)]` |
189
+ | **Base64 detection** | Text responses that are entirely base64-encoded (1000+ chars) are also saved as images |
190
+ | **Timeouts** | Tool calls are wrapped in a configurable timeout (default 60s, use `--timeout` to change) |
191
+ | **Truncation** | Text responses exceeding the limit (default 50K chars, use `--max-text` to change) are truncated |
192
+
193
+ ## Architecture
194
+
195
+ ```
196
+ ┌─────────────────────┐ ┌─────────────────────┐
197
+ │ │ stdio │ │
198
+ │ AI Agent / REPL │◄───────►│ run-mcp │
199
+ │ │ │ │
200
+ └─────────────────────┘ │ ┌───────────────┐ │
201
+ │ │ Interceptor │ │
202
+ │ │ • Timeouts │ │
203
+ │ │ • Image Save │ │
204
+ │ │ • Truncation │ │
205
+ │ └───────┬───────┘ │
206
+ │ │ │
207
+ │ ┌───────▼───────┐ │
208
+ │ │ TargetManager │ │
209
+ │ │ (MCP Client) │ │
210
+ │ └───────┬───────┘ │
211
+ └──────────┼──────────┘
212
+ │ stdio
213
+ ┌──────────▼──────────┐
214
+ │ Target MCP Server │
215
+ │ (child process) │
216
+ └─────────────────────┘
217
+ ```
218
+
219
+ ### Modules
220
+
221
+ | Module | File | Responsibility |
222
+ |--------|------|----------------|
223
+ | **TargetManager** | `src/target-manager.ts` | Spawns the target MCP server, manages the MCP Client connection, forwards all MCP primitives (tools, resources, prompts, logging), captures stderr, tracks lifecycle |
224
+ | **ResponseInterceptor** | `src/interceptor.ts` | Wraps tool calls with timeouts, extracts base64 images and audio to disk, truncates oversized text |
225
+ | **REPLMode** | `src/repl.ts` | Interactive readline REPL with shorthand command parsing and script mode |
226
+ | **ProxyMode** | `src/proxy.ts` | MCP Server that transparently forwards all MCP primitives to the target, with tool responses running through the interceptor |
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ # Install dependencies
232
+ npm install
233
+
234
+ # Build (one-time)
235
+ npm run build
236
+
237
+ # Watch mode (rebuild on changes)
238
+ npm run dev
239
+
240
+ # Run directly
241
+ node dist/index.js repl <target_command...>
242
+ ```
243
+
244
+ ## License
245
+
246
+ ISC
package/dist/index.js ADDED
@@ -0,0 +1,1038 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
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";
26
+
27
+ // src/interceptor.ts
28
+ import { mkdir, writeFile } from "fs/promises";
29
+ import { tmpdir } from "os";
30
+ import { join } from "path";
31
+ var BASE64_PATTERN = /^[A-Za-z0-9+/]{1000,}={0,2}$/;
32
+ var DEFAULT_TIMEOUT_MS = 6e4;
33
+ var DEFAULT_MAX_TEXT_LENGTH = 5e4;
34
+ var ResponseInterceptor = class {
35
+ outDir;
36
+ defaultTimeoutMs;
37
+ maxTextLength;
38
+ fileCounter = 0;
39
+ constructor(opts = {}) {
40
+ this.outDir = opts.outDir ?? join(tmpdir(), "run-mcp");
41
+ this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
42
+ this.maxTextLength = opts.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH;
43
+ }
44
+ /**
45
+ * Call a tool on the target, applying timeout, media extraction, and truncation.
46
+ *
47
+ * Returns the full result object as-is (including structuredContent, isError, _meta)
48
+ * with only the content array items modified when interception is needed.
49
+ */
50
+ async callTool(target, name, args = {}, timeoutMs) {
51
+ const timeout = timeoutMs ?? this.defaultTimeoutMs;
52
+ const result = await Promise.race([target.callTool(name, args), this._timeout(timeout, name)]);
53
+ const content = result.content;
54
+ if (Array.isArray(content)) {
55
+ for (let i = 0; i < content.length; i++) {
56
+ content[i] = await this._processItem(content[i]);
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ /**
62
+ * Process a single content item — extract media, truncate text.
63
+ * Preserves all item properties not related to the intercepted data
64
+ * (e.g., annotations, _meta).
65
+ */
66
+ async _processItem(item) {
67
+ if (item.type === "image" && item.data) {
68
+ return this._saveMedia(item.data, item.mimeType ?? "image/png", "image");
69
+ }
70
+ if (item.type === "audio" && item.data) {
71
+ return this._saveMedia(item.data, item.mimeType ?? "audio/wav", "audio");
72
+ }
73
+ if (item.type === "text" && item.text && BASE64_PATTERN.test(item.text.trim())) {
74
+ return this._saveMedia(item.text.trim(), "image/png", "image");
75
+ }
76
+ if (item.type === "text" && item.text && item.text.length > this.maxTextLength) {
77
+ const totalLength = item.text.length;
78
+ return {
79
+ ...item,
80
+ text: item.text.slice(0, this.maxTextLength) + `
81
+ ... (truncated, ${totalLength.toLocaleString()} chars total)`
82
+ };
83
+ }
84
+ return item;
85
+ }
86
+ /**
87
+ * Decode base64, write to disk, return a text item with the file path.
88
+ * Works for both images and audio.
89
+ */
90
+ async _saveMedia(base64Data, mimeType, mediaType) {
91
+ await mkdir(this.outDir, { recursive: true });
92
+ const ext = this._extensionFromMime(mimeType);
93
+ const timestamp = Date.now();
94
+ const counter = this.fileCounter++;
95
+ const prefix = mediaType === "audio" ? "audio" : "img";
96
+ const filename = `${prefix}_${timestamp}_${counter}${ext}`;
97
+ const filepath = join(this.outDir, filename);
98
+ const buffer = Buffer.from(base64Data, "base64");
99
+ await writeFile(filepath, buffer);
100
+ const sizeKB = (buffer.length / 1024).toFixed(1);
101
+ const label = mediaType === "audio" ? "Audio" : "Image";
102
+ return {
103
+ type: "text",
104
+ text: `[${label} saved to ${filepath} (${sizeKB}KB)]`
105
+ };
106
+ }
107
+ /**
108
+ * Returns a promise that rejects after the given timeout.
109
+ */
110
+ _timeout(ms, toolName) {
111
+ return new Promise((_, reject) => {
112
+ setTimeout(() => {
113
+ const humanMs = ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
114
+ reject(
115
+ new Error(
116
+ `Tool "${toolName}" timed out after ${ms}ms (${humanMs}). Use --timeout <ms> to increase the limit.`
117
+ )
118
+ );
119
+ }, ms);
120
+ });
121
+ }
122
+ /**
123
+ * Map MIME type to file extension.
124
+ * Covers image and audio types.
125
+ */
126
+ _extensionFromMime(mimeType) {
127
+ const map = {
128
+ // Images
129
+ "image/png": ".png",
130
+ "image/jpeg": ".jpg",
131
+ "image/gif": ".gif",
132
+ "image/webp": ".webp",
133
+ "image/svg+xml": ".svg",
134
+ "image/bmp": ".bmp",
135
+ // Audio
136
+ "audio/wav": ".wav",
137
+ "audio/mpeg": ".mp3",
138
+ "audio/mp3": ".mp3",
139
+ "audio/ogg": ".ogg",
140
+ "audio/flac": ".flac",
141
+ "audio/aac": ".aac",
142
+ "audio/webm": ".webm",
143
+ "audio/mp4": ".m4a"
144
+ };
145
+ return map[mimeType] ?? (mimeType.startsWith("audio/") ? ".wav" : ".png");
146
+ }
147
+ };
148
+
149
+ // src/target-manager.ts
150
+ import { EventEmitter } from "events";
151
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
152
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
153
+ var MIN_UPTIME_FOR_RESTART_MS = 5e3;
154
+ var MAX_RECONNECT_ATTEMPTS = 3;
155
+ var STABLE_CONNECTION_RESET_MS = 6e4;
156
+ var TargetManager = class _TargetManager extends EventEmitter {
157
+ constructor(command, args) {
158
+ super();
159
+ this.command = command;
160
+ this.args = args;
161
+ }
162
+ client = null;
163
+ transport = null;
164
+ startTime = 0;
165
+ childPid = null;
166
+ _connected = false;
167
+ // Enhanced status tracking
168
+ _lastResponseTime = null;
169
+ _stderrLineCount = 0;
170
+ // Auto-reconnect state
171
+ _reconnectAttempts = 0;
172
+ _stableTimer = null;
173
+ _autoReconnect = false;
174
+ _reconnecting = false;
175
+ _intentionalClose = false;
176
+ /**
177
+ * Enable auto-reconnect behavior.
178
+ * Only applies to interactive REPL mode — proxy mode manages its own lifecycle.
179
+ */
180
+ enableAutoReconnect() {
181
+ this._autoReconnect = true;
182
+ }
183
+ /**
184
+ * Spawn the target MCP server and establish the MCP client connection.
185
+ * Stderr from the child process is emitted as 'stderr' events.
186
+ */
187
+ async connect() {
188
+ this.transport = new StdioClientTransport({
189
+ command: this.command,
190
+ args: this.args,
191
+ stderr: "pipe"
192
+ });
193
+ this.transport.stderr?.on("data", (chunk) => {
194
+ const text = chunk.toString().trimEnd();
195
+ if (text) {
196
+ this._stderrLineCount += text.split("\n").length;
197
+ this.emit("stderr", text);
198
+ }
199
+ });
200
+ this.client = new Client({ name: "run-mcp", version: "1.2.0" }, { capabilities: {} });
201
+ this.client.onclose = () => {
202
+ this._connected = false;
203
+ this._clearStableTimer();
204
+ if (this._intentionalClose) {
205
+ return;
206
+ }
207
+ this.emit("disconnected");
208
+ this._maybeReconnect();
209
+ };
210
+ await this.client.connect(this.transport);
211
+ this._connected = true;
212
+ this.startTime = Date.now();
213
+ const proc = this.transport._process;
214
+ if (proc?.pid) {
215
+ this.childPid = proc.pid;
216
+ }
217
+ this.emit("connected");
218
+ this._registerCleanup();
219
+ this._startStableTimer();
220
+ }
221
+ get connected() {
222
+ return this._connected;
223
+ }
224
+ /**
225
+ * Record that a response was received (for status tracking).
226
+ */
227
+ recordResponse() {
228
+ this._lastResponseTime = Date.now();
229
+ }
230
+ // ─── Server introspection ───────────────────────────────────────────────────
231
+ /**
232
+ * Returns the target server's advertised capabilities.
233
+ * Available after connect() completes.
234
+ */
235
+ getServerCapabilities() {
236
+ return this.client?.getServerCapabilities();
237
+ }
238
+ /**
239
+ * Returns the target server's instructions string (if any).
240
+ * Agents may use this for system prompts or behavioral hints.
241
+ */
242
+ getInstructions() {
243
+ return this.client?.getInstructions();
244
+ }
245
+ // ─── Tools ──────────────────────────────────────────────────────────────────
246
+ /**
247
+ * List all tools exposed by the target MCP server.
248
+ * Supports cursor-based pagination via params.
249
+ */
250
+ async listTools(params) {
251
+ this._assertConnected();
252
+ const result = await this.client.listTools(params);
253
+ this.recordResponse();
254
+ return result;
255
+ }
256
+ /**
257
+ * Call a tool on the target MCP server.
258
+ */
259
+ async callTool(name, args = {}) {
260
+ this._assertConnected();
261
+ const result = await this.client.callTool({ name, arguments: args });
262
+ this.recordResponse();
263
+ return result;
264
+ }
265
+ // ─── Resources ──────────────────────────────────────────────────────────────
266
+ /**
267
+ * List resources exposed by the target MCP server.
268
+ * Supports cursor-based pagination.
269
+ */
270
+ async listResources(params) {
271
+ this._assertConnected();
272
+ const result = await this.client.listResources(params);
273
+ this.recordResponse();
274
+ return result;
275
+ }
276
+ /**
277
+ * List resource templates exposed by the target MCP server.
278
+ * Supports cursor-based pagination.
279
+ */
280
+ async listResourceTemplates(params) {
281
+ this._assertConnected();
282
+ const result = await this.client.listResourceTemplates(params);
283
+ this.recordResponse();
284
+ return result;
285
+ }
286
+ /**
287
+ * Read a specific resource by URI from the target MCP server.
288
+ */
289
+ async readResource(params) {
290
+ this._assertConnected();
291
+ const result = await this.client.readResource(params);
292
+ this.recordResponse();
293
+ return result;
294
+ }
295
+ /**
296
+ * Subscribe to resource updates on the target MCP server.
297
+ */
298
+ async subscribeResource(params) {
299
+ this._assertConnected();
300
+ const result = await this.client.subscribeResource(params);
301
+ this.recordResponse();
302
+ return result;
303
+ }
304
+ /**
305
+ * Unsubscribe from resource updates on the target MCP server.
306
+ */
307
+ async unsubscribeResource(params) {
308
+ this._assertConnected();
309
+ const result = await this.client.unsubscribeResource(params);
310
+ this.recordResponse();
311
+ return result;
312
+ }
313
+ // ─── Prompts ────────────────────────────────────────────────────────────────
314
+ /**
315
+ * List prompts exposed by the target MCP server.
316
+ * Supports cursor-based pagination.
317
+ */
318
+ async listPrompts(params) {
319
+ this._assertConnected();
320
+ const result = await this.client.listPrompts(params);
321
+ this.recordResponse();
322
+ return result;
323
+ }
324
+ /**
325
+ * Get a specific prompt by name from the target MCP server.
326
+ */
327
+ async getPrompt(params) {
328
+ this._assertConnected();
329
+ const result = await this.client.getPrompt(params);
330
+ this.recordResponse();
331
+ return result;
332
+ }
333
+ // ─── Logging ────────────────────────────────────────────────────────────────
334
+ /**
335
+ * Set the logging level on the target MCP server.
336
+ */
337
+ async setLoggingLevel(level) {
338
+ this._assertConnected();
339
+ const result = await this.client.setLoggingLevel(level);
340
+ this.recordResponse();
341
+ return result;
342
+ }
343
+ // ─── Completion ─────────────────────────────────────────────────────────────
344
+ /**
345
+ * Request completion from the target MCP server (for autocomplete UX).
346
+ */
347
+ async complete(params) {
348
+ this._assertConnected();
349
+ const result = await this.client.complete(params);
350
+ this.recordResponse();
351
+ return result;
352
+ }
353
+ // ─── Notification forwarding ────────────────────────────────────────────────
354
+ /**
355
+ * Access the underlying MCP client for advanced use cases like
356
+ * subscribing to notifications with proper SDK schemas.
357
+ * Prefer the typed methods above when possible.
358
+ */
359
+ getRawClient() {
360
+ return this.client;
361
+ }
362
+ // ─── Status & lifecycle ─────────────────────────────────────────────────────
363
+ /**
364
+ * Returns current connection status, PID, uptime, and diagnostics.
365
+ */
366
+ getStatus() {
367
+ return {
368
+ pid: this.childPid,
369
+ uptime: this._connected ? (Date.now() - this.startTime) / 1e3 : 0,
370
+ connected: this._connected,
371
+ command: this.command,
372
+ args: this.args,
373
+ lastResponseTime: this._lastResponseTime,
374
+ stderrLineCount: this._stderrLineCount,
375
+ reconnectAttempts: this._reconnectAttempts,
376
+ maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS
377
+ };
378
+ }
379
+ /**
380
+ * Cleanly shut down the client connection and child process.
381
+ */
382
+ async close() {
383
+ this._intentionalClose = true;
384
+ this._clearStableTimer();
385
+ if (this.client) {
386
+ try {
387
+ await this.client.close();
388
+ } catch {
389
+ }
390
+ this.client = null;
391
+ }
392
+ if (this.transport) {
393
+ try {
394
+ await this.transport.close();
395
+ } catch {
396
+ }
397
+ this.transport = null;
398
+ }
399
+ this._connected = false;
400
+ this.childPid = null;
401
+ }
402
+ // ─── Auto-reconnect logic ──────────────────────────────────────────────────
403
+ /**
404
+ * Decide whether to attempt auto-reconnect after a disconnect.
405
+ *
406
+ * Rules:
407
+ * 1. Auto-reconnect must be enabled
408
+ * 2. Server must have been alive for ≥5s (otherwise it's a startup bug)
409
+ * 3. Must not exceed MAX_RECONNECT_ATTEMPTS consecutive retries
410
+ * 4. Must not already be reconnecting
411
+ */
412
+ async _maybeReconnect() {
413
+ if (!this._autoReconnect || this._reconnecting) return;
414
+ const uptimeMs = Date.now() - this.startTime;
415
+ if (uptimeMs < MIN_UPTIME_FOR_RESTART_MS) {
416
+ this.emit("reconnect_failed", {
417
+ reason: "startup_crash",
418
+ message: `Server crashed after ${(uptimeMs / 1e3).toFixed(1)}s \u2014 too soon to be a transient failure (min ${MIN_UPTIME_FOR_RESTART_MS / 1e3}s). Not retrying.`
419
+ });
420
+ return;
421
+ }
422
+ if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
423
+ this.emit("reconnect_failed", {
424
+ reason: "max_retries",
425
+ message: `Server has crashed ${this._reconnectAttempts} times in a row. Giving up.`
426
+ });
427
+ return;
428
+ }
429
+ this._reconnecting = true;
430
+ this._reconnectAttempts++;
431
+ this.emit("reconnecting", {
432
+ attempt: this._reconnectAttempts,
433
+ maxAttempts: MAX_RECONNECT_ATTEMPTS
434
+ });
435
+ this.client = null;
436
+ this.transport = null;
437
+ this.childPid = null;
438
+ try {
439
+ await this.connect();
440
+ this.emit("reconnected", { attempt: this._reconnectAttempts });
441
+ } catch (err) {
442
+ this.emit("reconnect_failed", {
443
+ reason: "connect_error",
444
+ message: `Reconnect attempt ${this._reconnectAttempts} failed: ${err.message}`
445
+ });
446
+ } finally {
447
+ this._reconnecting = false;
448
+ }
449
+ }
450
+ /**
451
+ * After STABLE_CONNECTION_RESET_MS of being connected, reset the retry counter.
452
+ * This way, a server that crashes once after 10 minutes of stability
453
+ * gets a fresh set of retries.
454
+ */
455
+ _startStableTimer() {
456
+ this._clearStableTimer();
457
+ this._stableTimer = setTimeout(() => {
458
+ if (this._connected) {
459
+ this._reconnectAttempts = 0;
460
+ }
461
+ }, STABLE_CONNECTION_RESET_MS);
462
+ }
463
+ _clearStableTimer() {
464
+ if (this._stableTimer) {
465
+ clearTimeout(this._stableTimer);
466
+ this._stableTimer = null;
467
+ }
468
+ }
469
+ // ─── Internal helpers ──────────────────────────────────────────────────────
470
+ _assertConnected() {
471
+ if (!this._connected || !this.client) {
472
+ throw new Error("Not connected to target MCP server");
473
+ }
474
+ }
475
+ static _cleanupRegistered = false;
476
+ static _instances = /* @__PURE__ */ new Set();
477
+ _registerCleanup() {
478
+ _TargetManager._instances.add(this);
479
+ if (_TargetManager._cleanupRegistered) return;
480
+ _TargetManager._cleanupRegistered = true;
481
+ const cleanupAll = () => {
482
+ for (const instance of _TargetManager._instances) {
483
+ instance.close().catch(() => {
484
+ });
485
+ }
486
+ };
487
+ process.on("exit", cleanupAll);
488
+ process.on("SIGINT", () => {
489
+ cleanupAll();
490
+ process.exit(130);
491
+ });
492
+ process.on("SIGTERM", () => {
493
+ cleanupAll();
494
+ process.exit(143);
495
+ });
496
+ }
497
+ };
498
+
499
+ // src/proxy.ts
500
+ async function startProxy(targetCommand, opts) {
501
+ const [command, ...args] = targetCommand;
502
+ const target = new TargetManager(command, args);
503
+ const interceptor = new ResponseInterceptor({
504
+ outDir: opts.outDir,
505
+ defaultTimeoutMs: opts.timeoutMs,
506
+ maxTextLength: opts.maxTextLength
507
+ });
508
+ target.on("stderr", (text) => {
509
+ process.stderr.write(`[target] ${text}
510
+ `);
511
+ });
512
+ process.stderr.write("[proxy] Connecting to target MCP server...\n");
513
+ try {
514
+ await target.connect();
515
+ } catch (err) {
516
+ process.stderr.write(`[proxy] Failed to connect to target: ${err.message}
517
+ `);
518
+ process.exit(1);
519
+ }
520
+ const status = target.getStatus();
521
+ process.stderr.write(`[proxy] Connected to target (PID: ${status.pid})
522
+ `);
523
+ const targetCaps = target.getServerCapabilities() ?? {};
524
+ const proxyCaps = {};
525
+ proxyCaps.tools = targetCaps.tools ?? {};
526
+ if (targetCaps.resources) proxyCaps.resources = targetCaps.resources;
527
+ if (targetCaps.prompts) proxyCaps.prompts = targetCaps.prompts;
528
+ if (targetCaps.logging) proxyCaps.logging = targetCaps.logging;
529
+ if (targetCaps.completions) proxyCaps.completions = targetCaps.completions;
530
+ process.stderr.write(`[proxy] Mirroring capabilities: ${Object.keys(proxyCaps).join(", ")}
531
+ `);
532
+ const instructions = target.getInstructions();
533
+ if (instructions) {
534
+ process.stderr.write(
535
+ `[proxy] Target instructions: ${instructions.slice(0, 200)}${instructions.length > 200 ? "..." : ""}
536
+ `
537
+ );
538
+ }
539
+ const mcpServer = new McpServer(
540
+ {
541
+ name: "run-mcp-proxy",
542
+ version: "1.2.0"
543
+ },
544
+ { capabilities: proxyCaps }
545
+ );
546
+ const server = mcpServer.server;
547
+ server.setRequestHandler(ListToolsRequestSchema, async (request) => {
548
+ const result = await target.listTools(request.params);
549
+ return result;
550
+ });
551
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
552
+ const { name, arguments: toolArgs } = request.params;
553
+ try {
554
+ const result = await interceptor.callTool(
555
+ target,
556
+ name,
557
+ toolArgs ?? {}
558
+ );
559
+ return result;
560
+ } catch (err) {
561
+ return {
562
+ content: [{ type: "text", text: `Error: ${err.message}` }],
563
+ isError: true
564
+ };
565
+ }
566
+ });
567
+ if (targetCaps.resources) {
568
+ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
569
+ return await target.listResources(request.params);
570
+ });
571
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => {
572
+ return await target.listResourceTemplates(request.params);
573
+ });
574
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
575
+ return await target.readResource(request.params);
576
+ });
577
+ if (targetCaps.resources.subscribe) {
578
+ server.setRequestHandler(SubscribeRequestSchema, async (request) => {
579
+ return await target.subscribeResource(request.params);
580
+ });
581
+ server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
582
+ return await target.unsubscribeResource(request.params);
583
+ });
584
+ }
585
+ }
586
+ if (targetCaps.prompts) {
587
+ server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
588
+ return await target.listPrompts(request.params);
589
+ });
590
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
591
+ return await target.getPrompt(request.params);
592
+ });
593
+ }
594
+ if (targetCaps.logging) {
595
+ server.setRequestHandler(SetLevelRequestSchema, async (request) => {
596
+ return await target.setLoggingLevel(request.params.level);
597
+ });
598
+ }
599
+ if (targetCaps.completions) {
600
+ server.setRequestHandler(CompleteRequestSchema, async (request) => {
601
+ return await target.complete(request.params);
602
+ });
603
+ }
604
+ const rawClient = target.getRawClient();
605
+ if (rawClient) {
606
+ if (targetCaps.tools && targetCaps.tools.listChanged) {
607
+ rawClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
608
+ server.notification({ method: "notifications/tools/list_changed" });
609
+ });
610
+ }
611
+ if (targetCaps.resources && targetCaps.resources.listChanged) {
612
+ rawClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
613
+ server.notification({ method: "notifications/resources/list_changed" });
614
+ });
615
+ }
616
+ if (targetCaps.prompts && targetCaps.prompts.listChanged) {
617
+ rawClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
618
+ server.notification({ method: "notifications/prompts/list_changed" });
619
+ });
620
+ }
621
+ if (targetCaps.logging) {
622
+ rawClient.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
623
+ server.notification({
624
+ method: "notifications/message",
625
+ params: notification.params
626
+ });
627
+ });
628
+ }
629
+ }
630
+ const transport = new StdioServerTransport();
631
+ server.onclose = async () => {
632
+ process.stderr.write("[proxy] Parent disconnected, shutting down...\n");
633
+ await target.close();
634
+ process.exit(0);
635
+ };
636
+ await mcpServer.connect(transport);
637
+ process.stderr.write("[proxy] Proxy server running on stdio.\n");
638
+ target.on("disconnected", () => {
639
+ process.stderr.write("[proxy] Target server disconnected.\n");
640
+ process.exit(1);
641
+ });
642
+ }
643
+
644
+ // src/repl.ts
645
+ import { readFile } from "fs/promises";
646
+ import { createInterface } from "readline";
647
+ import pc from "picocolors";
648
+
649
+ // src/parsing.ts
650
+ function parseCommandLine(input) {
651
+ const spaceIdx = input.indexOf(" ");
652
+ if (spaceIdx === -1) {
653
+ return { cmd: input.toLowerCase(), rest: "" };
654
+ }
655
+ return {
656
+ cmd: input.slice(0, spaceIdx).toLowerCase(),
657
+ rest: input.slice(spaceIdx + 1)
658
+ };
659
+ }
660
+ function parseCallArgs(rest) {
661
+ const trimmed = rest.trim();
662
+ if (!trimmed) return { toolName: "", jsonArgs: "" };
663
+ const spaceIdx = trimmed.indexOf(" ");
664
+ if (spaceIdx === -1) {
665
+ return { toolName: trimmed, jsonArgs: "" };
666
+ }
667
+ const toolName = trimmed.slice(0, spaceIdx);
668
+ let remainder = trimmed.slice(spaceIdx + 1).trim();
669
+ let timeoutMs;
670
+ const timeoutMatch = remainder.match(/\s--timeout\s+(\d+)\s*$/);
671
+ if (timeoutMatch) {
672
+ timeoutMs = parseInt(timeoutMatch[1], 10);
673
+ remainder = remainder.slice(0, timeoutMatch.index).trim();
674
+ }
675
+ return { toolName, jsonArgs: remainder, timeoutMs };
676
+ }
677
+ function formatJson(obj, indent = 2) {
678
+ const json = JSON.stringify(obj, null, indent);
679
+ return json.split("\n").map((line) => " ".repeat(indent) + line).join("\n");
680
+ }
681
+ function levenshtein(a, b) {
682
+ const m = a.length;
683
+ const n = b.length;
684
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
685
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
686
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
687
+ for (let i = 1; i <= m; i++) {
688
+ for (let j = 1; j <= n; j++) {
689
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
690
+ }
691
+ }
692
+ return dp[m][n];
693
+ }
694
+ function suggestCommand(input, commands, threshold = 0.4) {
695
+ let best = null;
696
+ let bestDist = Infinity;
697
+ for (const cmd of commands) {
698
+ const dist = levenshtein(input, cmd);
699
+ if (dist < bestDist) {
700
+ bestDist = dist;
701
+ best = cmd;
702
+ }
703
+ }
704
+ if (best && bestDist <= Math.ceil(input.length * threshold)) {
705
+ return best;
706
+ }
707
+ return null;
708
+ }
709
+
710
+ // src/repl.ts
711
+ var KNOWN_COMMANDS = [
712
+ "tools/list",
713
+ "tools/describe",
714
+ "tools/call",
715
+ "status",
716
+ "help",
717
+ "exit",
718
+ "quit"
719
+ ];
720
+ async function startRepl(targetCommand, opts) {
721
+ const [command, ...args] = targetCommand;
722
+ const target = new TargetManager(command, args);
723
+ const interceptor = new ResponseInterceptor({ outDir: opts.outDir });
724
+ target.on("stderr", (text) => {
725
+ for (const line of text.split("\n")) {
726
+ console.error(pc.dim(`[server] ${line}`));
727
+ }
728
+ });
729
+ console.log(pc.cyan("\u27F3 Connecting to target MCP server..."));
730
+ console.log(pc.dim(` Command: ${targetCommand.join(" ")}`));
731
+ try {
732
+ await target.connect();
733
+ } catch (err) {
734
+ const msg = err.message ?? String(err);
735
+ if (msg.includes("ENOENT") || msg.includes("spawn")) {
736
+ console.error(pc.red(`\u2717 Failed to start server: command "${command}" not found.`));
737
+ console.error(pc.dim(` Check that "${command}" is installed and in your PATH.`));
738
+ } else {
739
+ console.error(pc.red(`\u2717 Failed to connect: ${msg}`));
740
+ console.error(pc.dim(` Check that the target command starts a valid MCP server on stdio.`));
741
+ }
742
+ process.exit(1);
743
+ }
744
+ const status = target.getStatus();
745
+ console.log(pc.green(`\u2713 Connected (PID: ${status.pid})`));
746
+ if (!opts.script) {
747
+ target.enableAutoReconnect();
748
+ target.on(
749
+ "reconnecting",
750
+ ({ attempt, maxAttempts }) => {
751
+ console.log(
752
+ pc.yellow(`
753
+ \u27F3 Server disconnected. Reconnecting (${attempt}/${maxAttempts})...`)
754
+ );
755
+ }
756
+ );
757
+ target.on("reconnected", ({ attempt }) => {
758
+ const s = target.getStatus();
759
+ console.log(pc.green(`\u2713 Reconnected (PID: ${s.pid}, attempt ${attempt})`));
760
+ });
761
+ target.on("reconnect_failed", ({ reason, message }) => {
762
+ console.error(pc.red(`\u2717 ${message}`));
763
+ if (reason === "max_retries") {
764
+ console.log(
765
+ pc.dim(" Use 'exit' to quit or wait for the server to be fixed and restart manually.")
766
+ );
767
+ }
768
+ });
769
+ }
770
+ try {
771
+ const { tools } = await target.listTools();
772
+ console.log(
773
+ pc.cyan(` ${tools.length} tool(s) available. Type ${pc.bold("help")} for commands.
774
+ `)
775
+ );
776
+ } catch (err) {
777
+ console.log(pc.yellow(` Warning: Could not list tools: ${err.message}
778
+ `));
779
+ }
780
+ const isScript = !!opts.script;
781
+ if (isScript) {
782
+ const lines = await readScriptLines(opts.script);
783
+ for (const line of lines) {
784
+ const trimmed = line.trim();
785
+ if (!trimmed || trimmed.startsWith("#")) continue;
786
+ try {
787
+ await handleCommand(trimmed, target, interceptor);
788
+ } catch (err) {
789
+ console.error(pc.red(`\u2717 Error: ${err.message}`));
790
+ console.log(pc.dim("\nShutting down..."));
791
+ await target.close();
792
+ process.exit(1);
793
+ }
794
+ }
795
+ console.log(pc.dim("\nShutting down..."));
796
+ await target.close();
797
+ process.exit(0);
798
+ } else {
799
+ const rl = createInterface({
800
+ input: process.stdin,
801
+ output: process.stdout,
802
+ prompt: pc.cyan("> "),
803
+ terminal: true
804
+ });
805
+ rl.prompt();
806
+ let processing = false;
807
+ const queue = [];
808
+ const processQueue = async () => {
809
+ if (processing) return;
810
+ processing = true;
811
+ while (queue.length > 0) {
812
+ const trimmed = queue.shift();
813
+ try {
814
+ await handleCommand(trimmed, target, interceptor);
815
+ } catch (err) {
816
+ console.error(pc.red(`\u2717 Error: ${err.message}`));
817
+ }
818
+ rl.prompt();
819
+ }
820
+ processing = false;
821
+ };
822
+ rl.on("line", (line) => {
823
+ const trimmed = line.trim();
824
+ if (!trimmed || trimmed.startsWith("#")) {
825
+ rl.prompt();
826
+ return;
827
+ }
828
+ queue.push(trimmed);
829
+ processQueue();
830
+ });
831
+ rl.on("close", async () => {
832
+ console.log(pc.dim("\nShutting down..."));
833
+ await target.close();
834
+ process.exit(0);
835
+ });
836
+ }
837
+ }
838
+ async function handleCommand(input, target, interceptor) {
839
+ const { cmd, rest } = parseCommandLine(input);
840
+ switch (cmd) {
841
+ case "help":
842
+ printHelp();
843
+ return;
844
+ case "tools/list":
845
+ await cmdToolsList(target);
846
+ return;
847
+ case "tools/describe":
848
+ await cmdToolsDescribe(target, rest);
849
+ return;
850
+ case "tools/call":
851
+ await cmdToolsCall(target, interceptor, rest);
852
+ return;
853
+ case "status":
854
+ cmdStatus(target);
855
+ return;
856
+ case "exit":
857
+ case "quit":
858
+ process.emit("SIGINT", "SIGINT");
859
+ return;
860
+ default: {
861
+ const suggestion = suggestCommand(cmd, KNOWN_COMMANDS);
862
+ if (suggestion) {
863
+ console.log(pc.yellow(`Unknown command: ${cmd}. Did you mean ${pc.bold(suggestion)}?`));
864
+ } else {
865
+ console.log(pc.yellow(`Unknown command: ${cmd}. Type ${pc.bold("help")} for usage.`));
866
+ }
867
+ }
868
+ }
869
+ }
870
+ async function cmdToolsList(target) {
871
+ const { tools } = await target.listTools();
872
+ if (tools.length === 0) {
873
+ console.log(pc.dim(" No tools available."));
874
+ return;
875
+ }
876
+ const nameWidth = Math.max(8, ...tools.map((t) => t.name.length));
877
+ console.log(pc.bold(` ${"Name".padEnd(nameWidth)} Description`));
878
+ console.log(pc.dim(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(50)}`));
879
+ for (const tool of tools) {
880
+ const desc = tool.description ? tool.description.length > 60 ? `${tool.description.slice(0, 57)}...` : tool.description : pc.dim("(no description)");
881
+ console.log(` ${pc.green(tool.name.padEnd(nameWidth))} ${desc}`);
882
+ }
883
+ console.log(pc.dim(`
884
+ ${tools.length} tool(s) total.`));
885
+ }
886
+ async function cmdToolsDescribe(target, rest) {
887
+ const name = rest.trim();
888
+ if (!name) {
889
+ console.log(pc.yellow("Usage: tools/describe <tool_name>"));
890
+ return;
891
+ }
892
+ const { tools } = await target.listTools();
893
+ const tool = tools.find((t) => t.name === name);
894
+ if (!tool) {
895
+ console.log(pc.red(`Tool "${name}" not found.`));
896
+ console.log(pc.dim(`Available: ${tools.map((t) => t.name).join(", ")}`));
897
+ return;
898
+ }
899
+ console.log(pc.bold(`
900
+ ${tool.name}`));
901
+ if (tool.description) {
902
+ console.log(pc.dim(` ${tool.description}`));
903
+ }
904
+ console.log(pc.cyan("\n Input Schema:"));
905
+ console.log(formatJson(tool.inputSchema, 4));
906
+ }
907
+ async function cmdToolsCall(target, interceptor, rest) {
908
+ const { toolName, jsonArgs, timeoutMs } = parseCallArgs(rest);
909
+ if (!toolName) {
910
+ console.log(pc.yellow("Usage: tools/call <tool_name> [json_args] [--timeout <ms>]"));
911
+ return;
912
+ }
913
+ let args = {};
914
+ if (jsonArgs) {
915
+ try {
916
+ args = JSON.parse(jsonArgs);
917
+ } catch (err) {
918
+ console.error(pc.red(`Invalid JSON: ${err.message}`));
919
+ console.log(pc.dim(` Received: ${jsonArgs}`));
920
+ return;
921
+ }
922
+ }
923
+ console.log(pc.dim(` Calling ${toolName}...`));
924
+ const startTime = Date.now();
925
+ const result = await interceptor.callTool(target, toolName, args, timeoutMs);
926
+ const elapsed = Date.now() - startTime;
927
+ const content = result.content;
928
+ if (Array.isArray(content)) {
929
+ for (const item of content) {
930
+ if (item.type === "text") {
931
+ console.log(item.text);
932
+ } else {
933
+ console.log(formatJson(item, 2));
934
+ }
935
+ }
936
+ } else {
937
+ console.log(formatJson(result, 2));
938
+ }
939
+ console.log(pc.dim(` (${elapsed}ms)`));
940
+ }
941
+ function cmdStatus(target) {
942
+ const s = target.getStatus();
943
+ const uptimeStr = s.uptime >= 60 ? `${Math.floor(s.uptime / 60)}m ${(s.uptime % 60).toFixed(0)}s` : `${s.uptime.toFixed(1)}s`;
944
+ const lastRespStr = s.lastResponseTime ? `${((Date.now() - s.lastResponseTime) / 1e3).toFixed(1)}s ago` : "never";
945
+ console.log(pc.bold("\n Target Server Status"));
946
+ console.log(` ${pc.dim("Connected:")} ${s.connected ? pc.green("yes") : pc.red("no")}`);
947
+ console.log(` ${pc.dim("PID:")} ${s.pid ?? "N/A"}`);
948
+ console.log(` ${pc.dim("Uptime:")} ${uptimeStr}`);
949
+ console.log(` ${pc.dim("Last response:")} ${lastRespStr}`);
950
+ console.log(` ${pc.dim("Stderr lines:")} ${s.stderrLineCount.toLocaleString()}`);
951
+ console.log(` ${pc.dim("Reconnects:")} ${s.reconnectAttempts}/${s.maxReconnectAttempts}`);
952
+ console.log(` ${pc.dim("Command:")} ${s.command} ${s.args.join(" ")}`);
953
+ console.log();
954
+ }
955
+ function printHelp() {
956
+ console.log(`
957
+ ${pc.bold("Available Commands:")}
958
+
959
+ ${pc.green("tools/list")} List all available tools
960
+ ${pc.green("tools/describe")} <name> Show a tool's input schema
961
+ ${pc.green("tools/call")} <name> <json> [opts] Call a tool with JSON arguments
962
+ Options: ${pc.dim("--timeout <ms>")} Override default timeout (60s)
963
+ ${pc.green("status")} Show target server status
964
+ ${pc.green("help")} Show this help
965
+ ${pc.green("exit")} / ${pc.green("quit")} Disconnect and exit
966
+
967
+ ${pc.dim("Lines starting with # are treated as comments.")}
968
+ ${pc.dim('JSON arguments can contain spaces: tools/call say {"message": "hello world"}')}
969
+ `);
970
+ }
971
+ async function readScriptLines(filepath) {
972
+ const content = await readFile(filepath, "utf-8");
973
+ return content.split("\n");
974
+ }
975
+
976
+ // src/index.ts
977
+ 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(
980
+ "after",
981
+ `
982
+ Examples:
983
+ $ run-mcp repl node my-server.js # Interactive testing
984
+ $ 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
986
+ $ run-mcp repl npx -y some-mcp-server # Test an npx server
987
+
988
+ Run 'run-mcp <command> --help' for detailed options.`
989
+ );
990
+ if (process.argv.length <= 2) {
991
+ program.outputHelp();
992
+ process.exit(0);
993
+ }
994
+ 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(
995
+ "after",
996
+ `
997
+ Examples:
998
+ $ run-mcp repl node my-server.js
999
+ $ run-mcp repl node my-server.js --script verify.txt
1000
+ $ run-mcp repl node my-server.js --out-dir ./screenshots
1001
+
1002
+ REPL Commands (once connected):
1003
+ tools/list List all available tools
1004
+ tools/describe <name> Show a tool's input schema
1005
+ tools/call <name> <json> [opts] Call a tool with JSON arguments
1006
+ status Show target server status
1007
+ help Show all commands`
1008
+ ).action(async (targetCommand, opts) => {
1009
+ await startRepl(targetCommand, opts);
1010
+ });
1011
+ 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(
1012
+ "after",
1013
+ `
1014
+ Examples:
1015
+ $ run-mcp proxy node my-server.js
1016
+ $ run-mcp proxy node my-server.js --out-dir ./images
1017
+ $ run-mcp proxy node my-server.js --timeout 120000
1018
+ $ run-mcp proxy node my-server.js --max-text 100000
1019
+
1020
+ Use this in your MCP client configuration to wrap any MCP server:
1021
+ {
1022
+ "mcpServers": {
1023
+ "my-server": {
1024
+ "command": "run-mcp",
1025
+ "args": ["proxy", "node", "my-server.js"]
1026
+ }
1027
+ }
1028
+ }`
1029
+ ).action(
1030
+ async (targetCommand, opts) => {
1031
+ await startProxy(targetCommand, {
1032
+ outDir: opts.outDir,
1033
+ timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
1034
+ maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
1035
+ });
1036
+ }
1037
+ );
1038
+ program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "run-mcp",
3
- "version": "1.0.0",
4
- "description": "",
3
+ "version": "1.2.0",
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": {
7
7
  "url": "https://github.com/funkyfunc/run-mcp/issues"
@@ -12,9 +12,45 @@
12
12
  },
13
13
  "license": "ISC",
14
14
  "author": "",
15
- "type": "commonjs",
16
- "main": "index.js",
15
+ "type": "module",
16
+ "main": "dist/index.js",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "proxy",
24
+ "repl",
25
+ "cli"
26
+ ],
27
+ "bin": {
28
+ "run-mcp": "dist/index.js"
29
+ },
17
30
  "scripts": {
18
- "test": "echo \"Error: no test specified\" && exit 1"
31
+ "build": "tsup",
32
+ "dev": "tsup --watch",
33
+ "start": "node dist/index.js",
34
+ "typecheck": "tsc --noEmit",
35
+ "lint": "biome check",
36
+ "lint:fix": "biome check --write",
37
+ "format": "biome format --write",
38
+ "pretest": "tsup",
39
+ "test": "vitest run",
40
+ "prepublishOnly": "tsup"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.12.1",
44
+ "commander": "^13.1.0",
45
+ "picocolors": "^1.1.1",
46
+ "zod": "^3.24.3"
47
+ },
48
+ "devDependencies": {
49
+ "@biomejs/biome": "^2.4.10",
50
+ "@types/node": "^22.13.14",
51
+ "tsup": "^8.5.1",
52
+ "tsx": "^4.21.0",
53
+ "typescript": "^5.8.2",
54
+ "vitest": "^4.1.2"
19
55
  }
20
56
  }