Spaces:
Running
Running
## Remote MCP Auth Options and Plan π | |
### Goals and constraints | |
- Keep Streamable HTTP working with Claude Desktop via `supergateway` | |
- Avoid breaking the existing Hugging Face Space deployment | |
- Minimal friction now; allow stronger auth later | |
- Secrets remain server-side (no tokens in client prompts) | |
### Current state | |
- Server: FastMCP HTTP on HF Spaces at `/mcp/` (see `main.py`) | |
- Access: Claude Desktop via `npx supergateway --streamableHttp <space>/mcp/` | |
- Auth: none (Space public), Notion secrets live only in server env | |
### Options overview | |
- **Option A β Bearer token gate via lightweight reverse proxy (recommended now)** | |
- **Option B β HF OAuth gate (simple but not Desktop-friendly)** | |
- **Option C β Cloudflare Access in front (enterprise-grade, more infra)** | |
- **Option D β Query param token (simplest, less secure)** | |
--- | |
## Option A β Bearer token gate via lightweight reverse proxy (recommended) | |
Add an ASGI reverse proxy inside the container that validates a token, then proxies to the FastMCP HTTP transport running on an internal port. | |
#### Architecture | |
``` | |
Claude Desktop β supergateway β HF Space (proxy:7860) | |
β | |
ββ> FastMCP HTTP (internal:7870) | |
``` | |
#### Benefits | |
- Works with Desktop + supergateway (no interactive OAuth) | |
- Simple env-only config and rotation (`MCP_AUTH_TOKEN`) | |
- Keeps FastMCP server unchanged; only wraps it | |
#### Implementation plan | |
1) Env vars | |
- `MCP_AUTH_TOKEN=<random-32+ chars>` | |
- `UPSTREAM_HOST=127.0.0.1` | |
- `UPSTREAM_PORT=7870` | |
2) Tiny ASGI proxy | |
- Validate `Authorization: Bearer <token>` OR `?key=<token>` | |
- Forward to upstream (`/mcp/` SSE + JSON payloads) | |
- Return 401 on missing/invalid | |
3) Run both servers in `main.py` | |
- Start FastMCP HTTP runner on `UPSTREAM_PORT` | |
- Start the proxy on `PORT` (7860) | |
4) Local run | |
- `uv run python main.py` (proxy on 7860) β supergateway to `http://localhost:7860/mcp/` | |
5) HF Space | |
- Add `MCP_AUTH_TOKEN` secret | |
- Keep `PORT=7860` | |
#### Example proxy (outline) | |
```python | |
# file: src/foodwise/mcp_server/auth_proxy.py (outline) | |
from fastapi import FastAPI, Request, HTTPException | |
import httpx, os | |
app = FastAPI() | |
TOKEN = os.getenv("MCP_AUTH_TOKEN") | |
UPSTREAM = f"http://{os.getenv('UPSTREAM_HOST','127.0.0.1')}:{os.getenv('UPSTREAM_PORT','7870')}" | |
def _authorized(req: Request) -> bool: | |
if not TOKEN: | |
return True | |
auth = req.headers.get("authorization", "") | |
if auth.startswith("Bearer ") and auth.split(" ",1)[1] == TOKEN: | |
return True | |
key = req.query_params.get("key") | |
return key == TOKEN | |
@app.api_route("/mcp/{path:path}", methods=["GET","POST"]) | |
async def proxy(req: Request, path: str): | |
if not _authorized(req): | |
raise HTTPException(status_code=401, detail="Unauthorized") | |
url = f"{UPSTREAM}/mcp/{path}" | |
async with httpx.AsyncClient(timeout=None) as client: | |
if req.method == "GET": | |
return await client.get(url, params=dict(req.query_params), headers=dict(req.headers)) | |
body = await req.body() | |
return await client.post(url, content=body, params=dict(req.query_params), headers=dict(req.headers)) | |
``` | |
#### Desktop usage | |
- If headers arenβt feasible with supergateway, use `?key=`: | |
- `npx supergateway --streamableHttp "https://<space>.hf.space/mcp/?key=$MCP_AUTH_TOKEN"` | |
#### Security notes | |
- Prefer headers over query params | |
- Rotate `MCP_AUTH_TOKEN` periodically | |
- Consider basic rate limiting at proxy | |
--- | |
## Option B β HF OAuth gate (simple, Desktop constraints) | |
- Set `hf_oauth: true` to gate the Space to logged-in HF users. | |
- Pros: zero code; handled by HF login. | |
- Cons: Claude Desktop + supergateway typically cannot complete interactive OAuth, so this blocks the Desktop flow. | |
Use for human dashboards, not recommended for MCP Desktop flow today. | |
--- | |
## Option C β Cloudflare Access (strongest perimeter) | |
- Put the endpoint behind Cloudflare Access; issue service tokens to a colocated bridge that injects headers. | |
- Pros: SSO/MFA, org policies, logs. | |
- Cons: Added infra; may require custom domain or tunnel. | |
Good when you need enterprise auth. | |
--- | |
## Option D β Query param token only (least secure) | |
- Server checks `?key=...` and rejects otherwise. | |
- Pros: Trivial; compatible with supergateway URLs. | |
- Cons: Token may appear in logs/referrers; use only if headers arenβt possible. | |
--- | |
## Recommended path | |
1) Implement Option A now (proxy + Bearer/`?key` token) | |
2) Keep Space public for Desktop connectivity | |
3) Revisit Option C for team/enterprise use cases | |
## Action checklist | |
- [ ] Add `MCP_AUTH_TOKEN` in Space secrets | |
- [ ] `uv add fastapi httpx uvicorn` (for proxy) | |
- [ ] Add `auth_proxy.py` and wire `main.py` to start FastMCP on 7870 + proxy on 7860 | |
- [ ] Update `planning/remote_mcp.md` with `?key=` example | |
- [ ] Test locally with `mcp-inspector` and Desktop via `supergateway` | |
--- | |
## Execution Plan β Option A Token Proxy (branch: `feat/mcp-auth-proxy`) | |
### Scope | |
- Add a lightweight ASGI reverse proxy that enforces a shared token (`MCP_AUTH_TOKEN`) and forwards requests to the existing FastMCP HTTP server. | |
- Keep the FastMCP server unchanged; only wrap it with the proxy. | |
- Update docs and examples to include token usage, and add a small smoke-test section for Space logs. | |
### Milestones | |
1) Dependencies (uv; do not edit `pyproject.toml` directly) | |
- Run: | |
- `uv add fastapi httpx uvicorn` | |
2) Proxy module | |
- File: `src/foodwise/mcp_server/auth_proxy.py` | |
- Responsibilities: | |
- Authorize via `Authorization: Bearer <MCP_AUTH_TOKEN>` OR `?key=<MCP_AUTH_TOKEN>` | |
- Forward all `/mcp/*` to upstream (`http://127.0.0.1:7870/mcp/*`) with SSE-safe streaming (no buffering, relaxed timeouts) | |
- Strip sensitive auth before forwarding (remove `Authorization` header and any `key` query param) | |
- Minimal `/health` endpoint returning `{"status": "ok"}` for quick checks | |
- Avoid logging headers or query strings; mask secrets in error logs | |
- Env: | |
- `MCP_AUTH_TOKEN` (required in Space) | |
- `UPSTREAM_HOST=127.0.0.1` | |
- `UPSTREAM_PORT=7870` | |
3) Process model | |
- FastMCP serves HTTP internally on `UPSTREAM_PORT` (7870) | |
- Proxy runs on public `PORT` (7860) | |
- `main.py` will start FastMCP (internal) and then the proxy app | |
- Implementation detail: run FastMCP in a background thread/process, then launch `uvicorn` for the proxy | |
4) Local run and tests | |
- Start: | |
- `uv run python main.py` | |
- Test (query param): | |
- `mcp-inspector "http://localhost:7860/mcp/?key=$MCP_AUTH_TOKEN"` | |
- `npx supergateway --streamableHttp "http://localhost:7860/mcp/?key=$MCP_AUTH_TOKEN" --logLevel debug` | |
- Test (header): | |
- `curl -I -H "Authorization: Bearer $MCP_AUTH_TOKEN" http://localhost:7860/mcp/` | |
- Health: | |
- `curl http://localhost:7860/health` | |
5) Hugging Face Space rollout | |
- Add Space secret: `MCP_AUTH_TOKEN` | |
- Keep `PORT=7860` (proxy) | |
- Push branch to Space remote or merge to `main` and push | |
- Verify Space logs: expect `401 Unauthorized` without token and `200` with valid token | |
- Desktop bridge: | |
- `npx supergateway --streamableHttp "https://<space>.hf.space/mcp/?key=$MCP_AUTH_TOKEN" --logLevel debug` | |
6) Documentation updates | |
- `planning/remote_mcp.md`: | |
- Add `?key=` examples for Inspector and supergateway | |
- Note the two-port model (proxy 7860, upstream 7870) | |
- `src/foodwise/mcp_server/README.md`: | |
- Add a "Remote (token)" subsection with examples and `/health` check | |
- Add `.env.example` including: | |
- `NOTION_SECRET`, `NOTION_INVENTORY_DB_ID`, optional `NOTION_SHOPPING_DB_ID` | |
- `MCP_AUTH_TOKEN`, `UPSTREAM_HOST`, `UPSTREAM_PORT`, `PORT` | |
### Acceptance criteria | |
- `/mcp/` returns 401 without valid token | |
- Valid token via header or `?key` allows full MCP functionality end-to-end (Inspector + supergateway) | |
- Local and Space deployments both verified | |
- Docs updated and `.env.example` committed | |
### Lean scope (MVP to avoid bloat) | |
- Implement now: | |
- Token check (header preferred, `?key` fallback for Desktop bridge) | |
- SSE-safe proxy streaming with relaxed timeouts | |
- Strip auth before forwarding; add `/health` | |
- Minimal logging with secret masking; no CORS | |
- Defer (post-MVP): | |
- Rate limiting and request size limits | |
- Structured metrics/observability | |
- Cloudflare Access or other perimeter options | |
- OAuth-based flows (not Desktop-friendly today) | |
### Security notes | |
- Prefer Authorization header in production; `?key` is for compatibility with bridges | |
- Rotate `MCP_AUTH_TOKEN` periodically; treat it like a password | |
- Consider basic rate limiting and request logging in proxy if abuse is a concern | |
### Rollback | |
- Revert the branch/merge commit; Space continues to run FastMCP directly on 7860 as today | |
- Remove `MCP_AUTH_TOKEN` secret if not using the proxy | |
### Next steps (post-merge) | |
- Evaluate Cloudflare Access (Option C) for team/org use | |
- Optional: add minimal request metrics to proxy and redact sensitive headers in logs | |