The server is live. Time to use it from the kind of client you actually ship to users.
1. Smoke-test with the MCP Inspector
Same drill as step 3, but the URL changes to your deployed one.
npx @modelcontextprotocol/inspectorIn the connection form:
Hit Connect. The inspector detects the 401 + WWW-Authenticate and runs the full OAuth flow you debugged locally in step 5 - now against the production URL.
A few things to confirm in the network panel:
| Request | Status | What to check |
|---|---|---|
GET /.well-known/oauth-protected-resource | 200 | resource and authorization_servers are the HTTPS Render URL, not http://. If you see http://, something is mangling PUBLIC_URL - check the Environment tab in the Render Dashboard. |
POST /register | 201 | Dynamic client registration worked; the response carries a fresh client_id. |
| GitHub redirect | 302 | The browser hops through github.com/login/oauth/authorize and back to /oauth/github/callback. |
POST /token | 200 | Returns a JWT. Audience claim is https://notes-mcp-abc1.onrender.com/mcp. |
POST /mcp (with Bearer) | 200 | First successful tool list. |
Call notes.create once. Then jump to the Render Dashboard’s Logs tab and confirm the structured log line shows up with your GitHub user field:
{"level":"info","time":1718900923012,"req":{"method":"POST","url":"/mcp","id":"a1c..."},"res":{"statusCode":200},"responseTime":58,"user":"github:1234567","msg":"request completed"}That single line tells you: the request was authenticated, the user’s GitHub identity made it through, the response was fast, and the request ID gives you a thread to pull on if anything went wrong.
2. Client configs
The template’s README ships configs for the three big clients. Your config differs in exactly one way: we don’t send an Authorization: Bearer header, because the server returns the WWW-Authenticate challenge that tells the client to do OAuth on its own. The header would short-circuit that.
Open Settings -> Connectors -> Add custom connector (label moves between versions; on macOS it’s under the Claude menu).
| Field | Value |
|---|---|
| Name | notes-mcp |
| Server URL | https://notes-mcp-abc1.onrender.com/mcp |
| Authentication | OAuth (selected automatically once Claude sees the WWW-Authenticate header) |
Click Add. Claude pops the GitHub OAuth window. Approve, and the connector turns green.
Add to your project’s .cursor/mcp.json:
{ "mcpServers": { "notes-mcp": { "url": "https://notes-mcp-abc1.onrender.com/mcp" } }}No headers block - Cursor sees the 401 + WWW-Authenticate and runs the OAuth flow.
codex mcp add --transport streamable-http \ --url https://notes-mcp-abc1.onrender.com/mcp \ notes-mcpOr declare it in .codex/config.toml:
[mcp_servers.notes-mcp]url = "https://notes-mcp-abc1.onrender.com/mcp"Same pattern: no header block, OAuth handled by the client.
3. Drive it from Claude
Ask Claude something that needs the tool:
Save a note titled "Render MCP tutorial" with the body"Connected from Claude Desktop, end-to-end OAuth works."Then read back the 3 most recent notes.You’ll see Claude pick notes.create from its tool list, run it, then read notes://recent and summarize. Two MCP calls; both authenticated; both logged with your GitHub identity in the Render logs.
4. Inspect what’s reaching the server
This is a good moment to learn the Render logs query syntax. Two queries you’ll want to keep in your back pocket:
service:notes-mcp user:"github:1234567" url:/mcpservice:notes-mcp level:errorThe user: filter only works because you wired customProps to include sub on every log line in step 6. Without that, you’d be grepping JWT payloads by hand.
5. End-to-end picture
sequenceDiagram
participant U as You (GitHub login)
participant C as Claude Desktop
participant R as notes-mcp.onrender.com
participant DB as Render Postgres<br/>(private)
C->>R: POST /mcp (no auth)
R-->>C: 401 + WWW-Authenticate
C->>R: discovery + dynamic registration
C->>U: Open browser -> GitHub OAuth
U-->>R: callback with code
R-->>C: redirect with the code
C->>R: POST /token (PKCE)
R-->>C: JWT (aud:.../mcp)
C->>R: POST /mcp tools/call notes.create<br/>+ Bearer JWT
R->>DB: INSERT INTO notes
R-->>C: { structuredContent: note }
C->>R: POST /mcp resources/read notes://recent<br/>+ Bearer JWT
R->>DB: SELECT... ORDER BY created_at DESC
R-->>C: list of recent notes
Everything between Claude and Render is over public HTTPS with OAuth. Everything between Render and Postgres is on the private network. Each leg is encrypted, and the database has no public surface to attack.
6. What you’ve shipped
Take stock - this is the real list of features behind the deployed URL:
- Render’s MCP template as a foundation, extended with a real tool surface.
- Streamable HTTP transport in stateless mode (horizontally scalable).
- OAuth 2.1 with dynamic client registration, PKCE, and short-lived audience-restricted JWTs.
- GitHub as the identity layer - no passwords stored, no user table to leak.
- Postgres persistence behind a clean interface, with TLS and no public network exposure.
- Per-identity rate limiting so one runaway client can’t take you down.
- Structured JSON logs with request IDs, redacted secrets, and a queryable
userfield. - DB-backed
/healthso Render’s load balancer routes around degraded instances. - One Blueprint that provisions every piece reproducibly.
That’s roughly the production envelope you’d find in a paid MCP service, sitting on Render’s Starter plan.
What you learned
- Same OAuth flow you debugged locally just works against the deployed URL - nothing changes except `PUBLIC_URL`
- Claude Desktop, Cursor, and Codex all speak Streamable HTTP + OAuth; the template's README configs work with the `Authorization` header removed
- Structured logs + a `user` field make `service:notes-mcp user:"github:..."` a useful single-query debug tool
- End-to-end: public HTTPS + OAuth from client to Render, private network from Render web to Postgres