@luutuankiet/mcp-proxy-shim 1.0.7 → 1.1.0-dev.1
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 +125 -24
- package/dist/core.d.ts +40 -0
- package/dist/core.js +496 -0
- package/dist/core.js.map +1 -0
- package/dist/http-server.d.ts +32 -0
- package/dist/http-server.js +238 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +10 -18
- package/dist/index.js +42 -573
- package/dist/index.js.map +1 -1
- package/dist/stdio.d.ts +6 -0
- package/dist/stdio.js +18 -0
- package/dist/stdio.js.map +1 -0
- package/package.json +8 -11
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @luutuankiet/mcp-proxy-shim
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**MCP shim for [mcpproxy-go](https://github.com/smart-mcp-proxy/mcpproxy-go)** — eliminates `args_json` string escaping overhead for LLM clients. Supports **stdio** and **HTTP Streamable** transports.
|
|
4
4
|
|
|
5
5
|
## The Problem
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ mcpproxy-go's `/mcp/call` mode uses **generic dispatcher tools** (`call_tool_rea
|
|
|
21
21
|
The LLM must escape every quote, every nested object, every bracket. For complex tool calls (file edits with match_text containing code), this becomes:
|
|
22
22
|
|
|
23
23
|
```json
|
|
24
|
-
"args_json": "{\"files\":[{\"path\":\"src/app.ts\",\"edits\":[{\"match_text\":\"function hello() {\\n return
|
|
24
|
+
"args_json": "{\"files\":[{\"path\":\"src/app.ts\",\"edits\":[{\"match_text\":\"function hello() {\\n return \\\\\"world\\\\\";\\n}\",\"new_string\":\"function hello() {\\n return \\\\\"universe\\\\\";\\n}\"}]}]}"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
This is **~400 tokens of overhead per call**, and LLMs frequently produce malformed payloads (mismatched escaping, missing backslashes).
|
|
@@ -56,11 +56,11 @@ Native JSON. No escaping. ~50 tokens. Zero malformed payloads.
|
|
|
56
56
|
```mermaid
|
|
57
57
|
sequenceDiagram
|
|
58
58
|
participant Client as MCP Client<br/>Claude Code / Cursor / etc
|
|
59
|
-
participant Shim as mcp-proxy-shim<br/>stdio
|
|
59
|
+
participant Shim as mcp-proxy-shim<br/>stdio or HTTP
|
|
60
60
|
participant Proxy as mcpproxy-go<br/>StreamableHTTP
|
|
61
61
|
|
|
62
62
|
Note over Client,Proxy: Connection Setup
|
|
63
|
-
Client->>Shim: initialize (stdio)
|
|
63
|
+
Client->>Shim: initialize (stdio or HTTP)
|
|
64
64
|
Shim->>Proxy: initialize (HTTP)
|
|
65
65
|
Proxy-->>Shim: capabilities + session ID
|
|
66
66
|
Shim-->>Client: capabilities
|
|
@@ -97,6 +97,8 @@ Only 3 tools are transformed. **Everything else passes through unchanged:**
|
|
|
97
97
|
|
|
98
98
|
## Quick Start
|
|
99
99
|
|
|
100
|
+
### Option A: Stdio (local MCP client)
|
|
101
|
+
|
|
100
102
|
Add to your `.mcp.json` — no install needed, `npx` fetches on first run:
|
|
101
103
|
|
|
102
104
|
```json
|
|
@@ -119,19 +121,95 @@ Or run directly from the CLI:
|
|
|
119
121
|
MCP_URL="https://your-proxy/mcp/?apikey=KEY" npx @luutuankiet/mcp-proxy-shim
|
|
120
122
|
```
|
|
121
123
|
|
|
124
|
+
### Option B: HTTP Streamable Server (remote agents)
|
|
125
|
+
|
|
126
|
+
Run as an HTTP server that remote MCP clients connect to over the network:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
MCP_URL="https://upstream-proxy/mcp/?apikey=KEY" \
|
|
130
|
+
MCP_APIKEY="my-secret" \
|
|
131
|
+
npx @luutuankiet/mcp-proxy-shim serve
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Then point your remote MCP client at:
|
|
135
|
+
```
|
|
136
|
+
http://localhost:3000/mcp?apikey=my-secret
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Production deployment with Docker
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# docker-compose.yml
|
|
143
|
+
services:
|
|
144
|
+
mcp-shim:
|
|
145
|
+
image: node:22-slim
|
|
146
|
+
command: npx -y @luutuankiet/mcp-proxy-shim serve
|
|
147
|
+
environment:
|
|
148
|
+
- MCP_URL=http://mcpproxy:9997/mcp/?apikey=admin
|
|
149
|
+
- MCP_PORT=3000
|
|
150
|
+
- MCP_HOST=0.0.0.0
|
|
151
|
+
- MCP_APIKEY=your-secret-key
|
|
152
|
+
ports:
|
|
153
|
+
- "3000:3000"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Put a reverse proxy (Caddy/nginx/Traefik) in front for TLS:
|
|
157
|
+
```
|
|
158
|
+
https://shim.yourdomain.com/mcp?apikey=KEY → http://localhost:3000/mcp
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### HTTP Server Architecture
|
|
162
|
+
|
|
163
|
+
```mermaid
|
|
164
|
+
sequenceDiagram
|
|
165
|
+
participant Agent as Remote Agent
|
|
166
|
+
participant Shim as mcp-proxy-shim<br/>HTTP :3000
|
|
167
|
+
participant Proxy as mcpproxy-go<br/>upstream
|
|
168
|
+
|
|
169
|
+
Note over Agent,Shim: Authentication
|
|
170
|
+
Agent->>Shim: POST /mcp?apikey=KEY
|
|
171
|
+
alt apikey invalid or missing
|
|
172
|
+
Shim-->>Agent: 401 Unauthorized
|
|
173
|
+
else apikey valid
|
|
174
|
+
Note over Shim: Create session transport
|
|
175
|
+
Shim->>Proxy: initialize shared upstream
|
|
176
|
+
Proxy-->>Shim: session ID
|
|
177
|
+
Shim-->>Agent: MCP session + Mcp-Session-Id header
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
Note over Agent,Shim: Subsequent requests
|
|
181
|
+
Agent->>Shim: POST /mcp?apikey=KEY<br/>Mcp-Session-Id: abc-123
|
|
182
|
+
Shim->>Proxy: tool call shared session
|
|
183
|
+
Proxy-->>Shim: result
|
|
184
|
+
Shim-->>Agent: result
|
|
185
|
+
|
|
186
|
+
Note over Shim: Multiple agents share<br/>one upstream connection
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Each downstream client gets its own MCP session, but all sessions **share a single upstream connection** to mcpproxy-go. This is efficient — one upstream session, many downstream clients.
|
|
190
|
+
|
|
191
|
+
**Endpoints:**
|
|
192
|
+
|
|
193
|
+
| Endpoint | Method | Auth | Description |
|
|
194
|
+
|----------|--------|------|-------------|
|
|
195
|
+
| `/mcp` | POST | `?apikey=` | MCP JSON-RPC (initialize, tool calls) |
|
|
196
|
+
| `/mcp` | GET | `?apikey=` | SSE stream reconnection |
|
|
197
|
+
| `/mcp` | DELETE | `?apikey=` | Session termination |
|
|
198
|
+
| `/health` | GET | None | Health check (session count, uptime) |
|
|
199
|
+
|
|
122
200
|
## Why Not `/mcp/all`?
|
|
123
201
|
|
|
124
202
|
mcpproxy-go exposes two routing modes:
|
|
125
203
|
|
|
126
204
|
```mermaid
|
|
127
205
|
flowchart LR
|
|
128
|
-
subgraph "/mcp/all
|
|
206
|
+
subgraph direct["/mcp/all - direct mode"]
|
|
129
207
|
A1[Client] --> B1[myserver__read_files<br/>native schema]
|
|
130
208
|
A1 --> C1[myserver__edit_files<br/>native schema]
|
|
131
209
|
A1 --> D1[github__get_user<br/>native schema]
|
|
132
210
|
end
|
|
133
211
|
|
|
134
|
-
subgraph "/mcp/call
|
|
212
|
+
subgraph call["/mcp/call - retrieve_tools mode"]
|
|
135
213
|
A2[Client] --> B2[retrieve_tools<br/>BM25 search]
|
|
136
214
|
A2 --> C2[call_tool_read<br/>generic dispatcher]
|
|
137
215
|
A2 --> D2[upstream_servers<br/>add/remove/patch]
|
|
@@ -150,41 +228,54 @@ We tested this live: added a YNAB financial tool mid-session → 43 new tools ap
|
|
|
150
228
|
# 1. User adds YNAB server to mcpproxy-go (via UI or API)
|
|
151
229
|
|
|
152
230
|
# 2. Client discovers new tools (no reconnect!)
|
|
153
|
-
|
|
154
|
-
|
|
231
|
+
# retrieve_tools("ynab accounts balance")
|
|
232
|
+
# => [ynab__getAccounts, ynab__getTransactions, ynab__getPlans, ...]
|
|
155
233
|
|
|
156
234
|
# 3. Client calls with native args (shim handles serialization)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
235
|
+
# call_tool_read {
|
|
236
|
+
# name: "utils:ynab__getAccounts",
|
|
237
|
+
# args: { plan_id: "abc-123" } // native object, not escaped string
|
|
238
|
+
# }
|
|
239
|
+
# => [{ name: "Checking", balance: 1500000, ... }]
|
|
162
240
|
```
|
|
163
241
|
|
|
164
242
|
## Configuration
|
|
165
243
|
|
|
166
|
-
| Environment variable | Default | Description |
|
|
167
|
-
|
|
168
|
-
| `MCP_URL` | **(required)** | mcpproxy-go StreamableHTTP endpoint |
|
|
169
|
-
| `
|
|
244
|
+
| Environment variable | Default | Transport | Description |
|
|
245
|
+
|---------------------|---------|-----------|-------------|
|
|
246
|
+
| `MCP_URL` | **(required)** | Both | mcpproxy-go StreamableHTTP endpoint |
|
|
247
|
+
| `MCP_PORT` | `3000` | HTTP only | Port to listen on |
|
|
248
|
+
| `MCP_HOST` | `0.0.0.0` | HTTP only | Host to bind to |
|
|
249
|
+
| `MCP_APIKEY` | — (open) | HTTP only | API key for downstream clients. When set, requests must include `?apikey=KEY`. Unset = no auth. |
|
|
250
|
+
| `https_proxy` / `HTTPS_PROXY` | — | Both | HTTPS proxy (auto-detected via undici ProxyAgent) |
|
|
170
251
|
|
|
171
252
|
## Architecture Details
|
|
172
253
|
|
|
254
|
+
### Transport Modes
|
|
255
|
+
|
|
256
|
+
| Feature | Stdio | HTTP Streamable (`serve`) |
|
|
257
|
+
|---------|-------|--------------------------|
|
|
258
|
+
| Use case | Local MCP client (Claude Code, Cursor) | Remote agents, cloud sandboxes |
|
|
259
|
+
| Connection | stdin/stdout | HTTP POST/GET/DELETE on `/mcp` |
|
|
260
|
+
| Auth | N/A (local process) | Optional `?apikey=` query param |
|
|
261
|
+
| Multi-client | Single client | Multiple concurrent sessions |
|
|
262
|
+
| Upstream sharing | One-to-one | Many-to-one (shared upstream) |
|
|
263
|
+
|
|
173
264
|
### Session Management
|
|
174
265
|
|
|
175
266
|
- Initializes upstream MCP session on startup via `initialize` + `notifications/initialized`
|
|
176
267
|
- Auto-reinitializes on session expiry (e.g., upstream restart, 405 responses)
|
|
177
268
|
- Retries transient failures with exponential backoff (1s, 2s, max 2 retries)
|
|
178
269
|
- Refreshes tool list on every `tools/list` request (upstream servers may have changed)
|
|
270
|
+
- HTTP mode: each downstream client gets its own `Mcp-Session-Id`, all sharing one upstream session
|
|
179
271
|
|
|
180
272
|
### Backward Compatibility
|
|
181
273
|
|
|
182
274
|
If a caller sends `args_json` directly (old style), the shim **passes it through unchanged**. You can migrate gradually — no breaking changes.
|
|
183
275
|
|
|
184
276
|
```json
|
|
185
|
-
//
|
|
186
|
-
{ "
|
|
187
|
-
{ "args_json": "{\"files\":[...]}" } // ← old: pre-serialized (shim passes through)
|
|
277
|
+
{ "args": { "files": [...] } } // new: native object (shim serializes)
|
|
278
|
+
{ "args_json": "{\"files\":[...]}" } // old: pre-serialized (shim passes through)
|
|
188
279
|
```
|
|
189
280
|
|
|
190
281
|
### HTTPS Proxy Support
|
|
@@ -206,19 +297,29 @@ StreamableHTTP responses may arrive as either `application/json` or `text/event-
|
|
|
206
297
|
## Development
|
|
207
298
|
|
|
208
299
|
```bash
|
|
209
|
-
git clone https://github.com/luutuankiet/
|
|
210
|
-
cd
|
|
300
|
+
git clone https://github.com/luutuankiet/mcp-proxy-shim
|
|
301
|
+
cd mcp-proxy-shim
|
|
211
302
|
npm install
|
|
212
303
|
npm run build
|
|
213
|
-
npm start
|
|
304
|
+
npm start # stdio mode (connects upstream, waits for stdio)
|
|
305
|
+
npm run start:http # HTTP serve mode
|
|
214
306
|
```
|
|
215
307
|
|
|
216
308
|
### Testing
|
|
217
309
|
|
|
218
310
|
```bash
|
|
219
|
-
#
|
|
311
|
+
# Stdio mode — send MCP JSON-RPC over stdin:
|
|
220
312
|
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \
|
|
221
313
|
| MCP_URL="https://your-proxy/mcp/?apikey=KEY" node dist/index.js
|
|
314
|
+
|
|
315
|
+
# HTTP mode — start server, then test with curl:
|
|
316
|
+
MCP_URL="https://your-proxy/mcp/?apikey=KEY" MCP_APIKEY="test" node dist/index.js serve
|
|
317
|
+
|
|
318
|
+
# In another terminal:
|
|
319
|
+
curl http://localhost:3000/health
|
|
320
|
+
curl -X POST http://localhost:3000/mcp?apikey=test \
|
|
321
|
+
-H "Content-Type: application/json" \
|
|
322
|
+
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
|
|
222
323
|
```
|
|
223
324
|
|
|
224
325
|
Logs go to stderr (stdout is the stdio transport):
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Proxy Shim — Shared Core
|
|
3
|
+
*
|
|
4
|
+
* Contains all upstream session management, schema transformation,
|
|
5
|
+
* response unwrapping, and tool handling logic shared between
|
|
6
|
+
* stdio and HTTP server entry points.
|
|
7
|
+
*/
|
|
8
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
|
+
export declare const UPSTREAM_URL: string;
|
|
10
|
+
export declare const REQUEST_TIMEOUT_MS = 120000;
|
|
11
|
+
export declare const MAX_RETRIES = 2;
|
|
12
|
+
export declare function log(...args: unknown[]): void;
|
|
13
|
+
/** Mask credentials in log output */
|
|
14
|
+
export declare function maskUrl(url: string): string;
|
|
15
|
+
export declare function ensureSession(): Promise<void>;
|
|
16
|
+
export interface ToolSchema {
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
inputSchema?: {
|
|
20
|
+
type: string;
|
|
21
|
+
properties?: Record<string, unknown>;
|
|
22
|
+
required?: string[];
|
|
23
|
+
[k: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
[k: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface ShimServerOptions {
|
|
28
|
+
/**
|
|
29
|
+
* If true, skip eager upstream initialization. Upstream session will be
|
|
30
|
+
* established lazily on first tool call (via ensureSession).
|
|
31
|
+
* Useful for HTTP server mode where we want the server to start even
|
|
32
|
+
* if upstream is temporarily unavailable.
|
|
33
|
+
*/
|
|
34
|
+
lazyInit?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create and wire up an MCP Server with all shim handlers.
|
|
38
|
+
* Caller connects their chosen transport (stdio or HTTP).
|
|
39
|
+
*/
|
|
40
|
+
export declare function createShimServer(options?: ShimServerOptions): Promise<Server>;
|