feat: MCP server with an OAuth 2.1 provider #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "testing"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the MCP OAuth gap the upgrade surfaced, and builds the MCP server it
was the authentication half of. Core ships no MCP server at all — only the
route exclusions in main.ts and the workspace settings — so this is the server,
not an adapter for one.
The design is upstream's, read off the parts it does ship, rather than
invented: the four oauth_* tables specify JWT access tokens with a jti row,
hashed opaque refresh tokens and per-(user,client) grants; the consent client
in apps/client/src/ee/oauth/ fixes the four endpoints, the two scopes and the
error shape; main.ts fixes the URLs.
oauth/
rotation, RFC 7591 dynamic registration, RFC 8414 + RFC 9728 discovery.
token row, grant, user and audience on every request: a JWT cannot be
un-issued, so revocation has to be a database fact or "revoke" would mean
"revoke within the hour".
revokes every token on the grant (RFC 6819). We cannot tell a buggy client
from a stolen credential, and the honest client loses one re-authorization.
nothing until a user approves it; redirect URIs are exact-match, https or
loopback-http, no fragments.
registers a parser itself rather than patching main.ts.
mcp/
user" is a property of the object rather than a discipline.
which is its statement of the intended boundary.
controllers make, including that a child page needs edit on the parent while
a root page needs Create on the space. Content writes go through the
collaboration gateway, because page bodies are Yjs documents and a direct
write would be overwritten by any connected editor.
is on, which is what its settings panel already promises.
One bug worth recording: the first version returned
oauthScopesfrom thestrategy. Core's guard reads
user.oauth.scopes, so that would have leftuser.oauth undefined, skipped the entire scope block, and let a read-only token
satisfy every @OAuthScope('write') route. Found by reading the guard, now
pinned by core-oauth-contract.spec.ts, which also asserts the prefix exclusions
that keep /mcp and the .well-known documents at the root.
98 tests pass; check-upstream.sh now gates on all of src/ee rather than only
the contract suite. Nothing here has been run against a live client — see
VERIFICATION.md, "Check these first".
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Closes the MCP OAuth gap the upgrade surfaced, and builds the MCP server it was the authentication half of. Core ships no MCP server at all — only the route exclusions in main.ts and the workspace settings — so this is the server, not an adapter for one. The design is upstream's, read off the parts it does ship, rather than invented: the four oauth_* tables specify JWT access tokens with a jti row, hashed opaque refresh tokens and per-(user,client) grants; the consent client in apps/client/src/ee/oauth/ fixes the four endpoints, the two scopes and the error shape; main.ts fixes the URLs. oauth/ - OAuth 2.1: authorization code with mandatory PKCE (S256 only), refresh with rotation, RFC 7591 dynamic registration, RFC 8414 + RFC 9728 discovery. - OAuthStrategyService is the hook jwt.strategy requires. It re-checks the token row, grant, user and audience on every request: a JWT cannot be un-issued, so revocation has to be a database fact or "revoke" would mean "revoke within the hour". - Replay of an authorization code, or of an already-rotated refresh token, revokes every token on the grant (RFC 6819). We cannot tell a buggy client from a stolen credential, and the honest client loses one re-authorization. - Registration is open, as the RFC and MCP clients require. A client row grants nothing until a user approves it; redirect URIs are exact-match, https or loopback-http, no fragments. - Fastify parses no urlencoded bodies and OAuth mandates them, so the module registers a parser itself rather than patching main.ts. mcp/ - /mcp over streamable HTTP, stateless, one server per request so "acts as this user" is a property of the object rather than a discipline. - Nine tools mirroring the 22 core routes upstream annotated with @OAuthScope, which is its statement of the intended boundary. - Permissions are core's: same PageAccessService and CASL checks the matching controllers make, including that a child page needs edit on the parent while a root page needs Create on the space. Content writes go through the collaboration gateway, because page bodies are Yjs documents and a direct write would be overwritten by any connected editor. - Write tools are not registered on a read-only grant. - enforceMcpOauth honoured with upstream's semantics: API keys work unless it is on, which is what its settings panel already promises. One bug worth recording: the first version returned `oauthScopes` from the strategy. Core's guard reads `user.oauth.scopes`, so that would have left user.oauth undefined, skipped the entire scope block, and let a read-only token satisfy every @OAuthScope('write') route. Found by reading the guard, now pinned by core-oauth-contract.spec.ts, which also asserts the prefix exclusions that keep /mcp and the .well-known documents at the root. 98 tests pass; check-upstream.sh now gates on all of src/ee rather than only the contract suite. Nothing here has been run against a live client — see VERIFICATION.md, "Check these first". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>