Skip to content

feat(mcp): OAuth 2.1 (DCR + PKCE + RFC 9728) for /api/mcp - #188

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

ralyodio merged 1 commit into
mainfrom
mcp-oauth21

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Makes the hosted myna MCP server (https://mynaposter.com/api/mcp) listable in the Claude Connectors Directory and as a ChatGPT app: both accept only no-auth or OAuth 2.1, not a static Bearer. Same pattern as threatcrush#450, built on myna's existing CLI authorization server (apps/api/src/oauth.ts) rather than a second one.

What

  • Discovery at the origin: /.well-known/oauth-protected-resource (+ /api/mcp suffix), resource = https://mynaposter.com/api/mcp exactly, authorization_servers[0] = https://mynaposter.com; /.well-known/oauth-authorization-server (+ /api suffix for the old issuer) and /.well-known/openid-configuration. The site forwards these to the API (apps/web/src/proxy.ts). Issuer moves from https://mynaposter.com/api to the origin; /api/.well-known/oauth-authorization-server still answers.
  • POST /api/v1/oauth/register: RFC 7591, public clients only, https redirects or http loopback (any port, RFC 8252). Table oauth_clients (idempotent create table if not exists in schema.sql, applied at boot).
  • /api/v1/oauth/authorize: registered clients alongside myna-cli. Unknown client/redirect is shown, never followed; bad requests go back to the client with error. Consent page names the client and its redirect host; sign-in is the existing myna cloud email+password. RFC 9207 iss on every redirect (CLI too).
  • /api/v1/oauth/token: form (or JSON) body, authorization_code with PKCE S256 + rotating refresh_token, codes/refresh bound to the client. Device flow stays CLI-only.
  • /api/mcp: 401 + WWW-Authenticate: Bearer resource_metadata="https://mynaposter.com/.well-known/oauth-protected-resource" with no or a bad credential. MYNA_API_TOKEN still works exactly as before.
  • Operator gate: the MCP tools act on this instance's connected accounts (Profullstack's socials), not per user, so an OAuth user can initialize/tools/list but tools/call returns isError unless their users.id is in MYNA_MCP_OPERATORS. Without this, any myna cloud signup could post as us.
  • Connector tokens (mcpc_ sessions) resolve only on /api/mcp; cloud.whoami refuses them elsewhere.
  • tools/list now carries resultType/ttlMs/cacheScope.

Tests

apps/api/test/mcp-oauth.integration.test.ts (8 tests, real Postgres): discovery docs, 401 + header, operator key incl. tools/call, DCR refusals, unknown client/redirect, full PKCE flow (consent names client, code + state + iss, bad verifier, replay, MCP-only token, operator gate, refresh rotation, reuse revokes the grant), loopback any port, deny, CLI flow unchanged. Full suite: 945 pass, 1 fail (openconnection.test.ts nichedb forums, fails on main too). tsc --noEmit clean.

Left

Tool annotations (title, readOnlyHint/destructiveHint) are still missing on the ~60 tools; the directory requires them for review.

🤖 Generated with Claude Code

…n in to /api/mcp

- RFC 9728 protected-resource metadata (resource = https://mynaposter.com/api/mcp)
  and RFC 8414 / OpenID discovery at the origin, plus path-suffixed forms; the
  site forwards /.well-known/oauth-* and openid-configuration to the API
- RFC 7591 dynamic registration at /api/v1/oauth/register: public clients,
  https or loopback redirects (any port), mcpc_ client ids
- the CLI's existing /v1/oauth authorize/token/revoke now take registered
  clients too: consent names the client, RFC 9207 iss on every redirect,
  form or JSON token bodies, refresh rotation and reuse detection as before
- /api/mcp: 401 + WWW-Authenticate resource_metadata without a valid
  credential; MYNA_API_TOKEN still works; an OAuth user lists tools and runs
  them only if listed in MYNA_MCP_OPERATORS (users.id)
- connector tokens resolve only on /api/mcp (cloud.whoami mcp flag)
- tools/list carries resultType/ttlMs/cacheScope
- integration tests: discovery, 401, operator key, DCR, PKCE flow, refresh,
  loopback port, deny, CLI unchanged

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

Copy link
Copy Markdown

ThreatCrush Security Scan

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

HIGH: 3 | MEDIUM: 22

Severity Rule Location
HIGH secret-database-url README.md:1556
HIGH secret-database-url README.md:1646
HIGH js-ssrf-outbound-request apps/web/src/proxy.ts:58
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:51
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:118
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:133
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:134
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:135
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:138
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:168
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:175
MEDIUM sql-template-interpolation apps/api/src/cloud.ts:182
MEDIUM sql-template-interpolation apps/api/src/mcp-oauth.ts:201
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:201
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:255
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:313
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:318
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:334
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:338
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:341
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:343
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:346
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:378
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:382
MEDIUM sql-template-interpolation apps/api/src/oauth.ts:501
98 pre-existing finding(s) elsewhere in the repository — **HIGH**: 2 | **MEDIUM**: 57 | **LOW**: 39

Not introduced by this pull request.

Severity Rule Location
HIGH js-electron-node-integration apps/desktop/src/main.cjs:33
HIGH secret-database-url docker-compose.yml:4
MEDIUM sql-template-interpolation apps/api/src/atproto.ts:63
MEDIUM sql-template-interpolation apps/api/src/atproto.ts:101
MEDIUM sql-template-interpolation apps/api/src/atproto.ts:121
MEDIUM sql-template-interpolation apps/api/src/handoff.ts:71
MEDIUM sql-template-interpolation apps/api/src/handoff.ts:84
MEDIUM sql-template-interpolation apps/api/src/handoff.ts:85
MEDIUM sql-template-interpolation apps/api/src/handoff.ts:93
MEDIUM sql-template-interpolation apps/api/src/handoff.ts:94
MEDIUM sql-template-interpolation apps/api/src/handoff.ts:102
MEDIUM sql-template-interpolation apps/api/src/mail.ts:257
MEDIUM sql-template-interpolation apps/api/src/newsletter.ts:28
MEDIUM sql-template-interpolation apps/api/src/newsletter.ts:41
MEDIUM sql-template-interpolation apps/api/src/openconnection.ts:163
MEDIUM sql-template-interpolation apps/api/src/openconnection.ts:169
MEDIUM sql-template-interpolation apps/api/src/openconnection.ts:173
MEDIUM sql-template-interpolation apps/api/src/openconnection.ts:237
MEDIUM sql-template-interpolation apps/api/src/openconnection.ts:286
MEDIUM sql-template-interpolation apps/api/src/openconnection.ts:292

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

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread apps/api/src/mcp-oauth.ts

export async function getClient(clientId: unknown): Promise<McpClient | null> {
if (!isMcpClientId(clientId)) return null;
const rows = await db()`select client_id, client_name, redirect_uris, client_uri from oauth_clients where client_id = ${clientId}`;
Comment thread apps/api/src/oauth.ts
});
if (!result) return oauthError(context, "invalid_grant");
const reply = await db().begin((sql) => makeTokenSet(sql, result, CLI_CLIENT_ID));
if (isMcpClientId(result.clientId)) await db()`update oauth_clients set last_used_at = now() where client_id = ${result.clientId}`;
@ralyodio
ralyodio merged commit 219c6b3 into main Nov 11, 2026
4 checks passed
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.

2 participants