@tournesol-tag/mcp-bridge
v0.1.5
Published
TAG MCP bridge — runs your local MCP server and proxies stdio ↔ TAG relay WebSocket so hosted TAG / engine can call your tools (J1, ADR-033).
Maintainers
Readme
@tournesol-tag/mcp-bridge
CLI that proxies a local MCP server to TAG over a reverse WebSocket relay, so a hosted TAG instance (or the TAG engine) can call tools that only exist on your laptop or inside your VPN.
hosted TAG ──HTTPS──► TAG MCP relay ──reverse WS──► tag-mcp-bridge ──stdio──► your MCP serverYou run tag-mcp-bridge locally. It dials out to the relay (so no inbound
ports), keeps the MCP child process warm, and translates relay RPC envelopes
into MCP JSON-RPC requests (tools/list, tools/call).
See ADR-033 for the full design.
Quick start (easiest path)
The fastest way to wire this up to your local MCP server:
Install the CLI (see below):
npm install -g @tournesol-tag/mcp-bridgeIn hosted TAG, open My MCP Bridge (left nav). That page shows:
- your relay URL (the
--relayvalue), - a Generate token button (the
--tokenvalue, valid 30 days), - a ready-to-paste
tag-mcp-bridge …command with both already filled in.
- your relay URL (the
Copy that command, append
--mcp-cmd "<how you start your MCP server>", and run it on the machine where your MCP server lives. Example:tag-mcp-bridge \ --relay https://tag-mcp-relay.<your-env>.azurecontainerapps.io \ --token "$TOKEN_FROM_UI" \ --mcp-cmd "python -m my_tools.server"Keep it running under
screen,pm2, orsystemdso it survives reboots. Hosted workflows that reference your tools will get a 503 while it's offline.
That's it — no inbound ports, no DNS, no public hostname for your machine. The bridge dials out to the relay. The rest of this README is reference detail.
Install
npm (recommended)
npm install -g @tournesol-tag/[email protected]
# or run ad-hoc without installing
npx @tournesol-tag/[email protected] --help0.1.2 (2026-06) adds a pre-flight binary check on
--mcp-cmdso a missing interpreter fails fast with an actionable message, plus an automaticpython→python3fallback (with a warning) for Debian/Ubuntu hosts that ship onlypython3. See thespawn … ENOENTentry under Troubleshooting.
Docker
docker pull ghcr.io/jaimetournesol/mcp-bridge:latest
docker run --rm ghcr.io/jaimetournesol/mcp-bridge --helpUsage
tag-mcp-bridge — proxy a local MCP server to TAG via reverse-WS
Usage:
tag-mcp-bridge --relay <url> --token <jwt> --mcp-cmd <cmd> [--dev-id <id>] [-- <mcp-args>]
Flags:
--relay <url> Relay base URL, e.g. https://tag-mcp-relay.example.com
Env: TAG_RELAY_URL
--token <jwt> Bridge JWT (sub=dev:<id>, scope=["bridge"])
Env: TAG_BRIDGE_TOKEN
--dev-id <id> Optional; derived from JWT subject if omitted.
--mcp-cmd <cmd> Shell-style command to spawn the MCP server.
Quoted form: --mcp-cmd "python -m tariff_tools.server"
Env: TAG_MCP_CMD
--mcp-env <k=v> Extra env var for the MCP child. Repeatable.
-- Everything after this is appended to the MCP child argv.
Reconnect: exponential backoff up to 30s. SIGINT to stop cleanly.Example
export TAG_RELAY_URL="https://tag-mcp-relay.example.com" # shown on the My MCP Bridge page
export TAG_BRIDGE_TOKEN="eyJhbGciOiJSUzI1NiIs..." # Generate token on the My MCP Bridge page
tag-mcp-bridge \
--relay "$TAG_RELAY_URL" \
--token "$TAG_BRIDGE_TOKEN" \
--mcp-cmd "python -m tariff_tools.server" \
--mcp-env "NEO4J_URL=bolt://localhost:7687" \
--mcp-env "API_KEY=local-dev"You should see:
[tag-mcp-bridge] dev=alice relay=https://tag-mcp-relay.example.com mcp="python -m tariff_tools.server "
[tag-mcp-bridge] ws connected
[tag-mcp-bridge] mcp child ready (15 tools)Then any hosted workflow whose serviceUrls.<name> resolves to your bridge
will route tool calls to your local MCP server.
Docker example
docker run --rm -i \
-e TAG_RELAY_URL="https://tag-mcp-relay.example.com" \
-e TAG_BRIDGE_TOKEN="$TAG_BRIDGE_TOKEN" \
-e TAG_MCP_CMD="python -m tariff_tools.server" \
ghcr.io/jaimetournesol/mcp-bridge:latest \
--relay "$TAG_RELAY_URL" --token "$TAG_BRIDGE_TOKEN" --mcp-cmd "$TAG_MCP_CMD"Note: the MCP child runs inside the container. If your tool needs Python
or other binaries, build a derived image with your runtime preinstalled and
keep the tag-mcp-bridge ENTRYPOINT.
How tokens are issued
The simplest path is the My MCP Bridge page in hosted TAG — click Generate token. Under the hood it calls the TAG API:
POST /api/me/relay-bridge-token
Authorization: Bearer <your-tag-session>The relay URL is likewise available from that page, or directly:
GET /api/me/relay-bridge-info
Authorization: Bearer <your-tag-session>
→ { "relayUrl": "https://tag-mcp-relay.<env>.azurecontainerapps.io" }The bridge JWT is valid 30 days, with sub: "dev:<your-id>" and
scope: ["bridge"]. The bridge decodes the sub claim to know which dev
slot to claim on the relay (no verification — the relay enforces it).
Troubleshooting
ws connect failed: 401
Token expired or wrong relay. Re-issue from POST /me/bridge/token and
re-check --relay.
ws connect failed: ENOTFOUND
Relay hostname unreachable from this machine. Try curl -I $TAG_RELAY_URL.
mcp child exited: 127
--mcp-cmd resolves to a binary that isn't on PATH (or inside the
container image, if you're running in Docker). Run the command standalone to
isolate.
spawn python ENOENT / Error: spawn <name> ENOENT
The bridge couldn't find the binary you asked it to run. Most common cause
on Debian/Ubuntu hosts: python isn't installed by default (only python3).
The bridge auto-falls-back to python3 when it can, and prints a warning;
for any other missing binary you'll get a clear pre-flight error. Three fixes:
- Use
python3explicitly:--mcp-cmd "python3 -m my_tools.server" - Install the symlink package:
apt-get install -y python-is-python3 - Pass an absolute path:
--mcp-cmd "/usr/bin/python3 -m my_tools.server"
Bridge keeps reconnecting in a loop Each disconnect backs off exponentially up to 30 seconds and retries forever. If the relay is up but the bridge keeps dropping, check:
- another bridge process is already holding your dev slot (last writer
wins — kill the old one with
pkill -f tag-mcp-bridge), - your laptop sleep/wake is breaking the TCP keepalive (the bridge will recover on the next wake; this is by design).
MCP session resets between calls The bridge keeps one MCP child for its entire lifetime, so tool state is preserved across relay reconnects. If you see state loss, the MCP child itself probably crashed and was respawned — check stderr.
Development
git clone https://github.com/jaimetournesol/TAG
cd TAG
pnpm install
pnpm --filter @tournesol-tag/mcp-bridge build
pnpm --filter @tournesol-tag/mcp-bridge testSource lives in apps/mcp-bridge/src/:
| file | purpose |
|----------------------|------------------------------------------------|
| cli.ts | argv parsing, env fallbacks, signal handling |
| bridge.ts | WS lifecycle, RPC envelope translation |
| mcp-stdio.ts | spawn MCP child, frame JSON-RPC over stdio |
| protocol.ts | relay envelope schema (Zod-free, hand-rolled) |
| backoff.ts | exponential backoff with jitter, capped at 30s |
License
MIT — see LICENSE.
