omniroute-mcp-bridge
by jorisc90
README.md
# omniroute-mcp-bridge
A stdio MCP bridge that DSH spawns as a child process (via the `dsh-mcp-client`
plugin, `transport: stdio`). It proxies `tools/list` / `tools/call` to the
omniroute streamable-http MCP server at
`https://llm.claassen.family/api/mcp/stream` and exposes **only** the
allow-listed tools to the model.
## Why
* The upstream streamable-http session dies when idle ("Unknown Mcp-Session-Id"
404). The bridge detects that exact failure, tears down the stale client,
re-runs the MCP initialize handshake (fresh session id) and retries — so a
dead upstream session no longer breaks a tool call.
* The `dsh-mcp-client` plugin has no tool-filtering config. This bridge filters
`tools/list` server-side, so only `omniroute_web_search` is exposed instead
of ~70 tools (saves model context; stable `serverName: omniroute` is kept,
so the public name stays `mcp__omniroute__omniroute_web_search`).
* The upstream auth header is passed via child **env**, not argv — the secret
never appears in the process list. It lives only in the 0600
`cordis.patch.yml` of the web profile.
## Files
| file | purpose |
|-----------------|----------------------------------------------------------------|
| `bridge.mjs` | stdio MCP server entrypoint (spawned by DSH) |
| `upstream.mjs` | injectable upstream accessor with dead-session re-init logic |
| `test-bridge.mjs` | standalone live test: full MCP handshake + real search call |
| `test-reinit.mjs` | deterministic fake-upstream test of the re-init loop |
| `probe.mjs` | dumps raw upstream tool names |
| `probe-ttl2.mjs`| measures upstream session idle TTL |
## SDK resolution
`package.json` pins `@modelcontextprotocol/sdk` to the exact version the
bridge was developed against. Two ways to get it:
* **Standalone:** `npm install` in this directory.
* **Inside the DSH host (this deployment):** `node_modules` is a **symlink**
to `/usr/lib/node_modules/@deepseek-ai/dsh/node_modules`, so the bridge
always uses the same SDK build DSH's own mcp-client plugin uses. The symlink
is gitignored — recreate it after cloning in that environment:
ln -sfn /usr/lib/node_modules/@deepseek-ai/dsh/node_modules node_modules
## Config (env, set in `~/.dsh/profiles/web/cordis.patch.yml`)
| var | meaning |
|------------------------------|------------------------------------------------|
| `MCP_BRIDGE_UPSTREAM_URL` | streamable-http URL (required) |
| `MCP_BRIDGE_UPSTREAM_HEADERS`| JSON object of request headers |
| `MCP_BRIDGE_ALLOWED_TOOLS` | comma-separated raw tool names (omit = all) |
| `MCP_BRIDGE_LOG` | `1` = verbose logging on the child's stderr |
## Tests
Credentials are **not** committed. Export them first (values from your patch file):
export MCP_BRIDGE_UPSTREAM_URL='https://llm.claassen.family/api/mcp/stream'
export MCP_BRIDGE_UPSTREAM_HEADERS='{"Authorization":"Bearer …","X-Forwarded-For":"127.0.0.1"}'
Then:
node test-reinit.mjs # deterministic: dead session -> re-init -> success (no network needed)
node test-bridge.mjs # live: handshake + filtered list + real search
node probe.mjs # dump the upstream's raw tool names
node probe-ttl2.mjs # measure upstream session idle TTL
## Change the allow-list
Edit `MCP_BRIDGE_ALLOWED_TOOLS` in the patch file (comma-separated raw names
from `node probe.mjs`). The include-plugin HMR re-applies the patch on file
change; the mcp-client plugin respawns the bridge child.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues