208 lines
8.3 KiB
Markdown
208 lines
8.3 KiB
Markdown
# 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 |
|
|
| `wallabag` | `http://wallabag-mcp:8000/mcp` | Article archiver; save_article/get_article/search/list_recent/archive/delete |
|
|
| `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`:
|
|
|
|
```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:
|
|
```bash
|
|
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:
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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` |
|