Skip to content

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 by 726bd10 "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 + uvicorn on 0.0.0.0:8080, container mcp-gateway. - DinD via bind-mounted /var/run/docker.sock; docker.from_env(). - Per-server containers spawned mcp-<name>, hardcoded onto ai-internal. - Config config.json (RW bind, currently EMPTY → falls back to DEFAULT_SERVERS). - Gateway joins ai-internal + shared_backend (both external: true).

B. Static compose mcp-tools.yaml — orphaned; ai/docker-compose.yml:6 include is commented out. Internally malformed (see §4).

C. LiteLLM-hostedlitellm-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-proxypaper_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 :8000 and spawn_container host-publishes 8000:8000two 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.json empty → always defaults; secrets hardcoded plaintext in server.py.

Broken / contradictory: - ai/docker-compose.yml:6 mcp-tools include commented out; gateway compose is a separate project not referenced by the stack either — wired in only via Zoraxy. - mcp-tools.yaml: duplicate markitdown-mcp key; comfyui-mcp env missing = (- COMFYUI_URL http://comfyui:8188); missing images/ports. - comfyui server type:"npm" vs Docker-image mismatch; upstream image/npm package existence unverified (image confirmed not pullable). - .env has CRAWL4AI_MCP_OAUTH_*, COMFYUI_MCP_OAUTH_* that server.py never 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)

  1. Hosting model: DinD gateway vs static mcp-tools.yaml vs LiteLLM-hosted.
  2. How Claude.ai/LobeChat reach a tool: the MCP transport bridge doesn't exist.
  3. Port allocation: all servers hardcode :8000 — need internal DNS, no host publish.
  4. Uniform npx/uvx/docker run model: prebuilt images vs runtime install.
  5. Auth model: gateway-terminated vs per-MCP vs pass-through (callback is broken).
  6. Secret handling: hardcoded → ${ENV} from ai/.env.
  7. comfyui server: image vs npm; keep only once ComfyUI itself is stable.
  8. Discovery for LobeChat/LiteLLM: /mcp.json is OAuth-gated, lists config not endpoints.
  1. 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 (uncomment ai/docker-compose.yml:6).
  2. Fix mcp-tools.yaml: dedupe markitdown-mcp, fix comfyui-mcp env =, unique service names → ai-internal DNS, pin images, drop comfyui for now.
  3. One streamable-http reverse proxy keyed by path (mcp.nuclide.systems/<server>) via Zoraxy or a small httpx proxy — replace the fake SSE stub. Backends stay internal on ai-internal:8000, never host-published.
  4. Move secrets to ${ENV} from ai/.env (keys already exist).
  5. Fix OAuth callback: exchange code against GENERIC_TOKEN_ENDPOINT.
  6. Verify each upstream image/tool exists before marking a server "working".
  7. Defer the DinD gateway + Web UI to phase-2 (read-only status over the running compose, not spawning).
  8. Answer the litellm question: once stable https://mcp.nuclide.systems/<server> URLs exist, populate litellm-config/config.yaml mcp_servers: so LiteLLM/LobeChat discover them — no bespoke discovery path needed.