Skip to main content
Glama

Sentry MCP

Official
by getsentry
search.ts6.81 kB
import { Hono } from "hono"; import type { Env } from "../types"; import { logIssue } from "@sentry/mcp-server/telem/logging"; import { SENTRY_GUIDES } from "@sentry/mcp-server/constants"; import { z } from "zod"; import type { RateLimitResult } from "../types/chat"; import type { AutoRagSearchResponse, ComparisonFilter, CompoundFilter, AutoRagSearchRequest, } from "@cloudflare/workers-types"; import { logger } from "@sentry/cloudflare"; // Request schema matching the MCP tool parameters const SearchRequestSchema = z.object({ query: z.string().trim().min(1, "Query is required"), maxResults: z.number().int().min(1).max(10).default(10).optional(), guide: z.enum(SENTRY_GUIDES).optional(), }); export default new Hono<{ Bindings: Env }>().post("/", async (c) => { try { // Get client IP address from Cloudflare header const clientIP = c.req.header("CF-Connecting-IP") || "unknown"; // Rate limiting check - use client IP as the key // Note: Rate limiting bindings are "unsafe" (beta) and may not be available in development // so we check if the binding exists before using it // https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/ if (c.env.SEARCH_RATE_LIMITER) { try { // Hash the IP for privacy and consistent key format const encoder = new TextEncoder(); const data = encoder.encode(clientIP); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); const rateLimitKey = `search:ip:${hashHex.substring(0, 16)}`; // Use first 16 chars of hash const { success }: RateLimitResult = await c.env.SEARCH_RATE_LIMITER.limit({ key: rateLimitKey, }); if (!success) { return c.json( { error: "Rate limit exceeded. You can perform up to 20 documentation searches per minute. Please wait before searching again.", name: "RATE_LIMIT_EXCEEDED", }, 429, ); } } catch (error) { const eventId = logIssue(error); return c.json( { error: "There was an error communicating with the rate limiter.", name: "RATE_LIMITER_ERROR", eventId, }, 500, ); } } // Parse and validate request body const body = await c.req.json(); const validationResult = SearchRequestSchema.safeParse(body); if (!validationResult.success) { return c.json( { error: "Invalid request", details: validationResult.error.errors, }, 400, ); } const { query, maxResults = 10, guide } = validationResult.data; // Check if AI binding is available if (!c.env.AI) { return c.json( { error: "AI service not available", name: "AI_SERVICE_UNAVAILABLE", }, 503, ); } try { const autoragId = c.env.AUTORAG_INDEX_NAME || "sentry-docs"; // Construct AutoRAG search parameters const searchParams: AutoRagSearchRequest = { query, max_num_results: maxResults, ranking_options: { score_threshold: 0.2, }, }; // Add filename filters based on guide parameter // TODO: This is a hack to get the guide to work. Currently 'filename' is not working // with folder matching which means we're lacking guideName.md in the search results. if (guide) { let filter: ComparisonFilter | CompoundFilter; if (guide.includes("/")) { // Platform/guide combination: platforms/[platform]/guides/[guide] const [platformName, guideName] = guide.split("/", 2); filter = { type: "and", filters: [ { type: "gte", key: "folder", value: `platforms/${platformName}/guides/${guideName}/`, }, { type: "lte", key: "folder", value: `platforms/${platformName}/guides/${guideName}/z`, }, ], }; } else { // Just platform: platforms/[platform]/ - use range filter filter = { type: "and", filters: [ { type: "gte", key: "folder", value: `platforms/${guide}/`, }, { type: "lte", key: "folder", value: `platforms/${guide}/z`, }, ], }; } searchParams.filters = filter; } const searchResult = await c.env.AI.autorag(autoragId).search(searchParams); // Process search results - handle the actual response format from Cloudflare AI const searchData = searchResult as AutoRagSearchResponse; if (searchData.data?.length === 0) { logger.warn( logger.fmt`No results found for query: ${query} with guide: ${guide}`, { result_query: searchData.search_query, guide, searchParams: JSON.stringify(searchParams), }, ); } return c.json({ query, results: searchData.data?.map((result) => { // Extract text from content array const text = result.content?.[0]?.text || ""; // Get filename from result - ensure it's a string const rawFilename = result.filename || result.attributes?.filename || ""; const filename = typeof rawFilename === "string" ? rawFilename : String(rawFilename); // Build URL from filename - remove .md extension const urlPath = filename.replace(/\.md$/, ""); const url = urlPath ? `https://docs.sentry.io/${urlPath}` : ""; return { id: filename, url: url, snippet: text, relevance: result.score || 0, }; }) || [], }); } catch (error) { const eventId = logIssue(error); return c.json( { error: "Failed to search documentation. Please try again later.", name: "SEARCH_FAILED", eventId, }, 500, ); } } catch (error) { const eventId = logIssue(error); return c.json( { error: "Internal server error", name: "INTERNAL_ERROR", eventId, }, 500, ); } });

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