Authentication and security patterns for the Sentry MCP server.
The remote HTTP deployment uses two separate authorization layers:
MCP Client → MCP OAuth (our server) → Sentry OAuth → Sentry API
This is not just "OAuth to Sentry, then proxy requests back." The Cloudflare deployment issues its own MCP access token to the client, and that token wraps the upstream Sentry credentials the server needs to make API calls.
-
Upstream Sentry token
- Issued by Sentry's OAuth server
- Stored inside the MCP grant props
- Never sent directly to the MCP client
- Used only by the MCP server when calling Sentry's REST API
-
Downstream MCP token
- Issued by our OAuth provider (
@cloudflare/workers-oauth-provider) - Sent to the MCP client
- Used to authenticate requests to
/mcp - Carries encrypted props such as the upstream Sentry token, granted skills, and optional org/project constraints
- Issued by our OAuth provider (
- The MCP client registers with our OAuth provider and starts authorization.
- Our approval UI collects the MCP-side permissions for the session.
- We redirect the user to Sentry OAuth.
- Sentry returns an authorization code to our
/oauth/callback. - We exchange that code for a Sentry access token and refresh token.
- We issue a downstream MCP token to the client and store the upstream Sentry credentials in its encrypted props.
- On
/mcprequests, the worker validates the downstream MCP token, reconstructsServerContext, and uses the upstream Sentry token to call Sentry APIs.
The important design point is that the upstream token and downstream token do not mean the same thing.
When we redirect to Sentry OAuth, we always request the shared Sentry scope set
defined in packages/mcp-core/src/scopes.ts:
org:read project:write team:write event:write
We ask Sentry for this broader shared token because:
- Sentry OAuth scopes are coarse compared to our MCP capability model.
- A single upstream token must support every tool that could be enabled for the granted MCP skills.
- The worker reuses the same upstream token across later MCP token refreshes and MCP requests for that grant.
This token is therefore a server-side capability token for talking to Sentry, not the final permission boundary presented to the MCP client.
The final token returned to the MCP client is more restrictive in practice. It captures three separate kinds of restriction:
-
OAuth scope requested by the MCP client
- Stored as
scopeon the MCP grant/token props - Represents the downstream OAuth grant made to the MCP client
- Useful as part of the wrapper-token contract even though runtime tool authorization is primarily skill-based today
- Stored as
-
Granted MCP skills
- Stored as
grantedSkills - This is the primary authorization mechanism for tool exposure in
mcp-core - Tools are registered only when their skill set is enabled
- Stored as
-
Optional resource constraints
- Derived from the OAuth
resourceparameter for/mcp,/mcp/:org, or/mcp/:org/:project - Stored as
constraintOrganizationSlugandconstraintProjectSlug - Enforced on every request so a token minted for one scoped MCP URL cannot be reused against a broader path
- Derived from the OAuth
Today, runtime authorization for remote HTTP sessions is driven primarily by:
grantedSkillsfor which tools the session can access- path constraints for which org/project the session can access
- Sentry's own upstream bearer-token checks for whether the user can perform the underlying API operation
grantedScopes still exists on tokens for backward compatibility with older
clients, but it is transitional. Skills are the primary authorization model.
The remote deployment intentionally separates trust boundaries:
-
Client-to-MCP trust boundary
- The client only receives an MCP token
- The client does not receive raw Sentry OAuth credentials
-
MCP-to-Sentry trust boundary
- Only the server uses the upstream Sentry bearer token
- All Sentry API access happens server-side
-
Session narrowing
- Broad upstream Sentry scopes are narrowed by MCP-side skills and URL constraints
- A client may hold a valid MCP token but still be unable to access tools or resources outside the granted session shape
-
Revocation on upstream failure
- If Sentry starts rejecting the stored upstream token, the MCP grant is treated as stale
- Future requests are forced back through re-authorization instead of silently continuing with an invalid wrapper token
- Sentry credentials are server-held: MCP clients never need direct Sentry API tokens.
- Authorization is layered: Sentry scopes, MCP skills, and MCP resource constraints each narrow access differently.
- Each session can be path-scoped:
/mcp/:organd/mcp/:org/:projectproduce tokens that only work for that scoped MCP URL. - Refresh does not widen access: MCP refresh reuses the same stored grant props and does not ask Sentry for broader permissions.
- Stale or invalid grants fail closed: legacy grants, missing props, or rejected upstream tokens are revoked or require re-authentication.
-
OAuth Provider (@cloudflare/workers-oauth-provider)
- Manages client authorization
- Stores tokens in KV storage
- Handles state management
- Sets auth props in ExecutionContext
-
Client Approval
- First-time clients require user approval
- Approved clients stored in signed cookies
- Can surface session scope for constrained
/mcp/...URLs
-
Token Management
- Access tokens encrypted in KV storage
- MCP refresh reuses cached Sentry access tokens while they remain valid
- Tokens can be constrained to organization/project paths
See implementation: packages/mcp-cloudflare/src/server/oauth/authorize.ts and packages/mcp-cloudflare/src/server/oauth/callback.ts
Key endpoints:
/authorize- Client approval and redirect to Sentry/callback- Handle Sentry callback, store tokens/approve- Process user approval
interface ServerContext {
userId?: string;
clientId: string;
accessToken: string;
grantedSkills: Set<Skill>; // Primary authorization method
// grantedScopes is deprecated and will be removed Jan 1, 2026
constraints: Constraints;
sentryHost: string;
mcpUrl?: string;
}Context captured in closures during server build and propagated through:
- Tool handlers (via closure capture and direct parameter passing)
- API client initialization
- Error messages (sanitized)
The MCP server validates regionUrl parameters to prevent Server-Side Request Forgery (SSRF) attacks:
// Region URL validation rules:
// 1. By default, only the base host itself is allowed as regionUrl
// 2. Additional domains must be in SENTRY_ALLOWED_REGION_DOMAINS allowlist
// 3. Must use HTTPS protocol for security
// 4. Empty/undefined regionUrl means use the base host
// Base host always allowed
validateRegionUrl("https://sentry.io", "sentry.io"); // ✅ Base host match
validateRegionUrl("https://mycompany.com", "mycompany.com"); // ✅ Base host match
// Allowlist domains (sentry.io, us.sentry.io, de.sentry.io)
validateRegionUrl("https://us.sentry.io", "sentry.io"); // ✅ In allowlist
validateRegionUrl("https://de.sentry.io", "mycompany.com"); // ✅ In allowlist
validateRegionUrl("https://sentry.io", "mycompany.com"); // ✅ In allowlist
// Rejected domains
validateRegionUrl("https://evil.com", "sentry.io"); // ❌ Not in allowlist
validateRegionUrl("http://us.sentry.io", "sentry.io"); // ❌ Must use HTTPS
validateRegionUrl("https://eu.sentry.io", "sentry.io"); // ❌ Not in allowlist
validateRegionUrl("https://sub.mycompany.com", "mycompany.com"); // ❌ Not base host or allowlistImplementation: packages/mcp-server/src/internal/tool-helpers/validate-region-url.ts
Tools that accept user input are vulnerable to prompt injection attacks. Key mitigations:
- Parameter Validation: All tool inputs validated with Zod schemas
- URL Validation: URLs parsed and validated before use
- Region Constraints: Region URLs restricted to known Sentry domains
- No Direct Command Execution: Tools don't execute user-provided commands
Example protection in tools:
// URLs must be valid and from expected domains
if (!issueUrl.includes('sentry.io')) {
throw new UserInputError("Invalid Sentry URL");
}
// Region URLs validated against base host
const validatedHost = validateRegionUrl(regionUrl, baseHost);The OAuth state is a compact HMAC-signed payload with a 10‑minute expiry:
// Payload contains only what's needed on callback
type OAuthState = {
clientId: string;
redirectUri: string; // must be a valid URL
scope: string[]; // from OAuth provider parseAuthRequest
permissions?: string[]; // user selections from approval
iat: number; // issued at (ms)
exp: number; // expires at (ms)
};
// Sign: `${hex(hmacSHA256(json))}.${btoa(json)}` using COOKIE_SECRET
const signed = `${signatureHex}.${btoa(JSON.stringify(state))}`;
// On callback: split, verify signature, parse, check exp > Date.now()Implementation: packages/mcp-cloudflare/src/server/oauth/state.ts
All user inputs sanitized:
- HTML content escaped
- URLs validated
- OAuth parameters verified
// Signed cookie for approved clients
const cookie = await signCookie(
`approved_clients=${JSON.stringify(approvedClients)}`,
COOKIE_SECRET
);
// Cookie attributes
"HttpOnly; Secure; SameSite=Lax; Max-Age=2592000" // 30 daysSecurity-aware error responses:
- No token/secret exposure in errors
- Generic messages for auth failures
- Detailed logging server-side only
catch (error) {
if (error.message.includes("token")) {
return new Response("Authentication failed", { status: 401 });
}
// Log full error server-side
console.error("OAuth error:", error);
return new Response("An error occurred", { status: 500 });
}- Tokens may be scoped to organizations or projects via the OAuth
resourceparameter - Users can switch organizations
- Each organization requires separate approval
// Verify organization access
const orgs = await apiService.listOrganizations();
if (!orgs.find(org => org.slug === requestedOrg)) {
throw new UserInputError("No access to organization");
}Required for OAuth:
SENTRY_CLIENT_ID=your_oauth_app_id
SENTRY_CLIENT_SECRET=your_oauth_app_secret
COOKIE_SECRET=random_32_char_string// Allowed origins for OAuth flow
const ALLOWED_ORIGINS = [
"https://sentry.io",
"https://*.sentry.io"
];- OAuth implementation:
packages/mcp-cloudflare/src/server/oauth/* - Cookie utilities:
packages/mcp-cloudflare/src/server/utils/cookies.ts - OAuth Provider:
packages/mcp-cloudflare/src/server/bindings.ts - Sentry OAuth docs: https://docs.sentry.io/api/guides/oauth/