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