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/
(seemain.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
- Env vars
MCP_AUTH_TOKEN=<random-32+ chars>
UPSTREAM_HOST=127.0.0.1
UPSTREAM_PORT=7870
- Tiny ASGI proxy
- Validate
Authorization: Bearer <token>
OR?key=<token>
- Forward to upstream (
/mcp/
SSE + JSON payloads) - Return 401 on missing/invalid
- Run both servers in
main.py
- Start FastMCP HTTP runner on
UPSTREAM_PORT
- Start the proxy on
PORT
(7860)
- Local run
uv run python main.py
(proxy on 7860) β supergateway tohttp://localhost:7860/mcp/
- HF Space
- Add
MCP_AUTH_TOKEN
secret - Keep
PORT=7860
Example proxy (outline)
# 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
- Implement Option A now (proxy + Bearer/
?key
token) - Keep Space public for Desktop connectivity
- 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 wiremain.py
to start FastMCP on 7870 + proxy on 7860 - Update
planning/remote_mcp.md
with?key=
example - Test locally with
mcp-inspector
and Desktop viasupergateway
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
Dependencies (uv; do not edit
pyproject.toml
directly)- Run:
uv add fastapi httpx uvicorn
- Run:
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 anykey
query param) - Minimal
/health
endpoint returning{"status": "ok"}
for quick checks - Avoid logging headers or query strings; mask secrets in error logs
- Authorize via
- Env:
MCP_AUTH_TOKEN
(required in Space)UPSTREAM_HOST=127.0.0.1
UPSTREAM_PORT=7870
- File:
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
- Implementation detail: run FastMCP in a background thread/process, then launch
- FastMCP serves HTTP internally on
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
- Start:
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 and200
with valid token - Desktop bridge:
npx supergateway --streamableHttp "https://<space>.hf.space/mcp/?key=$MCP_AUTH_TOKEN" --logLevel debug
- Add Space secret:
Documentation updates
planning/remote_mcp.md
:- Add
?key=
examples for Inspector and supergateway - Note the two-port model (proxy 7860, upstream 7870)
- Add
src/foodwise/mcp_server/README.md
:- Add a "Remote (token)" subsection with examples and
/health
check
- Add a "Remote (token)" subsection with examples and
- Add
.env.example
including:NOTION_SECRET
,NOTION_INVENTORY_DB_ID
, optionalNOTION_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
- Token check (header preferred,
- 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