Spaces:
Sleeping
Sleeping
File size: 9,040 Bytes
01708c6 aee78d5 8d62a8a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
## 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
|