7.8 KiB
STATUS: SUPERSEDED 2026-05-17 — current implementation lives in services/mcp-gateway.md. Kept for design-rationale history.
MCP Gateway — Reconstructed Design Spec (in-progress, "Phase 1")
Reconstructed 2026-05-16 from code/configs/git history. The gateway is a single-squash-commit first draft (
0cad389 "Phase 1: Create MCP Gateway with Docker-in-Docker support", preceded by726bd10 "WIP: MCP gateway prep"). Nothing has a second iteration in git — everything below is first-draft intent.
1. Goal / Intent
A single OAuth-protected HTTP entrypoint at https://mcp.nuclide.systems that
exposes a curated set of MCP servers to AI clients on the homelab. Primary
consumer: Claude.ai as a remote connector (SSE at /, every README's
"Usage in Claude.ai"). Secondary: LobeChat (chat.nuclide.systems) and
LiteLLM (ai.nuclide.systems), sharing the same Pocket ID OAuth app. It is
meant to replace the "cumbersome" static-compose approach (mcp-tools.yaml)
with a dynamic, UI-managed, self-hosting model — answering the open todo.md
question "MCP deployment seems cumbersome — can litellm host directly? how to
integrate npx, uvx, docker-based containers?". Unifying idea: normalize
npx / uvx / docker MCP servers behind one Dockerized gateway.
2. Architecture (three competing models in-repo; A chosen, B orphaned, C aspirational)
A. FastAPI gateway + Docker-in-Docker (chosen) — ai/mcp-gateway/
- FastAPI +
uvicornon0.0.0.0:8080, containermcp-gateway. - DinD via bind-mounted
/var/run/docker.sock;docker.from_env(). - Per-server containers spawned
mcp-<name>, hardcoded ontoai-internal. - Config
config.json(RW bind, currently EMPTY → falls back toDEFAULT_SERVERS). - Gateway joins
ai-internal+shared_backend(bothexternal: true).
B. Static compose mcp-tools.yaml — orphaned; ai/docker-compose.yml:6
include is commented out. Internally malformed (see §4).
C. LiteLLM-hosted — litellm-config/config.yaml mcp_servers: {} empty.
Confirms MCP hosting was intended for the gateway, not LiteLLM (the
todo.md "can litellm host directly?" question remains open).
Transports (normalized to HTTP-on-:8000): streamable-http (nextcloud,
mermaid), mcp-proxy --stateless stdio→HTTP (papersearch), native HTTP
(markitdown, crawl4ai :11235), and the gateway's own SSE / endpoint —
a STUB (fake initialize + 60s pings, no routing to backends).
Reverse proxy: Zoraxy mcp.nuclide.systems → 192.168.1.40:8080.
mcp-auth.nuclide.systems is an abandoned auth-sidecar idea (not exposed).
OAuth (Pocket ID @ id.nuclide.systems): OAuth2AuthorizationCodeBearer,
scopes {openid, mcp}, token validation via userinfo. Shared gateway client
(GENERIC_CLIENT_ID, same as LiteLLM/LobeChat). Per-server OAuth for
papersearch & nextcloud against the same Pocket ID.
3. MCP Server Inventory (reconciled — server.py MCP_SERVERS is authoritative)
| Server | Image / build | Transport | Port | Auth | Status |
|---|---|---|---|---|---|
| papersearch | python:3.12-slim + runtime uv tool install mcp-proxy → paper_search_mcp.server |
mcp-proxy stdio→http | 8000 | Pocket ID PAPERSEARCH_MCP_OAUTH_* |
plausible, runtime-install fragile |
| nextcloud | ghcr.io/cbcoutinho/nextcloud-mcp-server:latest |
streamable-http | 8000 | Pocket ID NEXTCLOUD_MCP_OAUTH_* |
likely workable (real image) |
| markitdown | python:3.12-slim + uvx markitdown-mcp --http |
http | 8000 | none | broken as written (uvx not in base image) |
| comfyui | ghcr.io/richardi-ai/comfyui-mcp-server:latest (type:"npm" mismatch) |
unspecified | 8000 | none | image not pullable; backend ComfyUI was crash-looping |
| crawl4ai | unclecode/crawl4ai:latest |
http | 11235 | none | likely workable; resource limits lost in rewrite |
| mermaid | node:20-slim + runtime npx -y mcp-mermaid |
streamable-http | 8000 | none | plausible, slow first start |
4. Implemented vs Unfinished vs Broken
Implemented: FastAPI app + OAuth scheme + userinfo token validation;
container lifecycle CRUD + persistence; Web UI SPA (templates/ui.html @ /ui);
gateway compose/Dockerfile + Zoraxy route.
Unfinished / stub:
- SSE
/is fake — no MCP transport bridging Claude.ai → spawned servers. Core gap. - No routing to per-server containers; all five servers bind the same
:8000andspawn_containerhost-publishes8000:8000→ two servers can't run at once. - OAuth callback non-functional — token-exchange URL built via
OAUTH_REDIRECT_URI.replace("/sso/callback","/token")(→ wrong host, not the Pocket ID token endpoint); token never stored/used. config.jsonempty → always defaults; secrets hardcoded plaintext inserver.py.
Broken / contradictory:
ai/docker-compose.yml:6mcp-tools include commented out; gateway compose is a separate project not referenced by the stack either — wired in only via Zoraxy.mcp-tools.yaml: duplicatemarkitdown-mcpkey;comfyui-mcpenv missing=(- COMFYUI_URL http://comfyui:8188); missing images/ports.comfyuiservertype:"npm"vs Docker-image mismatch; upstream image/npm package existence unverified (image confirmed not pullable)..envhasCRAWL4AI_MCP_OAUTH_*,COMFYUI_MCP_OAUTH_*thatserver.pynever consumes; code hardcodes secrets instead of${ENV}substitution.
5. Relevant .env keys (names only)
Gateway: GENERIC_CLIENT_ID/_SECRET/_REDIRECT_URI,
GENERIC_{AUTHORIZATION,TOKEN,USERINFO}_ENDPOINT, GENERIC_CLIENT_USE_PKCE,
OAUTH_SCOPES, OAUTH_TOKEN_URL.
Per-server: NEXTCLOUD_MCP_OAUTH_CLIENT_ID/_SECRET,
PAPERSEARCH_MCP_OAUTH_CLIENT_ID/_SECRET,
MARKITDOWN_MCP_OAUTH_CLIENT_ID/_SECRET (declared, unused),
CRAWL4AI_/COMFYUI_MCP_OAUTH_* (orphaned).
papersearch data sources: UNPAYWALL_EMAIL, CORE_API_KEY,
SEMANTIC_SCHOLAR_API_KEY, ZENODO_ACCESS_TOKEN, GOOGLE_SCHOLAR_PROXY_URL,
DOAJ_API_KEY. comfyui: COMFYUI_URL, COMFYUI_WS_URL.
6. Open Design Decisions (must resolve)
- Hosting model: DinD gateway vs static
mcp-tools.yamlvs LiteLLM-hosted. - How Claude.ai/LobeChat reach a tool: the MCP transport bridge doesn't exist.
- Port allocation: all servers hardcode
:8000— need internal DNS, no host publish. - Uniform npx/uvx/docker run model: prebuilt images vs runtime install.
- Auth model: gateway-terminated vs per-MCP vs pass-through (callback is broken).
- Secret handling: hardcoded →
${ENV}fromai/.env. - comfyui server: image vs npm; keep only once ComfyUI itself is stable.
- Discovery for LobeChat/LiteLLM:
/mcp.jsonis OAuth-gated, lists config not endpoints.
7. Recommended Path (ordered, lowest-risk first)
- Pick the static-compose path, not DinD — lowest risk on a single NUC;
DinD adds socket-exposure risk + a broken SSE bridge for little gain.
Fix and re-enable
mcp-tools.yaml(uncommentai/docker-compose.yml:6). - Fix
mcp-tools.yaml: dedupemarkitdown-mcp, fixcomfyui-mcpenv=, unique service names →ai-internalDNS, pin images, drop comfyui for now. - One streamable-http reverse proxy keyed by path (
mcp.nuclide.systems/<server>) via Zoraxy or a smallhttpxproxy — replace the fake SSE stub. Backends stay internal onai-internal:8000, never host-published. - Move secrets to
${ENV}fromai/.env(keys already exist). - Fix OAuth callback: exchange code against
GENERIC_TOKEN_ENDPOINT. - Verify each upstream image/tool exists before marking a server "working".
- Defer the DinD gateway + Web UI to phase-2 (read-only status over the running compose, not spawning).
- Answer the litellm question: once stable
https://mcp.nuclide.systems/<server>URLs exist, populatelitellm-config/config.yamlmcp_servers:so LiteLLM/LobeChat discover them — no bespoke discovery path needed.