@luutuankiet/mcp-proxy-shim
v1.7.0
Published
MCP proxy shim for mcpproxy-go — transforms call_tool_* args_json:string to native args:object. Supports stdio and HTTP Streamable transports.
Maintainers
Readme
@luutuankiet/mcp-proxy-shim
MCP shim for mcpproxy-go — eliminates args_json string escaping overhead for LLM clients. Supports stdio, HTTP Streamable, daemon, and passthru modes.
The Problem
mcpproxy-go's /mcp/call mode uses generic dispatcher tools (call_tool_read, call_tool_write, call_tool_destructive) that accept arguments as args_json: string — a pre-serialized JSON string. This is a sound design choice (one schema covers any upstream tool), but it creates real pain for LLM consumers:
Before (what the LLM must produce)
{
"name": "call_tool_read",
"arguments": {
"name": "myserver:read_files",
"args_json": "{\"files\":[{\"path\":\"src/index.ts\",\"head\":20}]}"
}
}The LLM must escape every quote, every nested object, every bracket. For complex tool calls (file edits with match_text containing code), this becomes:
"args_json": "{\"files\":[{\"path\":\"src/app.ts\",\"edits\":[{\"match_text\":\"function hello() {\\n return \\\\\"world\\\\\";\\n}\",\"new_string\":\"function hello() {\\n return \\\\\"universe\\\\\";\\n}\"}]}]}"This is ~400 tokens of overhead per call, and LLMs frequently produce malformed payloads (mismatched escaping, missing backslashes).
After (with the shim)
{
"name": "call_tool_read",
"arguments": {
"name": "myserver:read_files",
"args": {
"files": [{"path": "src/index.ts", "head": 20}]
}
}
}Native JSON. No escaping. ~50 tokens. Zero malformed payloads.
Impact at Scale
| Metric | Without shim | With shim | Savings | |--------|-------------|-----------|---------| | Tokens per call | ~400 | ~50 | 87% | | 30-call session overhead | ~12,000 tokens | ~1,500 tokens | 10,500 tokens saved | | Escaping bugs | Frequent | Zero | — | | Edit operations (worst case) | ~500 tokens | ~200 tokens | 60% |
How It Works
sequenceDiagram
participant Client as MCP Client<br/>Claude Code / Cursor / etc
participant Shim as mcp-proxy-shim<br/>stdio or HTTP
participant Proxy as mcpproxy-go<br/>StreamableHTTP
Note over Client,Proxy: Connection Setup
Client->>Shim: initialize (stdio or HTTP)
Shim->>Proxy: initialize (HTTP)
Proxy-->>Shim: capabilities + session ID
Shim-->>Client: capabilities
Note over Client,Proxy: Tool Discovery
Client->>Shim: tools/list
Shim->>Proxy: tools/list
Proxy-->>Shim: tools with args_json: string
Note over Shim: Transform 3 schemas<br/>args_json:string → args:object<br/>All others: passthrough
Shim-->>Client: tools with args: object
Note over Client,Proxy: Tool Call (the magic)
Client->>Shim: call_tool_read<br/>args: {files: [{path: "..."}]}
Note over Shim: Serialize<br/>args_json = JSON.stringify(args)
Shim->>Proxy: call_tool_read<br/>args_json: '{"files":[...]}'
Proxy-->>Shim: file content
Shim-->>Client: file content (passthrough)What Gets Transformed
Only 3 tools are transformed. Everything else passes through unchanged:
| Tool | Schema change | All other fields |
|------|--------------|-----------------|
| call_tool_read | args_json: string → args: object | Unchanged |
| call_tool_write | args_json: string → args: object | Unchanged |
| call_tool_destructive | args_json: string → args: object | Unchanged |
| retrieve_tools | — | Passthrough |
| upstream_servers | — | Passthrough |
| code_execution | — | Passthrough |
| read_cache | — | Passthrough |
| describe_tools | Shim-local | Batch-hydrate full schemas from BM25 |
| proxy_admin | Shim-local | Proxy lifecycle management (see below) |
| All others | — | Passthrough |
Quick Start
Option A: Stdio (local MCP client)
Add to your .mcp.json — no install needed, npx fetches on first run:
{
"mcpServers": {
"proxy": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@luutuankiet/mcp-proxy-shim"],
"env": {
"MCP_URL": "https://your-proxy.example.com/mcp/?apikey=YOUR_KEY"
}
}
}
}Or run directly from the CLI:
MCP_URL="https://your-proxy/mcp/?apikey=KEY" npx @luutuankiet/mcp-proxy-shimOption B: HTTP Streamable Server (remote agents)
Run as an HTTP server that remote MCP clients connect to over the network:
MCP_URL="https://upstream-proxy/mcp/?apikey=KEY" \
MCP_APIKEY="my-secret" \
npx @luutuankiet/mcp-proxy-shim serveThen point your remote MCP client at:
http://localhost:3000/mcp?apikey=my-secretProduction deployment with Docker
# docker-compose.yml
services:
mcp-shim:
image: node:22-slim
command: npx -y @luutuankiet/mcp-proxy-shim serve
environment:
- MCP_URL=http://mcpproxy:9997/mcp/?apikey=admin
- MCP_PORT=3000
- MCP_HOST=0.0.0.0
- MCP_APIKEY=your-secret-key
ports:
- "3000:3000"Put a reverse proxy (Caddy/nginx/Traefik) in front for TLS:
https://shim.yourdomain.com/mcp?apikey=KEY → http://localhost:3000/mcpHTTP Server Architecture
sequenceDiagram
participant Agent as Remote Agent
participant Shim as mcp-proxy-shim<br/>HTTP :3000
participant Proxy as mcpproxy-go<br/>upstream
Note over Agent,Shim: Authentication
Agent->>Shim: POST /mcp?apikey=KEY
alt apikey invalid or missing
Shim-->>Agent: 401 Unauthorized
else apikey valid
Note over Shim: Create session transport
Shim->>Proxy: initialize shared upstream
Proxy-->>Shim: session ID
Shim-->>Agent: MCP session + Mcp-Session-Id header
end
Note over Agent,Shim: Subsequent requests
Agent->>Shim: POST /mcp?apikey=KEY<br/>Mcp-Session-Id: abc-123
Shim->>Proxy: tool call shared session
Proxy-->>Shim: result
Shim-->>Agent: result
Note over Shim: Multiple agents share<br/>one upstream connectionEach 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.
Endpoints:
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| /mcp | POST | ?apikey= | MCP JSON-RPC (initialize, tool calls) |
| /mcp | GET | ?apikey= | SSE stream reconnection |
| /mcp | DELETE | ?apikey= | Session termination |
| /health | GET | None | Health check (session count, uptime) |
Option C: Daemon Mode (REST + MCP gateway for cloud agents)
Run as a REST gateway that connects to a single mcpproxy-go upstream and exposes both REST endpoints (for curl-based subagents) and a Streamable HTTP /mcp endpoint (for MCP clients).
Use case: Cloud agents (claude.ai/code, Codespaces, etc.) that can't spawn MCP servers on the fly, but can curl.
Cloud Agent ──curl──> daemon (:3456) ──HTTP──> mcpproxy-go (upstream)
<── clean JSON <──────────<── MCP responseMCP_URL="https://your-proxy/mcp/?apikey=KEY" npx @luutuankiet/mcp-proxy-shim daemonDaemon REST endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| /health | GET | Health check + session info |
| /retrieve_tools | POST | BM25 tool search { query } |
| /describe_tools | POST | Batch-hydrate full schemas { names: [...] } |
| /call | POST | Execute tool { method, name, args } |
| /exec | POST | Run code on upstream { code } |
| /reinit | POST | Force new upstream session |
| /mcp | POST/GET/DELETE | Streamable HTTP MCP (backward compat) |
Daemon environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| MCP_URL | (required) | mcpproxy-go StreamableHTTP endpoint |
| MCP_PORT | 3456 | Port to listen on |
| MCP_HOST | 0.0.0.0 | Host to bind to |
| MCP_APIKEY | — (open) | Require ?apikey=KEY on requests |
Option D: Passthru Mode (generic MCP→REST bridge)
Connect to any MCP server (not just mcpproxy-go) and expose its tools as clean REST endpoints. Designed for MCP server development and testing — lets LLM agents consume any MCP server via simple curl.
Use case: You're building an MCP server and want Claude (or any LLM agent) to test it mid-session without restarting.
Agent ──curl──> passthru (:3456) ──stdio/HTTP──> your MCP server
<── clean JSON <── MCP response (unwrapped)Stdio (spawn and manage the server process)
npx @luutuankiet/mcp-proxy-shim passthru -- npx tsx src/index.ts
# With extra env vars
npx @luutuankiet/mcp-proxy-shim passthru --env API_KEY=xxx -- python server.py
# With working directory
npx @luutuankiet/mcp-proxy-shim passthru --cwd ./my-server -- npm startHTTP Streamable (connect to running server)
npx @luutuankiet/mcp-proxy-shim passthru --url http://localhost:3000/mcp
# With auth headers
npx @luutuankiet/mcp-proxy-shim passthru --url http://localhost:3000/mcp --header "Authorization: Bearer xxx"SSE (legacy transport)
npx @luutuankiet/mcp-proxy-shim passthru --url http://localhost:3000/sse --transport sseConfig file
npx @luutuankiet/mcp-proxy-shim passthru --config server.jsonConfig file formats:
// Stdio
{"type": "stdio", "command": "npx", "args": ["tsx", "src/index.ts"], "env": {"API_KEY": "xxx"}}
// HTTP Streamable
{"type": "streamableHttp", "url": "http://localhost:3000/mcp", "headers": {"Authorization": "Bearer xxx"}}
// SSE
{"type": "sse", "url": "http://localhost:3000/sse", "headers": {}}Passthru REST endpoints
| Endpoint | Method | Description | Example |
|----------|--------|-------------|----------|
| /health | GET | Server status + transport info | curl localhost:3456/health |
| /tools | GET | Dehydrated tool list (name, description snippet, param summary) | curl localhost:3456/tools |
| /tools?q=keyword | GET | Search tools by name/description | curl "localhost:3456/tools?q=read" |
| /tools/:name | GET | Full tool schema with inputSchema | curl localhost:3456/tools/read_files |
| /call/:name | POST | Invoke a tool with native JSON args | curl -X POST localhost:3456/call/read_files -d '{"args": {"files": [...]}}' |
| /restart | POST | Restart upstream MCP connection | curl -X POST localhost:3456/restart |
Dehydrated tool listing
The /tools endpoint returns token-efficient tool summaries:
{
"tools": [
{
"name": "read_files",
"description": "Read the contents of multiple files simultaneously...",
"params": "files: object[]* | compact: boolean"
}
],
"count": 10,
"hint": "GET /tools/{name} for full inputSchema"
}Params use name: type format with * for required fields. Use /tools/:name to get the full inputSchema when needed.
Token economics: passthru vs mcpproxy-go path
| Metric | Via mcpproxy-go daemon | Via passthru | Savings |
|--------|----------------------|--------------|--------|
| Round-trips to test one tool | 3 (retrieve + describe + call) | 1-2 (optional list + call) | 50-66% |
| Request tokens per call | ~300 | ~50 | 83% |
| Tool name length | server_at_slash_path__tool_name | tool_name (native) | ~85% shorter |
| Response unwrapping | Automatic | Automatic | Same |
Passthru environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| MCP_PORT | 3456 | Port to listen on |
| MCP_HOST | 0.0.0.0 | Host to bind to |
Option E: --all Mode (lean passthrough for /mcp/all)
When pointed at mcpproxy-go's /mcp/all endpoint, the shim drops its BM25/dispatcher wrapping and forwards the upstream's native flat tool list verbatim.
Use case: you want every upstream tool surfaced directly with its real inputSchema (no args_json, no call_tool_* indirection, no shim-local describe_tools/proxy_admin). Trade-off: the tool list is frozen at connect time (see Why Not /mcp/all? below).
# Explicit flag
MCP_URL="https://proxy.example.com/mcp/all?apikey=KEY" npx @luutuankiet/mcp-proxy-shim --all
# Auto-detected (URL path ending in /mcp/all or /all)
MCP_URL="https://proxy.example.com/mcp/all?apikey=KEY" npx @luutuankiet/mcp-proxy-shim
# Env-var equivalent
MCP_URL="..." MCP_ALL_MODE=1 npx @luutuankiet/mcp-proxy-shimWhat changes in --all:
| Behavior | Default (/mcp/) | --all (/mcp/all) |
|---|---|---|
| Upstream schema | call_tool_* dispatchers with args_json: string | Flat <server>__<tool> with native inputSchema |
| args_json ⇄ args schema transform | Active (3 dispatcher tools) | No-op (no dispatchers exist) |
| Shim-local describe_tools | Injected | Skipped (upstream schemas already full) |
| Shim-local proxy_admin | Injected | Skipped (admin lives at /api/v1, not /mcp/all) |
| BM25 retrieve_tools over-request | Active | Skipped (every tool already visible) |
| Response-size annotation (MCP_MAX_RESULT_CHARS) | Active | Active |
| Dynamic tool discovery without reconnect | ✅ | ❌ (mcpproxy-go limitation) |
The passthru subcommand is unchanged and unrelated — that's a generic MCP→REST bridge for arbitrary upstream servers, not a mode of the mcpproxy-go shim.
⚠️ Sensitive URL handling.
MCP_URLtypically contains anapikeyquery parameter. Never paste real URLs into README examples, test fixtures, commit messages, or chat transcripts — usehttps://proxy.example.com/mcp/all?apikey=KEYas the literal placeholder. Run a pre-push sweep on the working tree forapikey=to catch leaks.
Why Not /mcp/all?
mcpproxy-go exposes two routing modes:
flowchart LR
subgraph direct["/mcp/all - direct mode"]
A1[Client] --> B1[myserver__read_files<br/>native schema]
A1 --> C1[myserver__edit_files<br/>native schema]
A1 --> D1[github__get_user<br/>native schema]
end
subgraph call["/mcp/call - retrieve_tools mode"]
A2[Client] --> B2[retrieve_tools<br/>BM25 search]
A2 --> C2[call_tool_read<br/>generic dispatcher]
A2 --> D2[upstream_servers<br/>add/remove/patch]
end/mcp/all gives each tool its native schema (no args_json), but freezes the tool list at connect time. Add a server? You must reconnect.
/mcp/call supports dynamic server management — add a YNAB server, a BigQuery connector, or a GitHub integration, and retrieve_tools discovers the new tools instantly. No reconnect.
We tested this live: added a YNAB financial tool mid-session → 43 new tools appeared immediately via retrieve_tools. The shim preserves this dynamic behavior while eliminating escaping overhead.
Real-World Example: Dynamic Tool Discovery
# 1. User adds YNAB server to mcpproxy-go (via UI or API)
# 2. Client discovers new tools (no reconnect!)
# retrieve_tools("ynab accounts balance")
# => [ynab__getAccounts, ynab__getTransactions, ynab__getPlans, ...]
# 3. Client calls with native args (shim handles serialization)
# call_tool_read {
# name: "utils:ynab__getAccounts",
# args: { plan_id: "abc-123" } // native object, not escaped string
# }
# => [{ name: "Checking", balance: 1500000, ... }]Proxy Administration (proxy_admin)
The shim exposes a proxy_admin tool that gives agents direct access to the upstream proxy's admin API — no curl, no port guessing, no run_command needed.
Operations
| Operation | Description | Example |
|-----------|-------------|--------|
| list | Show servers with health status | proxy_admin({operation: "list"}) |
| restart | Restart one upstream by name | proxy_admin({operation: "restart", server_name: "thinkpad"}) |
| reconnect | Reconnect all upstreams | proxy_admin({operation: "reconnect"}) |
| tail_log | Tail server logs for debugging | proxy_admin({operation: "tail_log", server_name: "pi", lines: 20}) |
Recursive Fleet Tree
With recursive: true, the shim walks nested shim-wrapped upstreams to build a full fleet view:
proxy_admin({operation: "list", recursive: true})
→ Mac proxy (outermost)
├─ thinkpad (60 tools) [shim-wrapped]
│ ├─ personal (9 tools)
│ ├─ joon (9 tools)
│ └─ kindle-gateway (13 tools)
├─ pi (12 tools) [shim-wrapped]
│ └─ pi_at_slash (9 tools)
└─ util (230 tools) [shim-wrapped]
├─ looker-da (62 tools)
├─ github (41 tools)
└─ ...19 serversNested Path Routing
Restart servers on nested proxies using path notation:
proxy_admin({operation: "restart", server_name: "thinkpad/dell"})This routes: outermost shim → call_tool_read → thinkpad's shim → thinkpad proxy admin API → dell restarted.
Shim-at-Every-Edge Architecture
When every proxy-to-proxy boundary has a shim, proxy_admin becomes available at every level:
graph TD
A[MCP Client] --> B[Outermost Shim<br/>proxy_admin for Mac proxy]
B --> C[Mac Proxy :9999]
C --> D[Thinkpad Shim<br/>proxy_admin for TK proxy]
C --> E[Pi Shim<br/>proxy_admin for Pi proxy]
C --> F[Util Shim<br/>proxy_admin for GCE proxy]
D --> G[Thinkpad Proxy :8888]
E --> H[Pi Proxy :8888]
F --> I[GCE Proxy :9999]Client-side deployment: shims run as stdio processes in the parent proxy's config — no changes needed on server hosts. Example:
{
"name": "thinkpad",
"type": "stdio",
"command": "npx",
"args": ["-y", "@luutuankiet/mcp-proxy-shim"],
"env": {"MCP_URL": "https://thinkpad.example.com/mcp/all/?apikey=admin"}
}How It Works
The shim derives the admin API URL from MCP_URL automatically:
http://localhost:9999/mcp/?apikey=admin→ admin API athttp://localhost:9999/api/v1/- API key extracted from
?apikey=query param (defaults toadmin)
Nested discovery uses retrieve_tools (BM25 with server attribution) to disambiguate multiple proxy_admin tools across shim-wrapped upstreams.
Response Size Annotation
Claude Code has a hardcoded 50,000 char ceiling (Vb_=50000 in the binary) for MCP tool results. Responses exceeding this are persisted to disk and replaced with a 2KB preview — forcing the agent to waste 3-5 extra calls recovering the content.
The shim solves this by annotating every proxied tool with _meta["anthropic/maxResultSizeChars"] in the tools/list response. When Claude Code sees this annotation, it raises the ceiling from 50k to up to 500k chars (IU6=500000), letting large responses flow through inline.
How it works
sequenceDiagram
participant CC as Claude Code
participant Shim as mcp-proxy-shim
participant Proxy as mcpproxy-go
CC->>Shim: tools/list
Shim->>Proxy: tools/list
Proxy-->>Shim: tools (no annotation)
Note over Shim: Inject _meta annotation<br/>on every tool
Shim-->>CC: tools with _meta:<br/>{"anthropic/maxResultSizeChars": 500000}
Note over CC: Persistence ceiling<br/>raised from 50k → 500k chars
CC->>Shim: call_tool_read (multi-file read)
Shim->>Proxy: call_tool_read
Proxy-->>Shim: 60k chars response
Shim-->>CC: 60k chars (inline ✅)
Note over CC: Without annotation:<br/>persisted to disk ❌Before vs After
| Scenario | Without annotation (v1.3.3) | With annotation (v1.3.4+) | |----------|---------------------------|---------------------------| | 4-file read (~53k chars) | Persisted to disk → 2KB preview → 5 wasted recovery calls | Inline, 1 call ✅ | | Large command output (~80k chars) | Same persistence trap | Inline up to 500k chars ✅ | | Responses under 50k chars | No change | No change |
Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| MCP_MAX_RESULT_CHARS | 500000 | Value for the anthropic/maxResultSizeChars annotation. Claude Code's max is 500,000 chars. Set to 0 to disable the annotation entirely. |
In a shim chain (e.g., Claude Code → shim → proxy → inner shim → proxy), only the outermost shim's annotation matters — Claude Code only sees tools listed by the shim it directly connects to. Inner shim annotations are harmless but overwritten.
Configuration
| Environment variable | Default | Transport | Description |
|---------------------|---------|-----------|-------------|
| MCP_URL | (required) | Both | mcpproxy-go StreamableHTTP endpoint |
| MCP_PORT | 3000 | HTTP only | Port to listen on |
| MCP_HOST | 0.0.0.0 | HTTP only | Host to bind to |
| MCP_APIKEY | — (open) | HTTP only | API key for downstream clients. When set, requests must include ?apikey=KEY. Unset = no auth. |
| MCP_MAX_RESULT_CHARS | 500000 | All | anthropic/maxResultSizeChars annotation value. Raises Claude Code's response persistence ceiling. Set 0 to disable. |
| MCP_ALL_MODE | unset | All | Set 1 to force lean passthrough (equivalent to --all CLI flag). Auto-detected when MCP_URL path ends in /mcp/all or /all. |
| https_proxy / HTTPS_PROXY | — | Both | HTTPS proxy (auto-detected via undici ProxyAgent) |
Architecture Details
Transport Modes
| Feature | Stdio | HTTP Streamable (serve) | Daemon (daemon) | Passthru (passthru) |
|---------|-------|--------------------------|-------------------|----------------------|
| Use case | Local MCP client (Claude Code, Cursor) | Remote agents, single upstream | Cloud agents, curl-based subagents | MCP server dev/testing |
| Connection | stdin/stdout | HTTP on /mcp | HTTP REST + /mcp | HTTP REST only |
| Upstream | Single (mcpproxy-go) | Single (mcpproxy-go) | Single (mcpproxy-go) | Any MCP server (stdio/HTTP/SSE) |
| Schema transforms | args_json → args | args_json → args | args_json → args | None (native schemas) |
| Auth | N/A (local process) | Optional ?apikey= | Optional ?apikey= | None (dev tool) |
| Multi-client | Single | Multiple sessions | Multiple sessions | Single |
| Response unwrapping | N/A | N/A | deepUnwrapResult | deepUnwrapResult |
Session Management
- Initializes upstream MCP session on startup via
initialize+notifications/initialized - Auto-reinitializes on session expiry (e.g., upstream restart, 405 responses)
- Retries transient failures with exponential backoff (1s, 2s, max 2 retries)
- Refreshes tool list on every
tools/listrequest (upstream servers may have changed) - HTTP mode: each downstream client gets its own
Mcp-Session-Id, all sharing one upstream session
Backward Compatibility
If a caller sends args_json directly (old style), the shim passes it through unchanged. You can migrate gradually — no breaking changes.
{ "args": { "files": [...] } } // new: native object (shim serializes)
{ "args_json": "{\"files\":[...]}" } // old: pre-serialized (shim passes through)HTTPS Proxy Support
Node.js's built-in fetch does not honor https_proxy environment variables. The shim uses undici's ProxyAgent to automatically route through HTTPS proxies when detected. This makes it work in cloud sandboxes (e.g., claude.ai/code) where HTTPS is routed through envoy sidecars.
SSE Support
StreamableHTTP responses may arrive as either application/json or text/event-stream (SSE). The shim detects the content type and handles both transparently.
SDK Bugs Worked Around
| Bug | Impact | Workaround in shim |
|-----|--------|------------|
| typescript-sdk #893 | McpServer.registerTool() breaks dynamic tool registration after client connects | Uses low-level Server class with setRequestHandler() |
| typescript-sdk #396 | StreamableHTTPClientTransport 2nd callTool times out due to broken session multiplexing | Uses plain fetch for upstream connection (no SDK client) |
| claude-code #13646 | Client ignores notifications/tools/list_changed | Refreshes tools on each tools/list request instead of relying on notifications |
Development
git clone https://github.com/luutuankiet/mcp-proxy-shim
cd mcp-proxy-shim
npm install
npm run build
npm start # stdio mode (connects upstream, waits for stdio)
npm run start:http # HTTP serve mode
node dist/index.js daemon # daemon mode (set MCP_SERVERS or MCP_CONFIG)Testing
# Stdio mode — send MCP JSON-RPC over stdin:
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \
| MCP_URL="https://your-proxy/mcp/?apikey=KEY" node dist/index.js
# HTTP mode — start server, then test with curl:
MCP_URL="https://your-proxy/mcp/?apikey=KEY" MCP_APIKEY="test" node dist/index.js serve
# In another terminal:
curl http://localhost:3000/health
curl -X POST http://localhost:3000/mcp?apikey=test \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'Logs go to stderr (stdout is the stdio transport):
[mcp-shim] Upstream: https://your-proxy.example.com/mcp/?apikey=KEY
[mcp-shim] Initializing upstream session...
[mcp-shim] Session ID: mcp-session-...
[mcp-shim] Fetched 10 upstream tools
[mcp-shim] Ready: 10 tools (3 with schema transform)
[mcp-shim] Stdio transport connected — shim is liveContributing
The ideal long-term fix is native args: object support in mcpproxy-go's /mcp/call mode. This shim is a client-side workaround until that lands. If you're a mcpproxy-go maintainer interested in this, see:
- Why args_json is a string:
internal/server/mcp.go— generic dispatchers need a static schema that accepts any upstream tool's arguments - Possible server-side fix: Accept both
args_json: stringandargs: objectin the same schema, withargstaking precedence when present
License
MIT
