mcp-reverse-proxy-client
Outbound-only reverse proxy for MCP servers.
It reads a project's mcp.json, launches each configured server as a local
subprocess, and opens one authenticated, persistent outbound WebSocket
tunnel per server to an MCP gateway (Context Forge).
No inbound port is ever opened — this is the mirror image of the usual
mcp-proxy tools, which run a local HTTP server that something else has to
reach.
Two pieces, published separately:
install.sh— a POSIX-shell bootstrap youcurl | sh. It makes surenpxanduvxexist, then hands off to the real tool viauvx.- This PyPI package — the actual reverse-proxy client, run via
uvx mcp-reverse-proxy-client ....
Quick start
curl -fsSL https://akospapp.github.io/agent-reverse-proxy/install.sh | sh -s -- \
--gateway-url https://gateway.example.com \
--token "$MY_GATEWAY_TOKEN"
or, if you already have uv installed:
uvx mcp-reverse-proxy-client --gateway-url https://gateway.example.com --token "$TOKEN"
CLI
| Flag | Env var | Required | Notes |
|---|---|---|---|
--gateway-url |
REVERSE_PROXY_GATEWAY |
yes | Same env var name Context Forge's own mcpgateway.reverse_proxy uses, so a value exported for that tool works unchanged here. |
--token |
REVERSE_PROXY_TOKEN |
yes | Same convention as above. |
--config |
— | no | Path to the MCP server config. Defaults to ./mcp.json, resolved strictly relative to the current working directory — no upward directory search. |
--reconnect-delay |
— | no | Initial reconnect backoff in seconds (default 1.0). Doubles on each attempt, capped at 60s — same shape Context Forge's client uses. |
--max-retries |
— | no | Per-server reconnect attempts before giving up, 0 = infinite (default). |
--keepalive |
— | no | Heartbeat interval in seconds (default 30). |
--log-level |
— | no | DEBUG/INFO/WARNING/ERROR/CRITICAL (default INFO). |
Any other argument is rejected with an error rather than silently ignored,
since install.sh forwards its own arguments to this tool blindly.
Config file
Two shapes are recognized in the same mcp.json:
1. Plain command entries (default)
The widely-used shape from Claude Desktop / Cursor / VS Code:
{
"mcpServers": {
"git": {
"command": "uvx",
"args": ["mcp-server-git"],
"env": { "SOME_VAR": "value" }
}
}
}
Each entry is spawned directly as command args... with env merged on top
of the current environment.
2. FastMCP-style entries
Discriminator rule (exact): if an entry contains a top-level source key,
it is treated as a FastMCP config
instead of a plain command. A config file can also be a bare FastMCP
config itself (no mcpServers wrapper) if its top level has a source key —
in that case it's treated as a single server named after its name field or
the file's stem.
{
"mcpServers": {
"my-fastmcp-server": {
"source": { "path": "server.py", "entrypoint": "mcp" },
"environment": { "dependencies": ["httpx"] },
"deployment": { "transport": "stdio" }
}
}
}
FastMCP-style entries are launched with fastmcp run <generated-config>
(via uvx), letting FastMCP itself resolve the environment/source
blocks with uv. Only stdio transport is tunneled — the wire protocol
only bridges stdin/stdout, so deployment.transport is forced to stdio
regardless of what the entry declares (a warning is logged if it declared
something else).
Protocol
Each server's tunnel is wire-compatible with Context Forge's own
mcpgateway.reverse_proxy: it connects to <gateway-url>/reverse-proxy/ws,
authenticates with Authorization: Bearer <token> and X-Session-ID, and
exchanges the same JSON envelope shape
({"type": "register" | "request" | "response" | "notification" | "heartbeat" | "unregister" | "error", "sessionId": ..., ...}).
All servers in a config run concurrently from a single invocation; one
tunnel reconnecting or dying does not affect the others. SIGINT/SIGTERM
trigger a clean shutdown: each tunnel unregisters, closes its WebSocket, and
terminates (then kills, if needed) its subprocess — no orphaned processes.
Shell installer (install.sh)
Responsibilities, in order:
- If
npxanduvxare both already onPATH, skip straight to step 4. - Else, if
nixis onPATH, usenix shell nixpkgs#nodejs nixpkgs#uvto provide whatever's missing for this invocation only — nothing is permanently installed. - Else, install natively without
sudoor touching system package state:uv/uvxvia the official installer (astral.sh/uv/install.sh).- Node/
npxby downloading the official Node.js LTS tarball for the detected OS/arch straight fromnodejs.orginto a cache directory (~/.cache/mcp-reverse-proxy-installer) and using its bundlednpxfor this run. The pinned version can be overridden with theNODE_VERSIONenv var.
- Every download works with either
curlorwget(whichever is present); the script fails loudly if neither exists. - Once both tools are available, it execs
uvx mcp-reverse-proxy-client "$@", forwarding all of the installer's own arguments unmodified.
The script is POSIX sh, idempotent, and safe to re-run.
Package / repo naming
- PyPI package:
mcp-reverse-proxy-client - Console script:
mcp-reverse-proxy-client - GitHub repo:
AkosPapp/agent-reverse-proxy - Installer hosting: GitHub Pages, deployed by CI —
https://akospapp.github.io/agent-reverse-proxy/install.sh
CI / releasing
Three workflows under .github/workflows/:
ci.yml— runs the test suite (Python 3.9 and 3.12) and shellchecksinstall.shon every push and pull request.pages.yml— on every push tomainthat touchesinstall.sh, publishes it to GitHub Pages so the install URL above always serves the latest committed script. Also runnable manually (workflow_dispatch).publish-pypi.yml— on every GitHub Release publish, builds the sdist- wheel and uploads them to PyPI with
twine, authenticating via thePYPI_API_TOKENrepo secret. It first checks that the release tag (vX.Y.Z) matches the version inpyproject.tomland fails loudly on a mismatch, so you can't accidentally publish the wrong version.
- wheel and uploads them to PyPI with
One-time setup before these run:
- GitHub Pages: repo Settings → Pages → Source → "GitHub Actions".
- PyPI token: create an API token scoped to this project on PyPI
(or an account-wide token for the very first publish, since the project
won't exist yet), then add it as a repo secret named
PYPI_API_TOKEN(Settings → Secrets and variables → Actions).
To cut a release: bump version in pyproject.toml, commit, tag as
vX.Y.Z (matching that version exactly), and publish a GitHub Release from
that tag — publish-pypi.yml does the rest.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_reverse_proxy_client-0.1.0.tar.gz.
File metadata
- Download URL: mcp_reverse_proxy_client-0.1.0.tar.gz
- Upload date:
- Size: 17.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b45cd47c2d2803c9efc61e8a4acf6ec51fd011d2348a99c8d2f873c9f83237c
|
|
| MD5 |
051d3ffc6884a24accb9582539646181
|
|
| BLAKE2b-256 |
1948aa40aea0eb5afe20a57380d819e863bf93d3d51d26ecf32dc53e555545d1
|
File details
Details for the file mcp_reverse_proxy_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_reverse_proxy_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5657a4f16fa5be6b07b493ae7db9ab35993b831db1c771fee506e9d39f7af6d
|
|
| MD5 |
7b3a54f40f7e36a20c8eab9ca29b4c64
|
|
| BLAKE2b-256 |
374bf3cf7876ee5185658dfc3f397a7f6f8682f30bc7e7aad1e74ad79cd199f6
|