Skip to main content
Glama

Sentry MCP

Official
by getsentry
helpers.ts8.21 kB
import type { TokenExchangeCallbackOptions, TokenExchangeCallbackResult, } from "@cloudflare/workers-oauth-provider"; import type { z } from "zod"; import { logIssue } from "@sentry/mcp-server/telem/logging"; import { TokenResponseSchema, SENTRY_TOKEN_URL } from "./constants"; import type { WorkerProps } from "../types"; import * as Sentry from "@sentry/cloudflare"; /** * Constructs an authorization URL for Sentry. */ export function getUpstreamAuthorizeUrl({ upstream_url, client_id, scope, redirect_uri, state, }: { upstream_url: string; client_id: string; scope: string; redirect_uri: string; state?: string; }) { const upstream = new URL(upstream_url); upstream.searchParams.set("client_id", client_id); upstream.searchParams.set("redirect_uri", redirect_uri); upstream.searchParams.set("scope", scope); if (state) upstream.searchParams.set("state", state); upstream.searchParams.set("response_type", "code"); return upstream.href; } /** * Exchanges an authorization code for an access token from Sentry. */ export async function exchangeCodeForAccessToken({ client_id, client_secret, code, upstream_url, redirect_uri, }: { code: string | undefined; upstream_url: string; client_secret: string; client_id: string; redirect_uri?: string; }): Promise<[z.infer<typeof TokenResponseSchema>, null] | [null, Response]> { if (!code) { const eventId = logIssue("[oauth] Missing code in token exchange", { oauth: { client_id, }, }); return [ null, new Response("Invalid request: missing authorization code", { status: 400, headers: { "X-Event-ID": eventId ?? "" }, }), ]; } const resp = await fetch(upstream_url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Sentry MCP Cloudflare", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id, client_secret, code, // binding the redirect_uri creates an unsupported_grant_type error // ...(redirect_uri ? { redirect_uri } : {}), }).toString(), }); if (!resp.ok) { const eventId = logIssue( `[oauth] Failed to exchange code for access token: ${await resp.text()}`, { oauth: { client_id, }, }, ); return [ null, new Response( "There was an issue authenticating your account and retrieving an access token. Please try again.", { status: 400, headers: { "X-Event-ID": eventId ?? "" } }, ), ]; } try { const body = await resp.json(); const output = TokenResponseSchema.parse(body); return [output, null]; } catch (e) { const eventId = logIssue( new Error("Failed to parse token response", { cause: e, }), { oauth: { client_id, }, }, ); return [ null, new Response( "There was an issue authenticating your account and retrieving an access token. Please try again.", { status: 500, headers: { "X-Event-ID": eventId ?? "" } }, ), ]; } } /** * Refreshes an access token using a refresh token from Sentry. */ export async function refreshAccessToken({ client_id, client_secret, refresh_token, upstream_url, }: { refresh_token: string | undefined; upstream_url: string; client_secret: string; client_id: string; }): Promise<[z.infer<typeof TokenResponseSchema>, null] | [null, Response]> { if (!refresh_token) { const eventId = logIssue("[oauth] Missing refresh token in token refresh", { oauth: { client_id, }, }); return [ null, new Response("Invalid request: missing refresh token", { status: 400, headers: { "X-Event-ID": eventId ?? "" }, }), ]; } const resp = await fetch(upstream_url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Sentry MCP Cloudflare", }, body: new URLSearchParams({ grant_type: "refresh_token", client_id, client_secret, refresh_token, }).toString(), }); if (!resp.ok) { const eventId = logIssue( `[oauth] Failed to refresh access token: ${await resp.text()}`, { oauth: { client_id, }, }, ); return [ null, new Response( "There was an issue refreshing your access token. Please re-authenticate.", { status: 400, headers: { "X-Event-ID": eventId ?? "" } }, ), ]; } try { const body = await resp.json(); const output = TokenResponseSchema.parse(body); return [output, null]; } catch (e) { const eventId = logIssue( new Error("Failed to parse refresh token response", { cause: e, }), { oauth: { client_id, }, }, ); return [ null, new Response( "There was an issue refreshing your access token. Please re-authenticate.", { status: 500, headers: { "X-Event-ID": eventId ?? "" } }, ), ]; } } /** * Token exchange callback for handling Sentry OAuth token refreshes. */ export async function tokenExchangeCallback( options: TokenExchangeCallbackOptions, env: { SENTRY_CLIENT_ID: string; SENTRY_CLIENT_SECRET: string; SENTRY_HOST?: string; }, ): Promise<TokenExchangeCallbackResult | undefined> { // Only handle refresh_token grant type if (options.grantType !== "refresh_token") { return undefined; // No-op for other grant types } Sentry.setUser({ id: options.props.id }); // Extract the refresh token from the stored props const currentRefreshToken = options.props.refreshToken; if (!currentRefreshToken) { logIssue("No refresh token available in stored props", { loggerScope: ["cloudflare", "oauth", "refresh"], }); return undefined; } try { // If we have a cached upstream expiry, and there's ample time left, // avoid calling upstream to reduce unnecessary refreshes. // Mint a new provider token with the remaining TTL. const props = options.props as WorkerProps; const maybeExpiresAt = props.accessTokenExpiresAt; if (maybeExpiresAt && Number.isFinite(maybeExpiresAt)) { const remainingMs = maybeExpiresAt - Date.now(); const SAFE_WINDOW_MS = 2 * 60 * 1000; // 2 minutes safety window if (remainingMs > SAFE_WINDOW_MS) { const remainingSec = Math.floor(remainingMs / 1000); return { newProps: { ...options.props }, accessTokenTTL: remainingSec, }; } } // Construct the upstream token URL for Sentry const upstreamTokenUrl = new URL( SENTRY_TOKEN_URL, `https://${env.SENTRY_HOST || "sentry.io"}`, ).href; // Use our refresh token function to get new tokens from Sentry const [tokenResponse, errorResponse] = await refreshAccessToken({ client_id: env.SENTRY_CLIENT_ID, client_secret: env.SENTRY_CLIENT_SECRET, refresh_token: currentRefreshToken, upstream_url: upstreamTokenUrl, }); if (errorResponse) { // Convert the Response to an Error for the OAuth provider const errorText = await errorResponse.text(); throw new Error( `Failed to refresh upstream token in OAuth provider: ${errorText}`, ); } if (!tokenResponse.refresh_token) { logIssue("[oauth] Upstream refresh response missing refresh_token", { loggerScope: ["cloudflare", "oauth", "refresh"], }); return undefined; } // Return the updated props with new tokens and TTL return { // This updates ctx.props newProps: { ...options.props, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token, accessTokenExpiresAt: Date.now() + tokenResponse.expires_in * 1000, }, accessTokenTTL: tokenResponse.expires_in, }; } catch (error) { logIssue(error); throw new Error("Failed to refresh upstream token in OAuth provider", { cause: error, }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/getsentry/sentry-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server