App-password generated via occ for fkrebs@nucli.de. Gateway patched to persist enabled flag from config.json across restarts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.0 KiB
MCP Gateway v2 — Operational
DinD MCP gateway on CT 104. Single OAuth-gated entry point fronting 24 MCP servers (mix of static, spawned, and Docker-catalog-bridged). Public via Zoraxy at https://mcp.nuclide.systems (→ 192.168.1.40:8080).
Architecture
- Stack:
ai/mcp-gateway/— FastAPI app, containermcp-gateway. - Spawn model: Docker-in-Docker. Each spawnable server runs as
mcp-<name>on theai-internalnetwork with no host ports — servers talk over internal DNS. - Transport bridge: generic streaming reverse proxy
https://mcp.nuclide.systems/<server>/<path>→http://<upstream>/<path>. Transport-agnostic (streamable-HTTP/mcpand legacy SSE/sse+/messages). Registered last so specific routes win. - Auth: Pocket ID. Two paths off the same OIDC app:
- MCP clients / API / proxy —
Bearertoken, validated via OIDC userinfo (300 s TTL cache).401carriesWWW-Authenticate+ RFC 9728/.well-known/oauth-protected-resourcefor discovery. - Browser Web UI — Authorization-Code flow with httpOnly session cookie.
/uiand/api/*accept cookie or Bearer;/{srv}/stays Bearer-only.
- MCP clients / API / proxy —
- Secrets: per-server OAuth client secrets live in
ai/.env(read viaos.environ), never hardcoded.
Server inventory (as of 2026-05-22)
26 servers configured in mcp-gateway/config.json. Status: 26 healthy, 1 disabled (nextcloud re-enabled 2026-05-22).
| Group | Server | Kind | Notes |
|---|---|---|---|
| dev | context7 | catalog | mcp/context7 |
| fetch | spawn | mcp-server-fetch via uvx |
|
| git | catalog | mcp/git — Docker MCP Catalog; mcp-git bridge + DinD child |
|
| gitlab | catalog | mcp/gitlab — GITLAB_API_URL=https://gitlab.dlr.de/api/v4; requires --pass-environment in bridge cmd |
|
| kroki | static | kroki-mcp:8000 (own stack) |
|
| markitdown | catalog | mcp/markitdown |
|
| sequential-thinking | catalog | mcp/sequentialthinking |
|
| time | spawn | mcp-server-time |
|
| docling | static | docling-mcp:8000 |
|
| coder | static | coder-mcp:8000 |
|
| shepard | static | https://shepard.nuclide.systems/v2/mcp |
|
| research | crawl4ai | spawn | unclecode/crawl4ai |
| paper-search | catalog | mcp/paper-search — Docker MCP Catalog; replaces old python:3.12-slim inline approach |
|
| searxng | spawn | isokoliuk/mcp-searxng |
|
| wikipedia-mcp | catalog | ||
| youtube-transcript | catalog | mcp/youtube-transcript |
|
| personal | bluesky | static | ariel-mcp:8000 |
| gotify | spawn | ||
| home-assistant | static | http://192.168.1.60:9583/private_... (HA add-on) |
|
| immich | spawn | ||
| memos | spawn | uses MEMOS_TOKEN from .env |
|
| n8n | static | https://n8n.nuclide.systems/mcp-server/http |
|
| nextcloud | spawn | ghcr.io/cbcoutinho/nextcloud-mcp-server — single_user_basic mode, NEXTCLOUD_VERIFY_SSL=false; app-password generated via occ for fkrebs@nucli.de 2026-05-22 |
|
| unifi | spawn | ||
| image | comfyui | static | comfyui-mcp:8000 (own stack, see comfyui.md) |
| storage | upload-artifact | static | upload-artifact-mcp:8000 |
Catalog bridge env var passthrough:
mcp-proxy'sstdio_clientuses a sanitized env (get_default_environment()), stripping custom vars. For catalog servers that need env vars (e.g.gitlab), the bridge command must include--pass-environmentso Docker CLI can resolve-e KEYpassthrough from the bridge container's env.
Nextcloud server count: 27 total (26 enabled + nextcloud now re-enabled 2026-05-22). Gateway updated to persist
enabledflag across restarts.
Client registration
Easy path — /mcp-config download
The gateway hands back a ready-to-use config blob for the signed-in user:
GET https://mcp.nuclide.systems/mcp-config?format=claude # default
GET https://mcp.nuclide.systems/mcp-config?format=cursor
GET https://mcp.nuclide.systems/mcp-config?format=raw # token + URLs
- Requires Pocket-ID session cookie or existing Bearer (303 →
/loginotherwise). - Mints a fresh long-lived gateway token (
mcp_<urlsafe>) bound to the user; persisted ingateway_tokens.jsonwithpurpose=client-config:<format>so it's distinguishable from OAuth-issued tokens and revocable. - Returns JSON with
Content-Disposition: attachment.
Claude Code workflow:
- Sign in at
https://mcp.nuclide.systems. - Click Claude config in the top nav → browser downloads
nuclide-mcp.claude.json. - Merge the
mcpServersblock into~/.claude/settings.json. Restart Claude Code.
The downloaded file has one entry per enabled MCP server, with type: http + url: https://mcp.nuclide.systems/<server>/mcp + Authorization: Bearer ... header. No per-server config needed.
Manual
LobeChat — Settings → Skills → Skill Store → Custom → Import JSON:
{ "mcpServers": { "comfyui": { "type": "http",
"url": "https://mcp.nuclide.systems/comfyui/mcp",
"auth": { "type": "bearer", "accessToken": "<pocket-id token>" } } } }
(For ComfyUI specifically, LobeChat can also connect straight to http://comfyui-mcp:8000/mcp internally — see comfyui.md — no token.)
Claude.ai — add https://mcp.nuclide.systems/<server>/mcp as a custom connector; auth is discovered via the protected-resource metadata.
Token rotation
Each /mcp-config download mints a NEW token. Old tokens stay valid until removed from gateway_tokens.json (manual prune; future: admin UI button).
Ops
cd /opt/stacks/ai/mcp-gateway && docker compose up -d --build
docker logs mcp-gateway
UI: https://mcp.nuclide.systems/ui (Pocket-ID-gated; /login to sign in).
Config: mcp-gateway/config.json (live config, persisted). Timestamped backups created on every edit (config.json.bak.YYYYMMDD-HHMMSS).
Add or enable a server
- Edit
mcp-gateway/config.json— add anmcpServers.<name>entry (or setenabled: true). - Restart the gateway:
docker restart mcp-gateway. - Spawn servers come up on demand via
POST /api/servers/<name>/start(bearer); orPOST /api/servers/bulk/startfor all.
Disable a server
Set mcpServers.<name>.enabled = false and add disabled_reason. Stop+rm the container (docker stop mcp-<name> && docker rm mcp-<name>); the gateway will not re-spawn it.
UI check (Playwright)
Live UI is auth-gated, so the local check serves the template + mocks /api/servers:
uv run --with playwright -- python -m playwright install chromium # once
uv run --with playwright -- python ai/mcp-gateway/ui_check.py # -> /tmp/mcp_ui.png
Verified end-to-end
/health200 (no auth);/.well-known/oauth-protected-resourceJSON (no auth)./mcp.json,/<server>/mcp→ 401 without token; correctWWW-Authenticate.- Static upstreams (
bluesky,coder,comfyui,docling,kroki,upload-artifact) — endpoints reachable from insideai-internal(406/405 to plain GET, which is the correct MCP behavior). - All spawn/catalog containers up (
docker ps mcp-*): 16/17 (nextcloud disabled; git/gitlab/paper-search added 2026-05-22).