Repository navigation
feat(mcp): OAuth 2.1 (DCR + PKCE + RFC 9728) for /api/mcp - #188
Merged
Merged
Conversation
…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>
ThreatCrush Security Scan25 finding(s) in the 10 file(s) this pull request changes. HIGH: 3 | MEDIUM: 22
98 pre-existing finding(s) elsewhere in the repository — **HIGH**: 2 | **MEDIUM**: 57 | **LOW**: 39Not introduced by this pull request.
…and 78 more; the full set is in the SARIF artifact. Snippets are redacted; ThreatCrush never prints matched credential material. |
|
|
||
| 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}`; |
| }); | ||
| 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}`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/.well-known/oauth-protected-resource(+/api/mcpsuffix),resource=https://mynaposter.com/api/mcpexactly,authorization_servers[0]=https://mynaposter.com;/.well-known/oauth-authorization-server(+/apisuffix for the old issuer) and/.well-known/openid-configuration. The site forwards these to the API (apps/web/src/proxy.ts). Issuer moves fromhttps://mynaposter.com/apito the origin;/api/.well-known/oauth-authorization-serverstill answers.POST /api/v1/oauth/register: RFC 7591, public clients only, https redirects or http loopback (any port, RFC 8252). Tableoauth_clients(idempotentcreate table if not existsin schema.sql, applied at boot)./api/v1/oauth/authorize: registered clients alongsidemyna-cli. Unknown client/redirect is shown, never followed; bad requests go back to the client witherror. Consent page names the client and its redirect host; sign-in is the existing myna cloud email+password. RFC 9207isson 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_TOKENstill works exactly as before.initialize/tools/listbuttools/callreturnsisErrorunless theirusers.idis inMYNA_MCP_OPERATORS. Without this, any myna cloud signup could post as us.mcpc_sessions) resolve only on/api/mcp;cloud.whoamirefuses them elsewhere.tools/listnow carriesresultType/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.tsnichedb forums, fails on main too).tsc --noEmitclean.Left
Tool
annotations(title,readOnlyHint/destructiveHint) are still missing on the ~60 tools; the directory requires them for review.🤖 Generated with Claude Code