run-mcp 1.1.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.
- package/README.md +70 -10
- package/dist/index.js +726 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# run-mcp
|
|
2
2
|
|
|
3
|
-
A smart proxy
|
|
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
|
|
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
|
|
|
@@ -93,8 +94,49 @@ Options:
|
|
|
93
94
|
run-mcp proxy <target_command...> [options]
|
|
94
95
|
|
|
95
96
|
Options:
|
|
96
|
-
-o, --out-dir <path>
|
|
97
|
+
-o, --out-dir <path> Directory to save intercepted images and audio (default: $TMPDIR/run-mcp)
|
|
98
|
+
-t, --timeout <ms> Default tool call timeout in milliseconds (default: 60000)
|
|
99
|
+
--max-text <chars> Max text response length before truncation (default: 50000)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Server Command
|
|
103
|
+
|
|
97
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 |
|
|
98
140
|
|
|
99
141
|
## REPL Commands
|
|
100
142
|
|
|
@@ -161,14 +203,32 @@ In proxy mode, `run-mcp` acts as an MCP server itself. Configure it as the comma
|
|
|
161
203
|
}
|
|
162
204
|
```
|
|
163
205
|
|
|
206
|
+
### What the proxy forwards
|
|
207
|
+
|
|
208
|
+
The proxy dynamically mirrors the target server's capabilities. All MCP primitives that the target supports are forwarded transparently:
|
|
209
|
+
|
|
210
|
+
| Primitive | Forwarded? |
|
|
211
|
+
|-----------|------------|
|
|
212
|
+
| **Tools** (`tools/list`, `tools/call`) | ✅ Always (with interception) |
|
|
213
|
+
| **Resources** (`resources/list`, `resources/read`, `resources/templates/list`) | ✅ If target supports |
|
|
214
|
+
| **Prompts** (`prompts/list`, `prompts/get`) | ✅ If target supports |
|
|
215
|
+
| **Logging** (`logging/setLevel`) | ✅ If target supports |
|
|
216
|
+
| **Completion** (`completion/complete`) | ✅ If target supports |
|
|
217
|
+
| **Notifications** (list changes, logging) | ✅ Forwarded from target to agent |
|
|
218
|
+
| **Tool annotations** (`readOnlyHint`, `destructiveHint`, etc.) | ✅ Preserved as-is |
|
|
219
|
+
| **Pagination** (`nextCursor` / `cursor`) | ✅ Passed through |
|
|
220
|
+
|
|
164
221
|
### What the proxy intercepts
|
|
165
222
|
|
|
223
|
+
Tool call responses are processed through the interceptor pipeline. All other primitives pass through untouched.
|
|
224
|
+
|
|
166
225
|
| Feature | Behavior |
|
|
167
226
|
|---------|----------|
|
|
168
|
-
| **Image extraction** | `type: "image"` responses with base64 data are saved to disk.
|
|
227
|
+
| **Image extraction** | `type: "image"` responses with base64 data are saved to disk. Replaced with `[Image saved to /path/to/img.png (24KB)]` |
|
|
228
|
+
| **Audio extraction** | `type: "audio"` responses with base64 data are saved to disk. Replaced with `[Audio saved to /path/to/audio.wav (12KB)]` |
|
|
169
229
|
| **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
|
|
230
|
+
| **Timeouts** | Tool calls are wrapped in a configurable timeout (default 60s, use `--timeout` to change) |
|
|
231
|
+
| **Truncation** | Text responses exceeding the limit (default 50K chars, use `--max-text` to change) are truncated |
|
|
172
232
|
|
|
173
233
|
## Architecture
|
|
174
234
|
|
|
@@ -200,10 +260,10 @@ In proxy mode, `run-mcp` acts as an MCP server itself. Configure it as the comma
|
|
|
200
260
|
|
|
201
261
|
| Module | File | Responsibility |
|
|
202
262
|
|--------|------|----------------|
|
|
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 |
|
|
263
|
+
| **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 |
|
|
264
|
+
| **ResponseInterceptor** | `src/interceptor.ts` | Wraps tool calls with timeouts, extracts base64 images and audio to disk, truncates oversized text |
|
|
205
265
|
| **REPLMode** | `src/repl.ts` | Interactive readline REPL with shorthand command parsing and script mode |
|
|
206
|
-
| **ProxyMode** | `src/proxy.ts` | MCP Server that
|
|
266
|
+
| **ProxyMode** | `src/proxy.ts` | MCP Server that transparently forwards all MCP primitives to the target, with tool responses running through the interceptor |
|
|
207
267
|
|
|
208
268
|
## Development
|
|
209
269
|
|
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
|
|
|
@@ -127,6 +167,8 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
127
167
|
// Enhanced status tracking
|
|
128
168
|
_lastResponseTime = null;
|
|
129
169
|
_stderrLineCount = 0;
|
|
170
|
+
_stderrLines = [];
|
|
171
|
+
static MAX_STDERR_LINES = 200;
|
|
130
172
|
// Auto-reconnect state
|
|
131
173
|
_reconnectAttempts = 0;
|
|
132
174
|
_stableTimer = null;
|
|
@@ -153,11 +195,16 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
153
195
|
this.transport.stderr?.on("data", (chunk) => {
|
|
154
196
|
const text = chunk.toString().trimEnd();
|
|
155
197
|
if (text) {
|
|
156
|
-
|
|
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
|
+
}
|
|
157
204
|
this.emit("stderr", text);
|
|
158
205
|
}
|
|
159
206
|
});
|
|
160
|
-
this.client = new Client({ name: "run-mcp", version: "1.
|
|
207
|
+
this.client = new Client({ name: "run-mcp", version: "1.3.0" }, { capabilities: {} });
|
|
161
208
|
this.client.onclose = () => {
|
|
162
209
|
this._connected = false;
|
|
163
210
|
this._clearStableTimer();
|
|
@@ -187,12 +234,29 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
187
234
|
recordResponse() {
|
|
188
235
|
this._lastResponseTime = Date.now();
|
|
189
236
|
}
|
|
237
|
+
// ─── Server introspection ───────────────────────────────────────────────────
|
|
238
|
+
/**
|
|
239
|
+
* Returns the target server's advertised capabilities.
|
|
240
|
+
* Available after connect() completes.
|
|
241
|
+
*/
|
|
242
|
+
getServerCapabilities() {
|
|
243
|
+
return this.client?.getServerCapabilities();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Returns the target server's instructions string (if any).
|
|
247
|
+
* Agents may use this for system prompts or behavioral hints.
|
|
248
|
+
*/
|
|
249
|
+
getInstructions() {
|
|
250
|
+
return this.client?.getInstructions();
|
|
251
|
+
}
|
|
252
|
+
// ─── Tools ──────────────────────────────────────────────────────────────────
|
|
190
253
|
/**
|
|
191
254
|
* List all tools exposed by the target MCP server.
|
|
255
|
+
* Supports cursor-based pagination via params.
|
|
192
256
|
*/
|
|
193
|
-
async listTools() {
|
|
257
|
+
async listTools(params) {
|
|
194
258
|
this._assertConnected();
|
|
195
|
-
const result = await this.client.listTools();
|
|
259
|
+
const result = await this.client.listTools(params);
|
|
196
260
|
this.recordResponse();
|
|
197
261
|
return result;
|
|
198
262
|
}
|
|
@@ -205,6 +269,112 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
205
269
|
this.recordResponse();
|
|
206
270
|
return result;
|
|
207
271
|
}
|
|
272
|
+
// ─── Resources ──────────────────────────────────────────────────────────────
|
|
273
|
+
/**
|
|
274
|
+
* List resources exposed by the target MCP server.
|
|
275
|
+
* Supports cursor-based pagination.
|
|
276
|
+
*/
|
|
277
|
+
async listResources(params) {
|
|
278
|
+
this._assertConnected();
|
|
279
|
+
const result = await this.client.listResources(params);
|
|
280
|
+
this.recordResponse();
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* List resource templates exposed by the target MCP server.
|
|
285
|
+
* Supports cursor-based pagination.
|
|
286
|
+
*/
|
|
287
|
+
async listResourceTemplates(params) {
|
|
288
|
+
this._assertConnected();
|
|
289
|
+
const result = await this.client.listResourceTemplates(params);
|
|
290
|
+
this.recordResponse();
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Read a specific resource by URI from the target MCP server.
|
|
295
|
+
*/
|
|
296
|
+
async readResource(params) {
|
|
297
|
+
this._assertConnected();
|
|
298
|
+
const result = await this.client.readResource(params);
|
|
299
|
+
this.recordResponse();
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Subscribe to resource updates on the target MCP server.
|
|
304
|
+
*/
|
|
305
|
+
async subscribeResource(params) {
|
|
306
|
+
this._assertConnected();
|
|
307
|
+
const result = await this.client.subscribeResource(params);
|
|
308
|
+
this.recordResponse();
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Unsubscribe from resource updates on the target MCP server.
|
|
313
|
+
*/
|
|
314
|
+
async unsubscribeResource(params) {
|
|
315
|
+
this._assertConnected();
|
|
316
|
+
const result = await this.client.unsubscribeResource(params);
|
|
317
|
+
this.recordResponse();
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
// ─── Prompts ────────────────────────────────────────────────────────────────
|
|
321
|
+
/**
|
|
322
|
+
* List prompts exposed by the target MCP server.
|
|
323
|
+
* Supports cursor-based pagination.
|
|
324
|
+
*/
|
|
325
|
+
async listPrompts(params) {
|
|
326
|
+
this._assertConnected();
|
|
327
|
+
const result = await this.client.listPrompts(params);
|
|
328
|
+
this.recordResponse();
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get a specific prompt by name from the target MCP server.
|
|
333
|
+
*/
|
|
334
|
+
async getPrompt(params) {
|
|
335
|
+
this._assertConnected();
|
|
336
|
+
const result = await this.client.getPrompt(params);
|
|
337
|
+
this.recordResponse();
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
// ─── Logging ────────────────────────────────────────────────────────────────
|
|
341
|
+
/**
|
|
342
|
+
* Set the logging level on the target MCP server.
|
|
343
|
+
*/
|
|
344
|
+
async setLoggingLevel(level) {
|
|
345
|
+
this._assertConnected();
|
|
346
|
+
const result = await this.client.setLoggingLevel(level);
|
|
347
|
+
this.recordResponse();
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
// ─── Completion ─────────────────────────────────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* Request completion from the target MCP server (for autocomplete UX).
|
|
353
|
+
*/
|
|
354
|
+
async complete(params) {
|
|
355
|
+
this._assertConnected();
|
|
356
|
+
const result = await this.client.complete(params);
|
|
357
|
+
this.recordResponse();
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
// ─── Notification forwarding ────────────────────────────────────────────────
|
|
361
|
+
/**
|
|
362
|
+
* Access the underlying MCP client for advanced use cases like
|
|
363
|
+
* subscribing to notifications with proper SDK schemas.
|
|
364
|
+
* Prefer the typed methods above when possible.
|
|
365
|
+
*/
|
|
366
|
+
getRawClient() {
|
|
367
|
+
return this.client;
|
|
368
|
+
}
|
|
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
|
+
}
|
|
208
378
|
/**
|
|
209
379
|
* Returns current connection status, PID, uptime, and diagnostics.
|
|
210
380
|
*/
|
|
@@ -345,7 +515,11 @@ var TargetManager = class _TargetManager extends EventEmitter {
|
|
|
345
515
|
async function startProxy(targetCommand, opts) {
|
|
346
516
|
const [command, ...args] = targetCommand;
|
|
347
517
|
const target = new TargetManager(command, args);
|
|
348
|
-
const interceptor = new ResponseInterceptor({
|
|
518
|
+
const interceptor = new ResponseInterceptor({
|
|
519
|
+
outDir: opts.outDir,
|
|
520
|
+
defaultTimeoutMs: opts.timeoutMs,
|
|
521
|
+
maxTextLength: opts.maxTextLength
|
|
522
|
+
});
|
|
349
523
|
target.on("stderr", (text) => {
|
|
350
524
|
process.stderr.write(`[target] ${text}
|
|
351
525
|
`);
|
|
@@ -361,17 +535,33 @@ async function startProxy(targetCommand, opts) {
|
|
|
361
535
|
const status = target.getStatus();
|
|
362
536
|
process.stderr.write(`[proxy] Connected to target (PID: ${status.pid})
|
|
363
537
|
`);
|
|
538
|
+
const targetCaps = target.getServerCapabilities() ?? {};
|
|
539
|
+
const proxyCaps = {};
|
|
540
|
+
proxyCaps.tools = targetCaps.tools ?? {};
|
|
541
|
+
if (targetCaps.resources) proxyCaps.resources = targetCaps.resources;
|
|
542
|
+
if (targetCaps.prompts) proxyCaps.prompts = targetCaps.prompts;
|
|
543
|
+
if (targetCaps.logging) proxyCaps.logging = targetCaps.logging;
|
|
544
|
+
if (targetCaps.completions) proxyCaps.completions = targetCaps.completions;
|
|
545
|
+
process.stderr.write(`[proxy] Mirroring capabilities: ${Object.keys(proxyCaps).join(", ")}
|
|
546
|
+
`);
|
|
547
|
+
const instructions = target.getInstructions();
|
|
548
|
+
if (instructions) {
|
|
549
|
+
process.stderr.write(
|
|
550
|
+
`[proxy] Target instructions: ${instructions.slice(0, 200)}${instructions.length > 200 ? "..." : ""}
|
|
551
|
+
`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
364
554
|
const mcpServer = new McpServer(
|
|
365
555
|
{
|
|
366
556
|
name: "run-mcp-proxy",
|
|
367
|
-
version: "1.
|
|
557
|
+
version: "1.3.0"
|
|
368
558
|
},
|
|
369
|
-
{ capabilities:
|
|
559
|
+
{ capabilities: proxyCaps }
|
|
370
560
|
);
|
|
371
561
|
const server = mcpServer.server;
|
|
372
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
373
|
-
const result = await target.listTools();
|
|
374
|
-
return
|
|
562
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
563
|
+
const result = await target.listTools(request.params);
|
|
564
|
+
return result;
|
|
375
565
|
});
|
|
376
566
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
377
567
|
const { name, arguments: toolArgs } = request.params;
|
|
@@ -381,13 +571,7 @@ async function startProxy(targetCommand, opts) {
|
|
|
381
571
|
name,
|
|
382
572
|
toolArgs ?? {}
|
|
383
573
|
);
|
|
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 };
|
|
574
|
+
return result;
|
|
391
575
|
} catch (err) {
|
|
392
576
|
return {
|
|
393
577
|
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
@@ -395,6 +579,69 @@ async function startProxy(targetCommand, opts) {
|
|
|
395
579
|
};
|
|
396
580
|
}
|
|
397
581
|
});
|
|
582
|
+
if (targetCaps.resources) {
|
|
583
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
584
|
+
return await target.listResources(request.params);
|
|
585
|
+
});
|
|
586
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => {
|
|
587
|
+
return await target.listResourceTemplates(request.params);
|
|
588
|
+
});
|
|
589
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
590
|
+
return await target.readResource(request.params);
|
|
591
|
+
});
|
|
592
|
+
if (targetCaps.resources.subscribe) {
|
|
593
|
+
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
594
|
+
return await target.subscribeResource(request.params);
|
|
595
|
+
});
|
|
596
|
+
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
597
|
+
return await target.unsubscribeResource(request.params);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (targetCaps.prompts) {
|
|
602
|
+
server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
|
|
603
|
+
return await target.listPrompts(request.params);
|
|
604
|
+
});
|
|
605
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
606
|
+
return await target.getPrompt(request.params);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (targetCaps.logging) {
|
|
610
|
+
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
611
|
+
return await target.setLoggingLevel(request.params.level);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (targetCaps.completions) {
|
|
615
|
+
server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
616
|
+
return await target.complete(request.params);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const rawClient = target.getRawClient();
|
|
620
|
+
if (rawClient) {
|
|
621
|
+
if (targetCaps.tools && targetCaps.tools.listChanged) {
|
|
622
|
+
rawClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
623
|
+
server.notification({ method: "notifications/tools/list_changed" });
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (targetCaps.resources && targetCaps.resources.listChanged) {
|
|
627
|
+
rawClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
|
|
628
|
+
server.notification({ method: "notifications/resources/list_changed" });
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (targetCaps.prompts && targetCaps.prompts.listChanged) {
|
|
632
|
+
rawClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
|
|
633
|
+
server.notification({ method: "notifications/prompts/list_changed" });
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (targetCaps.logging) {
|
|
637
|
+
rawClient.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
|
638
|
+
server.notification({
|
|
639
|
+
method: "notifications/message",
|
|
640
|
+
params: notification.params
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
398
645
|
const transport = new StdioServerTransport();
|
|
399
646
|
server.onclose = async () => {
|
|
400
647
|
process.stderr.write("[proxy] Parent disconnected, shutting down...\n");
|
|
@@ -741,16 +988,419 @@ async function readScriptLines(filepath) {
|
|
|
741
988
|
return content.split("\n");
|
|
742
989
|
}
|
|
743
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
|
+
|
|
744
1393
|
// src/index.ts
|
|
745
1394
|
program.name("run-mcp").enablePositionalOptions().description(
|
|
746
|
-
"A smart proxy
|
|
747
|
-
).version("1.
|
|
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(
|
|
748
1397
|
"after",
|
|
749
1398
|
`
|
|
750
1399
|
Examples:
|
|
751
|
-
$ run-mcp repl node my-server.js # Interactive testing
|
|
1400
|
+
$ run-mcp repl node my-server.js # Interactive testing (human)
|
|
752
1401
|
$ run-mcp repl node my-server.js -s test.txt # Run a script
|
|
753
|
-
$ run-mcp proxy node my-server.js #
|
|
1402
|
+
$ run-mcp proxy node my-server.js # Transparent proxy (agent)
|
|
1403
|
+
$ run-mcp server # Test harness (agent)
|
|
754
1404
|
$ run-mcp repl npx -y some-mcp-server # Test an npx server
|
|
755
1405
|
|
|
756
1406
|
Run 'run-mcp <command> --help' for detailed options.`
|
|
@@ -776,12 +1426,14 @@ REPL Commands (once connected):
|
|
|
776
1426
|
).action(async (targetCommand, opts) => {
|
|
777
1427
|
await startRepl(targetCommand, opts);
|
|
778
1428
|
});
|
|
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(
|
|
1429
|
+
program.command("proxy").description("Start as a transparent MCP proxy between an AI agent and a target server").passThroughOptions().allowUnknownOption().argument("<target_command...>", "Command to spawn the target MCP server").option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option("-t, --timeout <ms>", "Default tool call timeout in milliseconds (default: 60000)").option("--max-text <chars>", "Max text response length before truncation (default: 50000)").addHelpText(
|
|
780
1430
|
"after",
|
|
781
1431
|
`
|
|
782
1432
|
Examples:
|
|
783
1433
|
$ run-mcp proxy node my-server.js
|
|
784
1434
|
$ run-mcp proxy node my-server.js --out-dir ./images
|
|
1435
|
+
$ run-mcp proxy node my-server.js --timeout 120000
|
|
1436
|
+
$ run-mcp proxy node my-server.js --max-text 100000
|
|
785
1437
|
|
|
786
1438
|
Use this in your MCP client configuration to wrap any MCP server:
|
|
787
1439
|
{
|
|
@@ -792,7 +1444,43 @@ Use this in your MCP client configuration to wrap any MCP server:
|
|
|
792
1444
|
}
|
|
793
1445
|
}
|
|
794
1446
|
}`
|
|
795
|
-
).action(
|
|
796
|
-
|
|
1447
|
+
).action(
|
|
1448
|
+
async (targetCommand, opts) => {
|
|
1449
|
+
await startProxy(targetCommand, {
|
|
1450
|
+
outDir: opts.outDir,
|
|
1451
|
+
timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
|
|
1452
|
+
maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
);
|
|
1456
|
+
program.command("server").description("Start as an MCP server that lets AI agents dynamically test local MCP servers").option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option("-t, --timeout <ms>", "Default tool call timeout in milliseconds (default: 60000)").option("--max-text <chars>", "Max text response length before truncation (default: 50000)").addHelpText(
|
|
1457
|
+
"after",
|
|
1458
|
+
`
|
|
1459
|
+
Examples:
|
|
1460
|
+
$ run-mcp server
|
|
1461
|
+
$ run-mcp server --out-dir ./test-output
|
|
1462
|
+
$ run-mcp server --timeout 120000
|
|
1463
|
+
|
|
1464
|
+
Add to your MCP client configuration:
|
|
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
|
+
});
|
|
797
1485
|
});
|
|
798
1486
|
program.parse();
|