Skip to content

feat(oauth): dynamic client registration so claude.ai and ChatGPT can connect to /mcp - #428

Merged
ralyodio merged 1 commit into
mainfrom
mcp-oauth-dcr
Nov 11, 2026
Merged

ralyodio merged 1 commit into
mainfrom
mcp-oauth-dcr

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

nixamp.com/mcp and backtoschool.help/mcp already did OAuth 2.1 + PKCE S256 (nixamp.com is the AS for both), but only built-in clients could sign in, so neither could be added as a Claude Connectors Directory or ChatGPT connector.

What

  • RFC 7591 DCR at /api/v1/oauth/register, and POST /register (the default path) now answers JSON instead of the web app. Public clients only (token_endpoint_auth_method: none); redirects must be https (no fragment/credentials) or http on localhost/127.0.0.1 with any port, which covers https://claude.ai/api/mcp/auth_callback, https://chatgpt.com/connector_platform_oauth_redirect and http://localhost:*/callback. Rows in nixamp_oauth_clients (created on first use like the other OAuth tables; no manual migration).
  • AS metadata: registration_endpoint, authorization_response_iss_parameter_supported; iss (RFC 9207) on every authorization response.
  • A registered connector that asks for no scope gets profile parties streams stations offline_access (not email) and always a rotating refresh token; access tokens stay 1 h.
  • PRM: resource is now exactly the MCP URL (https://nixamp.com/mcp, https://backtoschool.help/mcp), also at /.well-known/oauth-protected-resource/mcp.
  • Consent: a signed-out browser signs in on the page and returns to the consent screen (which names the client); OAuth pages send frame-ancestors 'none' / X-Frame-Options: DENY.
  • Tools: every tool (nixamp, public directory, school) has title + readOnly/destructive/openWorld hints (src/mcp-annotations.ts, test keeps the table complete); tools/list carries resultType/ttlMs/cacheScope.

Existing nixamp tokens (CLI/session) and the built-in bittorrented/backtoschool clients are unchanged.

Tests

  • test/oauth-server.test.ts: metadata, DCR (accepts claude/chatgpt/loopback, refuses confidential, plain http, fragment, bad grants; /register JSON), full connector flow (sign-in page, consent names client, loopback port varies, code+state+iss, form-encoded exchange, refresh w/o offline_access, rotation, connections list, unknown client is a page), /mcp 401 + PRM at both paths + OAuth token and old token both list annotated tools.
  • test/school.test.ts: backtoschool PRM resource + annotated tools + cache fields. test/mcp-annotations.test.ts: no tool unannotated.
  • Full suite: 1151 pass, 2 fail (nixamp party / nixamp dns without a session: environment-dependent CLI tests, nixamp party fails identically on main).

🤖 Generated with Claude Code

… connect to /mcp

nixamp.com/mcp and backtoschool.help/mcp already used OAuth 2.1 + PKCE
S256 with nixamp.com as the authorization server, but only built-in clients
could sign in, so neither server could be added as a Claude or ChatGPT
connector.

- RFC 7591 registration at /api/v1/oauth/register (and POST /register, the
  default path, which now answers JSON instead of falling through to the
  web app): public clients only (token_endpoint_auth_method none), https
  redirects or http on localhost/127.0.0.1 with any port. Stored in
  nixamp_oauth_clients (created on first use, like the other OAuth tables).
- AS metadata advertises registration_endpoint and
  authorization_response_iss_parameter_supported; every authorization
  response (code, error, access_denied) carries iss (RFC 9207).
- A registered connector asking for no scope gets profile parties streams
  stations offline_access, and always a rotating refresh token (claude.ai
  and ChatGPT never ask for offline_access). Access tokens stay 1 h.
- Protected-resource metadata: `resource` is now exactly the MCP URL
  (https://nixamp.com/mcp, https://backtoschool.help/mcp), also served at
  /.well-known/oauth-protected-resource/mcp.
- Consent: not signed in now signs in on the page itself and reloads to the
  consent screen, which names the client; the pages refuse framing.
- Every tool (nixamp, public directory, school) has a title and
  readOnly/destructive/openWorld hints; tools/list carries resultType,
  ttlMs and cacheScope.

Existing nixamp tokens (CLI, session) and the built-in bittorrented and
backtoschool clients work as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

12 finding(s) in the 10 file(s) this pull request changes.

HIGH: 11 | MEDIUM: 1

Severity Rule Location
HIGH js-host-header-trust src/server.ts:3130
HIGH js-host-header-trust src/server.ts:3144
HIGH js-host-header-trust src/server.ts:3161
HIGH js-host-header-trust src/server.ts:3170
HIGH js-host-header-trust src/server.ts:3175
HIGH js-host-header-trust src/server.ts:3278
HIGH js-host-header-trust src/server.ts:3344
HIGH js-host-header-trust src/server.ts:3431
HIGH js-host-header-trust src/server.ts:3459
HIGH js-host-header-trust src/server.ts:3629
HIGH js-host-header-trust src/server.ts:3645
MEDIUM redos-nested-quantifier src/server.ts:4842
44 pre-existing finding(s) elsewhere in the repository — **HIGH**: 6 | **MEDIUM**: 14 | **LOW**: 24

Not introduced by this pull request.

Severity Rule Location
HIGH secret-database-url .github/workflows/translation.yml:31
HIGH secret-generic-credential src/hls.ts:81
HIGH js-ssrf-outbound-request src/music-hub.ts:304
HIGH js-ssrf-outbound-request web/src/app.ts:115
HIGH js-ssrf-outbound-request web/src/app.ts:163
HIGH js-ssrf-outbound-request web/src/app.ts:167
MEDIUM js-unescaped-html-sink backtoschool/src/main.ts:95
MEDIUM js-unescaped-html-sink backtoschool/src/main.ts:241
MEDIUM js-unescaped-html-sink backtoschool/src/main.ts:257
MEDIUM js-unescaped-html-sink backtoschool/src/main.ts:281
MEDIUM js-unescaped-html-sink backtoschool/src/main.ts:366
MEDIUM js-unescaped-html-sink backtoschool/src/main.ts:494
MEDIUM js-unescaped-html-sink web/src/app.ts:391
MEDIUM js-unescaped-html-sink web/src/app.ts:392
MEDIUM js-timing-unsafe-mac-compare web/src/app.ts:2454
MEDIUM js-open-redirect web/src/app.ts:3869
MEDIUM js-unescaped-html-sink web/src/app.ts:7376
MEDIUM js-unescaped-html-sink web/src/control-icons.ts:19
MEDIUM insecure-randomness-for-secret web/src/music.ts:419
MEDIUM insecure-randomness-for-secret web/src/station.ts:314

…and 24 more; the full set is in the SARIF artifact.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio

Copy link
Copy Markdown
Contributor Author

Local evidence (CI here runs scans only, no test job): bun x tsc -p tsconfig.json --noEmit clean; bun test test web/test 1151 pass / 9 skip / 2 fail. The 2 failures are environment-dependent CLI tests (nixamp party / nixamp dns without a session); nixamp party fails the same way on main. New/changed: oauth-server.test.ts 25/25, school.test.ts 4/4, mcp-annotations.test.ts 2/2, transcript-routes.test.ts pass.

@ralyodio
ralyodio merged commit 9bd0348 into main Nov 11, 2026
5 checks passed
@ralyodio
ralyodio deleted the mcp-oauth-dcr branch November 11, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant