Skip to main content
Glama

Sentry MCP

Official
by getsentry
mcp-handler.ts6.13 kB
/** * MCP Handler using experimental_createMcpHandler from Cloudflare agents library. * * Stateless request handling approach: * - Uses experimental_createMcpHandler to wrap the MCP server * - Extracts auth props directly from ExecutionContext (set by OAuth provider) * - Context captured in tool handler closures during buildServer() * - No session state required - each request is independent */ import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp"; import { buildServer } from "@sentry/mcp-server/server"; import { expandScopes, parseScopes, type Scope, } from "@sentry/mcp-server/permissions"; import { parseSkills, type Skill } from "@sentry/mcp-server/skills"; import { logIssue, logWarn } from "@sentry/mcp-server/telem/logging"; import type { ServerContext } from "@sentry/mcp-server/types"; import type { Env } from "../types"; import { verifyConstraintsAccess } from "./constraint-utils"; import type { ExportedHandler } from "@cloudflare/workers-types"; /** * ExecutionContext with OAuth props injected by the OAuth provider. */ type OAuthExecutionContext = ExecutionContext & { props?: Record<string, unknown>; }; /** * Main request handler that: * 1. Extracts auth props from ExecutionContext * 2. Parses org/project constraints from URL * 3. Verifies user has access to the constraints * 4. Builds complete ServerContext * 5. Creates and configures MCP server per-request (context captured in closures) * 6. Runs MCP handler */ const mcpHandler: ExportedHandler<Env> = { async fetch( request: Request, env: Env, ctx: ExecutionContext, ): Promise<Response> { const url = new URL(request.url); // Parse constraints from URL pattern /mcp/:org?/:project? const pattern = new URLPattern({ pathname: "/mcp/:org?/:project?" }); const result = pattern.exec(url); if (!result) { return new Response("Not found", { status: 404 }); } const { groups } = result.pathname; const organizationSlug = groups?.org || null; const projectSlug = groups?.project || null; // Check for agent mode query parameter const isAgentMode = url.searchParams.get("agent") === "1"; // Extract OAuth props from ExecutionContext (set by OAuth provider) const oauthCtx = ctx as OAuthExecutionContext; if (!oauthCtx.props) { throw new Error("No authentication context available"); } const sentryHost = env.SENTRY_HOST || "sentry.io"; // Verify user has access to the requested org/project const verification = await verifyConstraintsAccess( { organizationSlug, projectSlug }, { accessToken: oauthCtx.props.accessToken as string, sentryHost, }, ); if (!verification.ok) { return new Response(verification.message, { status: verification.status ?? 500, }); } // Parse and expand granted scopes (LEGACY - for backward compatibility) let expandedScopes: Set<Scope> | undefined; if (oauthCtx.props.grantedScopes) { const { valid, invalid } = parseScopes( oauthCtx.props.grantedScopes as string[], ); if (invalid.length > 0) { logWarn("Ignoring invalid scopes from OAuth provider", { loggerScope: ["cloudflare", "mcp-handler"], extra: { invalidScopes: invalid, }, }); } expandedScopes = expandScopes(new Set(valid)); } // Parse and validate granted skills (NEW - primary authorization method) let grantedSkills: Set<Skill> | undefined; if (oauthCtx.props.grantedSkills) { const { valid, invalid } = parseSkills( oauthCtx.props.grantedSkills as string[], ); if (invalid.length > 0) { logWarn("Ignoring invalid skills from OAuth provider", { loggerScope: ["cloudflare", "mcp-handler"], extra: { invalidSkills: invalid, }, }); } grantedSkills = new Set(valid); // Validate that at least one valid skill was granted if (grantedSkills.size === 0) { return new Response( "Authorization failed: No valid skills were granted. Please re-authorize and select at least one permission.", { status: 400 }, ); } } // Validate that at least one authorization system is active // This should never happen in practice - indicates a bug in OAuth flow if (!grantedSkills && !expandedScopes) { logIssue( new Error( "No authorization grants found - server would expose no tools", ), { loggerScope: ["cloudflare", "mcp-handler"], extra: { clientId: oauthCtx.props.clientId, hasGrantedSkills: !!oauthCtx.props.grantedSkills, hasGrantedScopes: !!oauthCtx.props.grantedScopes, }, }, ); return new Response( "Authorization failed: No valid permissions were granted. Please re-authorize and select at least one permission.", { status: 401 }, ); } // Build complete ServerContext from OAuth props + verified constraints const serverContext: ServerContext = { userId: oauthCtx.props.id as string | undefined, clientId: oauthCtx.props.clientId as string, accessToken: oauthCtx.props.accessToken as string, // Scopes derived from skills - for backward compatibility with old MCP clients // that don't support grantedSkills and only understand grantedScopes grantedScopes: expandedScopes, grantedSkills, // Primary authorization method constraints: verification.constraints, sentryHost, mcpUrl: env.MCP_URL, }; // Create and configure MCP server with tools filtered by context // Context is captured in tool handler closures during buildServer() const server = buildServer({ context: serverContext, agentMode: isAgentMode, }); // Run MCP handler - context already captured in closures return createMcpHandler(server, { route: url.pathname, })(request, env, ctx); }, }; export default mcpHandler;

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