run-mcp 1.1.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.
- package/README.md +27 -7
- package/dist/index.js +275 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,9 @@ Options:
|
|
|
93
93
|
run-mcp proxy <target_command...> [options]
|
|
94
94
|
|
|
95
95
|
Options:
|
|
96
|
-
-o, --out-dir <path>
|
|
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)
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
## REPL Commands
|
|
@@ -161,14 +163,32 @@ In proxy mode, `run-mcp` acts as an MCP server itself. Configure it as the comma
|
|
|
161
163
|
}
|
|
162
164
|
```
|
|
163
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
|
+
|
|
164
181
|
### What the proxy intercepts
|
|
165
182
|
|
|
183
|
+
Tool call responses are processed through the interceptor pipeline. All other primitives pass through untouched.
|
|
184
|
+
|
|
166
185
|
| Feature | Behavior |
|
|
167
186
|
|---------|----------|
|
|
168
|
-
| **Image extraction** | `type: "image"` responses with base64 data are saved to disk.
|
|
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)]` |
|
|
169
189
|
| **Base64 detection** | Text responses that are entirely base64-encoded (1000+ chars) are also saved as images |
|
|
170
|
-
| **Timeouts** | Tool calls are wrapped in a
|
|
171
|
-
| **Truncation** | Text responses exceeding
|
|
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 |
|
|
172
192
|
|
|
173
193
|
## Architecture
|
|
174
194
|
|
|
@@ -200,10 +220,10 @@ In proxy mode, `run-mcp` acts as an MCP server itself. Configure it as the comma
|
|
|
200
220
|
|
|
201
221
|
| Module | File | Responsibility |
|
|
202
222
|
|--------|------|----------------|
|
|
203
|
-
| **TargetManager** | `src/target-manager.ts` | Spawns the target MCP server, manages the MCP Client connection, captures stderr, tracks lifecycle |
|
|
204
|
-
| **ResponseInterceptor** | `src/interceptor.ts` | Wraps tool calls with timeouts, extracts base64 images to disk, truncates oversized text |
|
|
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 |
|
|
205
225
|
| **REPLMode** | `src/repl.ts` | Interactive readline REPL with shorthand command parsing and script mode |
|
|
206
|
-
| **ProxyMode** | `src/proxy.ts` | MCP Server that
|
|
226
|
+
| **ProxyMode** | `src/proxy.ts` | MCP Server that transparently forwards all MCP primitives to the target, with tool responses running through the interceptor |
|
|
207
227
|
|
|
208
228
|
## Development
|
|
209
229
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,23 @@ import { program } from "commander";
|
|
|
6
6
|
// src/proxy.ts
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
CompleteRequestSchema,
|
|
12
|
+
GetPromptRequestSchema,
|
|
13
|
+
ListPromptsRequestSchema,
|
|
14
|
+
ListResourcesRequestSchema,
|
|
15
|
+
ListResourceTemplatesRequestSchema,
|
|
16
|
+
ListToolsRequestSchema,
|
|
17
|
+
LoggingMessageNotificationSchema,
|
|
18
|
+
PromptListChangedNotificationSchema,
|
|
19
|
+
ReadResourceRequestSchema,
|
|
20
|
+
ResourceListChangedNotificationSchema,
|
|
21
|
+
SetLevelRequestSchema,
|
|
22
|
+
SubscribeRequestSchema,
|
|
23
|
+
ToolListChangedNotificationSchema,
|
|
24
|
+
UnsubscribeRequestSchema
|
|
25
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
26
|
|
|
11
27
|
// src/interceptor.ts
|
|
12
28
|
import { mkdir, writeFile } from "fs/promises";
|
|
@@ -14,17 +30,22 @@ import { tmpdir } from "os";
|
|
|
14
30
|
import { join } from "path";
|
|
15
31
|
var BASE64_PATTERN = /^[A-Za-z0-9+/]{1000,}={0,2}$/;
|
|
16
32
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
17
|
-
var
|
|
33
|
+
var DEFAULT_MAX_TEXT_LENGTH = 5e4;
|
|
18
34
|
var ResponseInterceptor = class {
|
|
19
35
|
outDir;
|
|
20
36
|
defaultTimeoutMs;
|
|
37
|
+
maxTextLength;
|
|
21
38
|
fileCounter = 0;
|
|
22
39
|
constructor(opts = {}) {
|
|
23
40
|
this.outDir = opts.outDir ?? join(tmpdir(), "run-mcp");
|
|
24
41
|
this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
42
|
+
this.maxTextLength = opts.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH;
|
|
25
43
|
}
|
|
26
44
|
/**
|
|
27
|
-
* Call a tool on the target, applying timeout,
|
|
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.
|
|
28
49
|
*/
|
|
29
50
|
async callTool(target, name, args = {}, timeoutMs) {
|
|
30
51
|
const timeout = timeoutMs ?? this.defaultTimeoutMs;
|
|
@@ -38,20 +59,25 @@ var ResponseInterceptor = class {
|
|
|
38
59
|
return result;
|
|
39
60
|
}
|
|
40
61
|
/**
|
|
41
|
-
* Process a single content item — extract
|
|
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).
|
|
42
65
|
*/
|
|
43
66
|
async _processItem(item) {
|
|
44
67
|
if (item.type === "image" && item.data) {
|
|
45
|
-
return this.
|
|
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");
|
|
46
72
|
}
|
|
47
73
|
if (item.type === "text" && item.text && BASE64_PATTERN.test(item.text.trim())) {
|
|
48
|
-
return this.
|
|
74
|
+
return this._saveMedia(item.text.trim(), "image/png", "image");
|
|
49
75
|
}
|
|
50
|
-
if (item.type === "text" && item.text && item.text.length >
|
|
76
|
+
if (item.type === "text" && item.text && item.text.length > this.maxTextLength) {
|
|
51
77
|
const totalLength = item.text.length;
|
|
52
78
|
return {
|
|
53
|
-
|
|
54
|
-
text: item.text.slice(0,
|
|
79
|
+
...item,
|
|
80
|
+
text: item.text.slice(0, this.maxTextLength) + `
|
|
55
81
|
... (truncated, ${totalLength.toLocaleString()} chars total)`
|
|
56
82
|
};
|
|
57
83
|
}
|
|
@@ -59,20 +85,23 @@ var ResponseInterceptor = class {
|
|
|
59
85
|
}
|
|
60
86
|
/**
|
|
61
87
|
* Decode base64, write to disk, return a text item with the file path.
|
|
88
|
+
* Works for both images and audio.
|
|
62
89
|
*/
|
|
63
|
-
async
|
|
90
|
+
async _saveMedia(base64Data, mimeType, mediaType) {
|
|
64
91
|
await mkdir(this.outDir, { recursive: true });
|
|
65
92
|
const ext = this._extensionFromMime(mimeType);
|
|
66
93
|
const timestamp = Date.now();
|
|
67
94
|
const counter = this.fileCounter++;
|
|
68
|
-
const
|
|
95
|
+
const prefix = mediaType === "audio" ? "audio" : "img";
|
|
96
|
+
const filename = `${prefix}_${timestamp}_${counter}${ext}`;
|
|
69
97
|
const filepath = join(this.outDir, filename);
|
|
70
98
|
const buffer = Buffer.from(base64Data, "base64");
|
|
71
99
|
await writeFile(filepath, buffer);
|
|
72
100
|
const sizeKB = (buffer.length / 1024).toFixed(1);
|
|
101
|
+
const label = mediaType === "audio" ? "Audio" : "Image";
|
|
73
102
|
return {
|
|
74
103
|
type: "text",
|
|
75
|
-
text: `[
|
|
104
|
+
text: `[${label} saved to ${filepath} (${sizeKB}KB)]`
|
|
76
105
|
};
|
|
77
106
|
}
|
|
78
107
|
/**
|
|
@@ -92,17 +121,28 @@ var ResponseInterceptor = class {
|
|
|
92
121
|
}
|
|
93
122
|
/**
|
|
94
123
|
* Map MIME type to file extension.
|
|
124
|
+
* Covers image and audio types.
|
|
95
125
|
*/
|
|
96
126
|
_extensionFromMime(mimeType) {
|
|
97
127
|
const map = {
|
|
128
|
+
// Images
|
|
98
129
|
"image/png": ".png",
|
|
99
130
|
"image/jpeg": ".jpg",
|
|
100
131
|
"image/gif": ".gif",
|
|
101
132
|
"image/webp": ".webp",
|
|
102
133
|
"image/svg+xml": ".svg",
|
|
103
|
-
"image/bmp": ".bmp"
|
|
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"
|
|
104
144
|
};
|
|
105
|
-
return map[mimeType] ?? ".png";
|
|
145
|
+
return map[mimeType] ?? (mimeType.startsWith("audio/") ? ".wav" : ".png");
|
|
106
146
|
}
|
|
107
147
|
};
|
|
108
148
|
|
|
@@ -157,7 +197,7 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
157
197
|
this.emit("stderr", text);
|
|
158
198
|
}
|
|
159
199
|
});
|
|
160
|
-
this.client = new Client({ name: "run-mcp", version: "1.
|
|
200
|
+
this.client = new Client({ name: "run-mcp", version: "1.2.0" }, { capabilities: {} });
|
|
161
201
|
this.client.onclose = () => {
|
|
162
202
|
this._connected = false;
|
|
163
203
|
this._clearStableTimer();
|
|
@@ -187,12 +227,29 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
187
227
|
recordResponse() {
|
|
188
228
|
this._lastResponseTime = Date.now();
|
|
189
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 ──────────────────────────────────────────────────────────────────
|
|
190
246
|
/**
|
|
191
247
|
* List all tools exposed by the target MCP server.
|
|
248
|
+
* Supports cursor-based pagination via params.
|
|
192
249
|
*/
|
|
193
|
-
async listTools() {
|
|
250
|
+
async listTools(params) {
|
|
194
251
|
this._assertConnected();
|
|
195
|
-
const result = await this.client.listTools();
|
|
252
|
+
const result = await this.client.listTools(params);
|
|
196
253
|
this.recordResponse();
|
|
197
254
|
return result;
|
|
198
255
|
}
|
|
@@ -205,6 +262,104 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
205
262
|
this.recordResponse();
|
|
206
263
|
return result;
|
|
207
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 ─────────────────────────────────────────────────────
|
|
208
363
|
/**
|
|
209
364
|
* Returns current connection status, PID, uptime, and diagnostics.
|
|
210
365
|
*/
|
|
@@ -345,7 +500,11 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
345
500
|
async function startProxy(targetCommand, opts) {
|
|
346
501
|
const [command, ...args] = targetCommand;
|
|
347
502
|
const target = new TargetManager(command, args);
|
|
348
|
-
const interceptor = new ResponseInterceptor({
|
|
503
|
+
const interceptor = new ResponseInterceptor({
|
|
504
|
+
outDir: opts.outDir,
|
|
505
|
+
defaultTimeoutMs: opts.timeoutMs,
|
|
506
|
+
maxTextLength: opts.maxTextLength
|
|
507
|
+
});
|
|
349
508
|
target.on("stderr", (text) => {
|
|
350
509
|
process.stderr.write(`[target] ${text}
|
|
351
510
|
`);
|
|
@@ -361,17 +520,33 @@ async function startProxy(targetCommand, opts) {
|
|
|
361
520
|
const status = target.getStatus();
|
|
362
521
|
process.stderr.write(`[proxy] Connected to target (PID: ${status.pid})
|
|
363
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
|
+
}
|
|
364
539
|
const mcpServer = new McpServer(
|
|
365
540
|
{
|
|
366
541
|
name: "run-mcp-proxy",
|
|
367
|
-
version: "1.
|
|
542
|
+
version: "1.2.0"
|
|
368
543
|
},
|
|
369
|
-
{ capabilities:
|
|
544
|
+
{ capabilities: proxyCaps }
|
|
370
545
|
);
|
|
371
546
|
const server = mcpServer.server;
|
|
372
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
373
|
-
const result = await target.listTools();
|
|
374
|
-
return
|
|
547
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
548
|
+
const result = await target.listTools(request.params);
|
|
549
|
+
return result;
|
|
375
550
|
});
|
|
376
551
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
377
552
|
const { name, arguments: toolArgs } = request.params;
|
|
@@ -381,13 +556,7 @@ async function startProxy(targetCommand, opts) {
|
|
|
381
556
|
name,
|
|
382
557
|
toolArgs ?? {}
|
|
383
558
|
);
|
|
384
|
-
|
|
385
|
-
if (item.type === "image") {
|
|
386
|
-
return { type: "image", data: item.data, mimeType: item.mimeType };
|
|
387
|
-
}
|
|
388
|
-
return { type: "text", text: String(item.text ?? "") };
|
|
389
|
-
});
|
|
390
|
-
return { content };
|
|
559
|
+
return result;
|
|
391
560
|
} catch (err) {
|
|
392
561
|
return {
|
|
393
562
|
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
@@ -395,6 +564,69 @@ async function startProxy(targetCommand, opts) {
|
|
|
395
564
|
};
|
|
396
565
|
}
|
|
397
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
|
+
}
|
|
398
630
|
const transport = new StdioServerTransport();
|
|
399
631
|
server.onclose = async () => {
|
|
400
632
|
process.stderr.write("[proxy] Parent disconnected, shutting down...\n");
|
|
@@ -744,7 +976,7 @@ async function readScriptLines(filepath) {
|
|
|
744
976
|
// src/index.ts
|
|
745
977
|
program.name("run-mcp").enablePositionalOptions().description(
|
|
746
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"
|
|
747
|
-
).version("1.
|
|
979
|
+
).version("1.2.0").addHelpText(
|
|
748
980
|
"after",
|
|
749
981
|
`
|
|
750
982
|
Examples:
|
|
@@ -776,12 +1008,14 @@ REPL Commands (once connected):
|
|
|
776
1008
|
).action(async (targetCommand, opts) => {
|
|
777
1009
|
await startRepl(targetCommand, opts);
|
|
778
1010
|
});
|
|
779
|
-
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").addHelpText(
|
|
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(
|
|
780
1012
|
"after",
|
|
781
1013
|
`
|
|
782
1014
|
Examples:
|
|
783
1015
|
$ run-mcp proxy node my-server.js
|
|
784
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
|
|
785
1019
|
|
|
786
1020
|
Use this in your MCP client configuration to wrap any MCP server:
|
|
787
1021
|
{
|
|
@@ -792,7 +1026,13 @@ Use this in your MCP client configuration to wrap any MCP server:
|
|
|
792
1026
|
}
|
|
793
1027
|
}
|
|
794
1028
|
}`
|
|
795
|
-
).action(
|
|
796
|
-
|
|
797
|
-
|
|
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
|
+
);
|
|
798
1038
|
program.parse();
|