LeoWalker's picture
chore(project): clean up project files and organize planning docs
8d62a8a
## 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