Skip to main content
Glama
leonguyen2020

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`.