nutrivita-mcp-oauth-spike
README.md
# N-US-078 MCP OAuth Spike
Throwaway spike to prove Claude can complete OAuth 2.1 + PKCE against a Nutrivita-hosted MCP server and call a trivial `ping` tool.
**Do not merge to `dev`.** This code is intentionally disposable.
## What it does
- Serves RFC 9728 protected-resource metadata and RFC 8414 authorization-server metadata.
- Implements a minimal OAuth 2.1 Authorization Server:
- `GET /oauth/authorize` — hardcoded login + consent page.
- `POST /oauth/authorize` — issues an authorization `code` (with PKCE binding and `iss`).
- `POST /oauth/token` — exchanges code for a JWT access token whose `aud` is the canonical MCP URI.
- Implements a stateless MCP Resource Server over Streamable HTTP:
- `POST /mcp` — returns `401` with `WWW-Authenticate` + `as_uri` when unauthenticated.
- Authenticated requests list and call a single `ping` tool.
## Run locally
```bash
cd scratch/n-us-078-mcp-oauth-spike
cp .env.example .env
# edit .env: set SPIKE_CLIENT_SECRET and JWT_SECRET
npm run dev
```
## Local curl test
```bash
# 1. Discover protected resource metadata
curl http://localhost:3000/.well-known/oauth-protected-resource
# 2. Open the consent page in a browser, approve, and copy the `code` from the URL.
# Or use the authorize URL directly:
open "http://localhost:3000/oauth/authorize?client_id=n-us-078-spike-client-id&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%2Fcallback&scope=mcp%3Aping&code_challenge=abc123&code_challenge_method=S256&state=xyz"
# 3. Exchange the code for an access token
curl -X POST http://localhost:3000/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "n-us-078-spike-client-id",
"client_secret": "n-us-078-spike-client-secret",
"code": "PASTE_CODE",
"redirect_uri": "https://localhost/callback",
"code_verifier": "PASTE_VERIFIER"
}'
# 4. Call ping via MCP (Accept must include both application/json and text/event-stream)
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer PASTE_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping"}}'
```
Note: the MCP transport returns a streamable response, so the body is formatted as an SSE event (`event: message\ndata: {...}`) even for direct HTTP responses.
## Deploy for Claude testing
The server **must** be on a public host with an **IPv4 A record** and **HTTPS** (Claude calls from `160.79.104.0/21`).
The recommended approach is a **separate subdomain** on the existing Nutrivita backend VM so the spike doesn't touch the backend Nginx config.
## Deploy on Render (quick option)
If you don't want to touch GCP, Render can host the spike directly. The repo includes a `render.yaml` blueprint that creates a free Web Service with the required env vars.
### 1. Create a GitHub repo for the spike
Create a new **private** GitHub repository (e.g. `nutrivita-mcp-oauth-spike`). Do not include the Nutrivita main repo.
### 2. Push the spike files
From this directory, push all files **except** secrets and dependencies:
```bash
# Run in a terminal
git init
git checkout -b main
# Add all the files: package.json, *.mjs, Dockerfile, README.md, .env.example,
# .env.production.example, nginx-mcp-spike.conf, deploy.sh, render.yaml
git add package.json config.mjs oauth.mjs server.mjs test-flow.mjs Dockerfile README.md .env.example .env.production.example nginx-mcp-spike.conf deploy.sh render.yaml
git commit -m "N-US-078 MCP OAuth spike"
git remote add origin https://github.com/YOUR_USER/nutrivita-mcp-oauth-spike.git
git push -u origin main
```
### 3. Create the Render service
1. In the Render dashboard, click **New → Web Service**.
2. Connect the `nutrivita-mcp-oauth-spike` GitHub repository.
3. Render will detect `render.yaml` and provision the service automatically.
4. Wait for the build to finish. The service URL will be something like `https://nutrivita-mcp-spike.onrender.com`.
The `render.yaml` sets:
- `SPIKE_CLIENT_ID` = `n-us-078-spike-client-id`
- `SPIKE_CLIENT_SECRET`, `JWT_SECRET`, `SPIKE_USER_PASSWORD` = Render-generated random secrets
- `BASE_URL` is derived automatically from `RENDER_EXTERNAL_URL`.
You can override any of these in the Render Dashboard → Environment tab.
### 4. Verify
```bash
curl https://nutrivita-mcp-spike.onrender.com/.well-known/oauth-authorization-server
curl https://nutrivita-mcp-spike.onrender.com/health
```
### 5. Connect in Claude
Use the Render URL as the MCP server endpoint:
- MCP server URL: `https://nutrivita-mcp-spike.onrender.com/mcp`
- Client ID: `n-us-078-spike-client-id`
- Client Secret: copy from the Render Dashboard (`SPIKE_CLIENT_SECRET`)
- Consent password: copy from the Render Dashboard (`SPIKE_USER_PASSWORD`)
> **Note:** Render's free Web Service sleeps after 15 minutes of inactivity. The first request may take 30–60 seconds to wake up. For the OAuth handshake, either ping `/health` first or upgrade to a paid instance.
## GCP deployment (if you prefer the Nutrivita VM)
### 1. Prepare environment
Create the production env file and fill in real secrets:
```bash
cp .env.production.example .env.production
# edit .env.production
```
### 2. Add a DNS record
Create an A record for `mcp-spike.nutrivita.asia` (or your chosen subdomain) pointing to the external IPv4 address of the `nutrivita-backend` VM.
### 3. Run the deploy script
Requires: `gcloud` CLI authenticated, Docker installed, and permission to push to the project's Artifact Registry.
```bash
export GCP_PROJECT_ID=your-project-id
export GCP_REPOSITORY_NAME=your-repo-name
export DOMAIN=mcp-spike.nutrivita.asia
export SSL_EMAIL=admin@nutrivita.asia
chmod +x deploy.sh
./deploy.sh
```
The script will:
1. Build and push the spike Docker image.
2. SSH into the existing VM and run the container on `localhost:3002`.
3. Install an Nginx site for the subdomain.
4. Obtain a Let's Encrypt certificate.
5. Test the metadata endpoint.
If you prefer, you can run the same steps manually; see `deploy.sh` for the exact commands.
### 4. Verify
```bash
curl https://mcp-spike.nutrivita.asia/.well-known/oauth-authorization-server
curl https://mcp-spike.nutrivita.asia/health
```
### 5. Connect in Claude
1. In Claude, go to **Settings → Custom Connectors → Add connector**.
2. Enter the MCP server URL: `https://mcp-spike.nutrivita.asia/mcp`.
3. In **Advanced settings**, enter the `SPIKE_CLIENT_ID` and `SPIKE_CLIENT_SECRET` from `.env.production`.
4. On the hardcoded login+consent page, enter the `SPIKE_USER_PASSWORD` and click **Approve**.
5. Ask Claude "call ping" and confirm it returns `pong`.
## Capture redirect_uris
During discovery, leave `ALLOWED_REDIRECT_URIS` unset. The spike logs every `redirect_uri` sent by Claude. Copy the exact observed values and add them to `.env` before handing off to N-US-079.
## Stop the spike
When the spike is done (pass or fail), delete this directory or keep it on a clearly named branch. Do not merge to `develop`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing