132 lines
7.8 KiB
Markdown
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.
|