Add OAuth 2.1 to the MCP server#61
Merged
Merged
Conversation
The /api/v1/mcp endpoint previously accepted only a static bb_studio_ API key and returned a bare 401. Claude Code can inject a static header, so it connected fine; Claude Desktop's remote connector needs an OAuth login flow (it has no static-token field), so it could not. Both clients already speak Streamable HTTP — the gap was authentication, not transport. This adds an OAuth 2.1 Authorization Server (django-oauth-toolkit) and makes the MCP endpoint accept OAuth tokens alongside the existing API keys: - apps/oauth_server: Dynamic Client Registration (RFC 7591), RFC 8414/9728 discovery metadata, and an S256-only PKCE validator. - McpAuth (apps/api/auth.py): bb_studio_ tokens take the existing key path unchanged; any other bearer resolves a DOT access token, maps the user to their active workspace, and runs the handlers via an OAuthMcpActor shim carrying that user's own permissions. - A 401 + WWW-Authenticate challenge on the MCP path to start the OAuth flow. - ApiKeyAuditLog gains a nullable api_key plus actor_user/actor_label so OAuth calls are audited against the user. Any Studio user can connect; they act only with their own workspace permissions (read-only roles get read tools; writes require the matching permission). Mirrors social-intelligence-app's OAuth server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
What does this PR do?
Adds OAuth 2.1 support to the MCP server (
POST /api/v1/mcp/) so it can be connected from Claude Desktop (and other native OAuth MCP connectors), not just Claude Code. Existingbb_studio_API keys keep working unchanged.apps/oauth_server/(new) — an OAuth 2.1 Authorization Server ondjango-oauth-toolkit: Dynamic Client Registration (RFC 7591,POST /oauth/register), RFC 8414/9728 discovery documents (/.well-known/oauth-authorization-server,/.well-known/oauth-protected-resource[/api/v1/mcp]), and an S256-only PKCE validator.McpAuth(apps/api/auth.py) — attached only to the MCP router. Abb_studio_token takes the existingApiKeyAuthpath verbatim; any other bearer is resolved as a DOT access token (by indexedtoken_checksum+is_valid(['mcp'])), mapped to the user's active workspace, and exposed to the handlers via anOAuthMcpActorshim that carries that user's own workspace permissions.WWW-Authenticateon the MCP path (apps/api/api.py) — the challenge header that triggers Desktop's OAuth login.ApiKeyAuditLog.api_keyis now nullable, withactor_user/actor_labeladded so OAuth calls are attributed to the user (migration0003).README.md,.env.example) + tests.Why?
The MCP server only accepted a static
Authorization: Bearer bb_studio_…key and returned a bare401. Claude Code can inject a static header (claude mcp add --header …), so it connected; Claude Desktop's remote-connector flow has no static-token field — it reads.well-knowndiscovery metadata and runs a browser OAuth login, kicked off by a401 + WWW-Authenticate. Studio emitted neither.Both clients already speak Streamable HTTP, so the gap was authentication, not transport. This mirrors the approach already proven in the sibling
social-intelligence-apprepo.Access model: any Studio user can connect; they act only with their own
WorkspaceMembershippermissions (read-only roles get the read tools; create/schedule/upload require the matching permission), operating on their last-active workspace.How to test
Live (with the dev server running):
GET /.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource/api/v1/mcp→ 200 JSON.POST /api/v1/mcp/with no auth →401carryingWWW-Authenticate: Bearer resource_metadata="…".POST /api/v1/mcp/withAuthorization: Bearer bb_studio_…→200(no regression).POST /oauth/registerwith an httpsredirect_uris→201(public client).End-to-end (needs Studio at a public https URL): Claude Desktop → Settings → Connectors → Add custom connector →
https://<studio>/api/v1/mcp→ browser login → tools appear.Reviewer notes
OAuthMcpActor(apps/api/auth.py) duck-types theApiKeysurface the handlers read (no DB row).apps/mcp/tests/test_oauth_auth.pyincludes a drift sentinel that drives every registered tool over the OAuth path so a future handler reading an un-shimmed attribute fails CI.AuthenticationErrorhandler normalizes the 401 body for the whole Agent API (REST included) from Ninja's default{"detail": "Unauthorized"}to{"error": "unauthorized", "detail": "…"}(matches the existingHttpErrorenvelope). TheWWW-Authenticateheader is MCP-path-scoped only.MCP_PUBLIC_BASE_URL/MCP_OAUTH_ISSUER_URLdefault toAPP_URL(Studio is single-host); override only for a split app/api deployment.django-oauth-toolkit>=3.0,<4.0dependency.Checklist
pytest) — 184 passedruff check .andruff format --check .)