Skip to content

# MCP Gateway — Bifrost (migrated 2026-05-26)

MCP servers are now aggregated by Bifrost at `https://ai.nuclide.systems/mcp`. The legacy FastAPI DinD mcp-gateway (`mcp.nuclide.systems`) is pending decommission.

## Architecture

- Stack: `ai/bifrost/` on CT 104 — Bifrost LLM+MCP gateway, SQLite state at `data/config.db`.
- Endpoint: `https://ai.nuclide.systems/mcp` (Zoraxy → `192.168.1.40:14003`)
- Auth: Virtual Key (`sk-bf-` prefix) via `Authorization: Bearer <vk>`.
- Client model: Each upstream MCP server is a registered client in Bifrost (`auth_type=none`, `allow_on_all_virtual_keys=true`). Tools are auto-discovered and enabled via `tools_to_execute_json`.
- Internal upstreams: All child MCP containers run on the `ai-internal` Docker network at `http://<name>-mcp:8000/mcp` (streamable-HTTP) or as dedicated stacks.

## Virtual keys

Name | Key prefix | Use
--- | --- | ---
`claude-code` | `sk-bf-bfc19117-4c46-4d48-9b10-85d78b1ae2b3` | Claude Code + Claude.ai
`mcp-dev` | `sk-bf-279d3ecc-9031-41ff-a582-ecf3dca61d52` | Testing / dev
`open-webui` | `sk-bf-7e6999fc-2d86-48f9-8ac9-0ba558893d87` | Open WebUI (`LITELLM_API_KEY` in `ai/.env`)

## Server inventory (as of 2026-05-26)

29 connected clients, ~760 tools total.

Client name | Upstream | Notes
--- | --- | ---
`bluesky` | `http://ariel-mcp:8000/mcp` |
`coder` | `http://coder-mcp:8000/mcp` |
`comfyui` | `http://comfyui-mcp:8000/mcp` | Intel Arc image gen
`context7` | `http://mcp-context7:8000/mcp` |
`crawl4ai` | `http://mcp-crawl4ai:11235/mcp/sse` (SSE) |
`docling` | `http://docling-mcp:8000/mcp` | PDF→Markdown
`fetch` | `http://mcp-fetch:8000/mcp` |
`git` | `http://mcp-git:8000/mcp` |
`gitea` | `http://gitea-mcp:8000/mcp` |
`gitlab` | `http://mcp-gitlab:8000/mcp` | `GITLAB_API_URL=https://gitlab.dlr.de/api/v4`
`gotify` | `http://mcp-gotify:8000/mcp` |
`home_assistant` | `http://192.168.1.60:9583/private_ehnWeRl2G3De6NnbcN7teQ` | HA add-on; no TLS
`immich` | `http://mcp-immich:8000/mcp` |
`kroki` | `http://kroki-mcp:8000/mcp` | Diagram rendering
`markitdown` | `http://mcp-markitdown:8000/mcp` |
`memos` | `http://mcp-memos:8000/mcp` |
`nextcloud` | `http://mcp-nextcloud:8000/mcp` |
`ntfy` | `http://mcp-ntfy:8000/mcp` |
`obsidian` | `http://mcp-obsidian:8000/mcp` | Vault at Nextcloud/UNAS
`paper_search` | `http://mcp-paper-search:8000/mcp` |
`paperless` | `http://paperless-mcp:8000/mcp` |
`proxmox` | `http://mcp-proxmox:8000/mcp` | Read-only (`PVEAuditor`)
`searxng` | `http://mcp-searxng:8000/mcp` |
`sequential_thinking` | `http://mcp-sequential-thinking:8000/mcp` |
`time` | `http://mcp-time:8000/mcp` |
`unifi` | `http://mcp-unifi:8000/mcp` |
`upload_artifact` | `http://upload-artifact-mcp:8000/mcp` | S3 via Garage
`wikipedia` | `http://mcp-wikipedia-mcp:8000/mcp` |
`youtube_transcript` | `http://mcp-youtube-transcript:8000/mcp` |

### Not yet connected

Client | Reason
--- | ---
`n8n` — `https://n8n.nuclide.systems/mcp-server/http` | Streamable-HTTP transport: POST returns SSE stream, Bifrost HTTP client times out.
`shepard` — `https://shepard.nuclide.systems/v2/mcp` | Same streamable-HTTP issue. Bearer token stored in `/tmp/migrate_mcp_oauth.py`.

## Client configuration

### Claude Code

Add to `~/.claude.json` or project `.mcp.json`:

    {
      "mcpServers": {
        "nuclide": {
          "type": "http",
          "url": "https://ai.nuclide.systems/mcp",
          "headers": {
            "Authorization": "Bearer sk-bf-bfc19117-4c46-4d48-9b10-85d78b1ae2b3"
          }
        }
      }
    }

### Claude.ai

Settings → Integrations → Add MCP server: - URL: `https://ai.nuclide.systems/mcp` - Header: `Authorization: Bearer sk-bf-279d3ecc-9031-41ff-a582-ecf3dca61d52`

## LLM Inference & Governance

Bifrost proxies LLM inference at `/v1` (OpenAI-compatible). `enforce_auth_on_inference=1` — all `/v1` calls require a valid `sk-bf-*` VK.

### Providers

Provider | Internal name | Base URL | Notes
--- | --- | --- | ---
SAIA (GPU cluster) | `openai` | `https://chat-ai.academiccloud.de/v1` | Rate limited (see below)
Google Gemini | `gemini` | default |
Mistral AI | `mistral` | default |
Cerebras | `cerebras` | default |
claude-max-bridge | `openrouter` | `http://claude-max-bridge:8000` | Claude Opus/Sonnet/Haiku via max subscription

### SAIA rate limits

SAIA enforces per-account quotas. Bifrost is configured with a global provider-level limit (`config_providers.rate_limit_id='saia-minute'`):

Window | SAIA limit | Bifrost enforcement
--- | --- | ---
Per minute | 30 req | ✅ active (`saia-minute` row)
Per hour | 200 req | row exists (`saia-hour`), not linked
Per day | 1000 req | row exists (`saia-day`), not linked
Per month | 3000 req | not trackable across restarts

Note: Bifrost only supports one rate limit window per provider. The minute window is linked because it provides burst protection. The `saia-hour` / `saia-day` rows are in `governance_rate_limits` and can be linked by updating `config_providers SET rate_limit_id='saia-hour'` if needed.

To change the active window:

    sqlite3 /opt/stacks/ai/bifrost/data/config.db \
      "UPDATE config_providers SET rate_limit_id='saia-day' WHERE name='openai';"
    docker compose -f /opt/stacks/ai/bifrost.yml up -d --force-recreate

Rate limit API: `GET /api/governance/rate-limits` is read-only. POST/PUT return 405. Use direct SQLite to create new entries.

### Virtual key governance

All VKs (`governance_virtual_key_provider_configs`) have: - `allow_all_keys=1` — set via SQL (Bifrost API PUT silently ignores this field) - `allowed_models` — explicit JSON model list as text in DB (SQL NULL = deny all with enforce_auth_on_inference=1)

If models stop working after a Bifrost upgrade/restore, re-run `/tmp/fix_vk_final.py` on CT 104 and then:

    sqlite3 /opt/stacks/ai/bifrost/data/config.db \
      "UPDATE governance_virtual_key_provider_configs SET allow_all_keys=1;"
    docker compose -f /opt/stacks/ai/bifrost.yml up -d --force-recreate

## Ops

    # On CT 104
    cd /opt/stacks/ai/bifrost
    docker compose up -d --force-recreate

    docker logs bifrost -f

    # Inspect config DB
    sqlite3 data/config.db '.tables'
    sqlite3 data/config.db 'SELECT name, auth_type, allow_on_all_virtual_keys FROM config_mcp_clients;'

    # Count active tools via API
    curl -s http://localhost:14003/mcp \
      -H "Authorization: Bearer sk-bf-279d3ecc-9031-41ff-a582-ecf3dca61d52" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | python3 -m json.tool | grep '"name"' | wc -l

### Add a new MCP client

    curl -sc /tmp/bfcookies http://localhost:14003/api/session/login \
      -H "Content-Type: application/json" \
      -d '{"username":"fkrebs","password":"tapirnase"}' > /dev/null

    curl -s -X POST http://localhost:14003/api/mcp/client \
      -b /tmp/bfcookies \
      -H "Content-Type: application/json" \
      -d '{
        "name": "<name>",
        "connection_type": "http",
        "connection_string": "http://<host>:8000/mcp",
        "auth_type": "none",
        "allow_on_all_virtual_keys": true
      }'

Wait ~10 s for tool discovery, then enable tools via `PUT /api/mcp/client/<id>` with `tools_to_execute`. See `/tmp/add_all_mcp_clients.py` on CT 104 for a complete example.

### Key requirements for tools to appear in `/mcp tools/list`

1. `auth_type=none` (not `per_user_oauth`)
2. `allow_on_all_virtual_keys=true`
3. `tools_to_execute_json` populated (tool names without client prefix)
4. VK must have `sk-bf-` prefix

## Legacy mcp-gateway (FastAPI / DinD)

Status: DECOMMISSIONED 2026-05-26.

- Zoraxy route removed (config renamed to `.DECOMMISSIONED-2026-05-26` on CT 108)
- Container stopped; compose file renamed to `docker-compose.yml.DECOMMISSIONED-2026-05-26`
- Pocket-ID clients `e73bb7b9` (litellm/mcp-gateway) and `78c78998` (Claude MCP) deleted from CT 110 DB

## Bifrost OIDC client (Pocket ID)

Used for `per_user_oauth` flows (not currently active — all clients use `auth_type=none`):

Field | Value
--- | ---
Client ID | `ec0d15e6-e86d-49b0-ac12-cdfb5afc9086`
Authorize URL | `https://id.nuclide.systems/authorize`
Token URL | `https://id.nuclide.systems/api/oidc/token`
`mcp_external_client_url` | `https://ai.nuclide.systems`