@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @luutuankiet/mcp-proxy-shim
2
2
 
3
- **Stdio MCP shim for [mcpproxy-go](https://github.com/smart-mcp-proxy/mcpproxy-go)** — eliminates `args_json` string escaping overhead for LLM clients.
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 \\\"world\\\";\\n}\",\"new_string\":\"function hello() {\\n return \\\"universe\\\";\\n}\"}]}]}"
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 (direct mode)"
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 (retrieve_tools mode)"
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
- retrieve_tools("ynab accounts balance")
154
- [ynab__getAccounts, ynab__getTransactions, ynab__getPlans, ...]
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
- call_tool_read {
158
- name: "utils:ynab__getAccounts",
159
- args: { plan_id: "abc-123" } // native object, not escaped string
160
- }
161
- [{ name: "Checking", balance: 1500000, ... }]
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
- | `https_proxy` / `HTTPS_PROXY` | | HTTPS proxy (auto-detected via undici ProxyAgent) |
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
- // Both work:
186
- { "args": { "files": [...] } } // ← new: native object (shim serializes)
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/sandbox-cc
210
- cd sandbox-cc/mcp-shim
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 # starts the shim (connects upstream, waits for stdio)
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
- # Send MCP JSON-RPC over stdin:
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>;