Skip to main content
Glama
jorisc90

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.