Files
docs/history/mcp-gateway-requirements.md

132 lines
7.8 KiB
Markdown

> **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-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 `:8000`
and `spawn_container` host-publishes `8000: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.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.
## 7. Recommended Path (ordered, lowest-risk first)
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.