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
  1. Tiny ASGI proxy
  • Validate Authorization: Bearer <token> OR ?key=<token>
  • Forward to upstream (/mcp/ SSE + JSON payloads)
  • Return 401 on missing/invalid
  1. Run both servers in main.py
  • Start FastMCP HTTP runner on UPSTREAM_PORT
  • Start the proxy on PORT (7860)
  1. Local run
  • uv run python main.py (proxy on 7860) β†’ supergateway to http://localhost:7860/mcp/
  1. 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

  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