Conversation
Adds /internal as an OAuth-protected MCP endpoint for Claude Desktop/Cowork users, authenticated via Google SSO restricted to @sentry.io emails. Uses a shared team Plausible API key for OAuth users. - New src/auth-handler.ts: Google OAuth flow with @sentry.io domain check - worker.ts: OAuthProvider wraps /internal (OAuth) while /mcp stays as direct Bearer token for CLI users (backward compatible) - wrangler.toml: KV namespace for OAuth state, pinned dev port - New dependency: @cloudflare/workers-oauth-provider Deploy requires: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, PLAUSIBLE_API_KEY secrets and a KV namespace for OAUTH_KV. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap internalApiHandler and defaultHandler with Sentry.withSentry() since OAuthProvider export can't be wrapped directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| # Local dev uses --local which simulates KV automatically | ||
| [[kv_namespaces]] | ||
| binding = "OAUTH_KV" | ||
| id = "PLACEHOLDER_REPLACE_BEFORE_DEPLOY" |
There was a problem hiding this comment.
Bug: A placeholder KV namespace ID in wrangler.toml combined with non-defensive code in auth-handler.ts will cause runtime crashes, as env.OAUTH_KV will be uninitialized.
Severity: HIGH
Suggested Fix
Replace the placeholder ID in wrangler.toml with a valid KV namespace ID before deployment. As a defensive measure, add a runtime check in auth-handler.ts to verify that env.OAUTH_KV is defined before it is used, and throw a clear error if it is not.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: wrangler.toml#L18
Potential issue: The `wrangler.toml` file contains a placeholder value for the
`OAUTH_KV` namespace ID. The code in `auth-handler.ts` directly accesses `env.OAUTH_KV`
to perform `put`, `get`, and `delete` operations without checking if the binding is
valid or defined. If the worker is deployed with this placeholder, the `env.OAUTH_KV`
binding will likely be invalid or `undefined`. Consequently, any attempt to initiate the
OAuth flow will trigger a runtime error, such as `TypeError: Cannot read property 'put'
of undefined`, when the code tries to access the non-existent KV namespace, breaking the
authentication flow.
Did we get this right? 👍 / 👎 to inform future reviews.
| if (user.hd !== "sentry.io" || !user.email.endsWith("@sentry.io")) { | ||
| return new Response("Access restricted to @sentry.io accounts", { | ||
| status: 403, |
There was a problem hiding this comment.
Bug: The authentication logic incorrectly uses an || operator, which will deny access to legitimate users with @sentry.io emails if they use personal Google accounts.
Severity: MEDIUM
Suggested Fix
Change the logical operator from || (OR) to && (AND) in the if condition. A simpler and more robust fix would be to remove the check for user.hd and only validate that user.email.endsWith("@sentry.io") is true.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/auth-handler.ts#L106-L108
Potential issue: The authentication logic at `src/auth-handler.ts:106` incorrectly uses
an `||` (OR) operator to validate a user's domain. The condition `user.hd !==
"sentry.io" || !user.email.endsWith("@sentry.io")` will incorrectly deny access to users
with a valid `@sentry.io` email address if they authenticate using a personal Google
account, because the `user.hd` field will be `undefined`. An `undefined` `hd` field
causes the first part of the condition to evaluate to `true`, leading to an access
denied error for a legitimate user.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a2f4dfa. Configure here.
| defaultSiteId: env.PLAUSIBLE_DEFAULT_SITE_ID, | ||
| }); | ||
|
|
||
| const response = await createMcpHandler(server)(request, env, ctx); |
There was a problem hiding this comment.
MCP handler path mismatch causes /internal endpoint 404
High Severity
createMcpHandler(server) defaults to matching the /mcp path, but OAuthProvider routes requests to internalApiHandler at the /internal path (configured via apiRoute: "/internal"). Since /internal doesn't match the default /mcp route, the MCP handler will always return 404 for OAuth-authenticated requests. The createMcpHandler call needs a { route: "/internal" } option to match the actual request path.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a2f4dfa. Configure here.
| const response = await createMcpHandler(server)(request, env, ctx); | ||
| return corsResponse(response); | ||
| }, | ||
| } satisfies ExportedHandler<Env>); |
There was a problem hiding this comment.
OAuth endpoint missing rate limiting unlike Bearer endpoint
Medium Severity
The internalApiHandler for the OAuth-protected /internal endpoint has no rate limiting, while handleDirectMcp applies per-IP rate limiting via env.RATE_LIMITER. Since /internal uses a shared PLAUSIBLE_API_KEY, unthrottled usage by any authenticated user could exhaust Plausible API rate limits, impacting all OAuth users.
Reviewed by Cursor Bugbot for commit a2f4dfa. Configure here.


Summary
/internalas OAuth 2.1-protected MCP endpoint for Claude Desktop/Cowork users@sentry.ioemails/mcpremains unchanged for direct Bearer token CLI users (backward compatible)@cloudflare/workers-oauth-providerfor OAuth 2.1 server with KV-backed token storageArchitecture
Deploy checklist
wrangler kv namespace create plausible-mcp-oauthwrangler.tomlKVidwith actual namespace IDGOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,PLAUSIBLE_API_KEYhttps://plausible-mcp.sentry.dev/callbackTest plan
/mcpdirect Bearer flow still works (error messages verified)🤖 Generated with Claude Code