# Directory Structure ``` ├── .env.example ├── .gitignore ├── bin │ └── mcp-server-sentry ├── package-lock.json ├── package.json ├── README-zh_CN.md ├── README.md ├── src │ ├── index.ts │ └── sentry-client.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Server Sentry - TypeScript Implementation This is a Model Context Protocol (MCP) server implemented in TypeScript for connecting to the Sentry error tracking service. This server allows AI models to query and analyze error reports and events on Sentry. ## Features 1. `get_sentry_issue` Tool * Retrieves and analyzes Sentry issues by ID or URL * Input: * `issue_id_or_url` (string): Sentry issue ID or URL to analyze * Returns: Issue details including: * Title * Issue ID * Status * Level * First seen timestamp * Last seen timestamp * Event count * Complete stack trace 2. `sentry-issue` Prompt Template * Retrieves issue details from Sentry * Input: * `issue_id_or_url` (string): Sentry issue ID or URL * Returns: Formatted issue details as conversation context ## Installation ```bash # Install dependencies npm install # Build the project npm run build ``` ## Configuration The server is configured using environment variables. Create a `.env` file in the project root directory: ``` # Required: Sentry authentication token SENTRY_AUTH_TOKEN=your_sentry_auth_token # Optional: Sentry organization name SENTRY_ORGANIZATION_SLUG=your_organization_slug # Optional: Sentry project name SENTRY_PROJECT_SLUG=your_project_slug # Optional: Sentry base url SENTRY_BASE_URL=https://sentry.com/api/0 ``` Alternatively, you can set these environment variables at runtime. ## Running Run the server via standard IO: ```bash node dist/index.js ``` Debug with MCP Inspector: ```bash npx @modelcontextprotocol/inspector node dist/index.js ``` ## Environment Variables Description - `SENTRY_AUTH_TOKEN` (required): Your Sentry API access token - `SENTRY_PROJECT_SLUG` (optional): The slug of your Sentry project - `SENTRY_ORGANIZATION_SLUG` (optional): The slug of your Sentry organization The latter two variables can be omitted if project and organization information are provided in the URL. ## License This project is licensed under the MIT License. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "outDir": "dist", "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-server-sentry", "version": "1.0.0", "description": "MCP Server for Sentry - TypeScript Implementation", "main": "dist/index.js", "type": "module", "bin": { "mcp-server-sentry": "./bin/mcp-server-sentry" }, "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "npm run build && npm run start", "lint": "eslint src --ext .ts", "debug": "npx @modelcontextprotocol/inspector node dist/index.js" }, "keywords": [ "mcp", "sentry", "typescript" ], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "axios": "^1.6.2", "dotenv": "^16.3.1", "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.10.0", "typescript": "^5.3.2", "eslint": "^8.54.0", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { SentryClient } from "./sentry-client.js"; import dotenv from "dotenv"; import path from "path"; import { fileURLToPath } from "url"; // Get the directory of the current file const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load .env environment variables dotenv.config({ path: path.resolve(__dirname, "../.env") }); // Get configuration from environment variables const authToken = process.env.SENTRY_AUTH_TOKEN; const projectSlug = process.env.SENTRY_PROJECT_SLUG; const organizationSlug = process.env.SENTRY_ORGANIZATION_SLUG; if (!authToken) { console.error("Error: Missing required environment variable SENTRY_AUTH_TOKEN"); process.exit(1); } // Create Sentry client const sentryClient = new SentryClient(authToken, organizationSlug, projectSlug); // Create MCP server const server = new McpServer({ name: "Sentry", version: "1.0.0", description: "MCP Server for accessing and analyzing Sentry issues" }); // Add tool for getting Sentry issues server.tool( "get_sentry_issue", { issue_id_or_url: z.string().describe("Sentry issue ID or URL to analyze") }, async ({ issue_id_or_url }: { issue_id_or_url: string }) => { try { const issue = await sentryClient.getIssue(issue_id_or_url); return { content: [ { type: "text", text: JSON.stringify(issue, null, 2) } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error when fetching Sentry issue"; return { content: [{ type: "text", text: errorMessage }], isError: true }; } } ); // Add Sentry issue prompt template server.prompt( "sentry-issue", { issue_id_or_url: z.string().describe("Sentry issue ID or URL") }, async ({ issue_id_or_url }: { issue_id_or_url: string }) => { try { const issue = await sentryClient.getIssue(issue_id_or_url); return { messages: [ { role: "user", content: { type: "text", text: `I need help analyzing this Sentry issue: Title: ${issue.title} Issue ID: ${issue.id} Status: ${issue.status} Level: ${issue.level} First seen: ${issue.firstSeen} Last seen: ${issue.lastSeen} Event count: ${issue.count} Stacktrace: ${issue.stacktrace || "No stacktrace available"} Please help me understand this error and suggest potential fixes.` } } ] }; } catch (error) { return { messages: [ { role: "user", content: { type: "text", text: `I tried to analyze a Sentry issue, but encountered an error: ${error instanceof Error ? error.message : "Unknown error"}` } } ] }; } } ); // Start server async function start() { const transport = new StdioServerTransport(); await server.connect(transport); } start(); ``` -------------------------------------------------------------------------------- /src/sentry-client.ts: -------------------------------------------------------------------------------- ```typescript import axios, { AxiosError } from "axios"; // Response type for Sentry API export interface SentryIssue { id: string; title: string; status: string; level: string; firstSeen: string; lastSeen: string; count: number; stacktrace?: string; } export class SentryClient { private readonly baseUrl = process.env.SENTRY_BASE_URL || "https://sentry.com/api/0"; private readonly authToken: string; private readonly organizationSlug?: string; private readonly projectSlug?: string; constructor(authToken: string, organizationSlug?: string, projectSlug?: string) { this.authToken = authToken; this.organizationSlug = organizationSlug; this.projectSlug = projectSlug; } /** * Parse Sentry issue ID or URL */ private parseIssueIdOrUrl(issueIdOrUrl: string): { issueId: string; organizationSlug?: string; projectSlug?: string } { // Check if it's a URL if (issueIdOrUrl.startsWith("http")) { try { const url = new URL(issueIdOrUrl); const pathParts = url.pathname.split("/").filter(part => part.length > 0); // Try to extract organization, project, and issue ID from URL path if (pathParts.length >= 4 && pathParts[0] === "organizations") { return { organizationSlug: pathParts[1], projectSlug: pathParts[3], issueId: pathParts[pathParts.length - 1] }; } // Some older Sentry URLs may have different formats if (pathParts.length >= 3) { return { organizationSlug: pathParts[0], projectSlug: pathParts[1], issueId: pathParts[2] }; } } catch (error) { // URL parsing failed, fallback to using original input as issue ID } } // If not a URL or unable to parse URL, use input directly as issue ID return { issueId: issueIdOrUrl, organizationSlug: this.organizationSlug, projectSlug: this.projectSlug }; } /** * Get Sentry issue details */ async getIssue(issueIdOrUrl: string): Promise<SentryIssue> { const { issueId, organizationSlug, projectSlug } = this.parseIssueIdOrUrl(issueIdOrUrl); if (!organizationSlug || !projectSlug) { throw new Error( "Organization slug and project slug are required. Provide them either in the constructor " + "or as part of the issue URL." ); } try { // Get basic issue information const issueResponse = await axios.get( `${this.baseUrl}/organizations/${organizationSlug}/issues/${issueId}/`, { headers: { Authorization: `Bearer ${this.authToken}`, "Content-Type": "application/json" } } ); // Get the latest event to extract stack trace information const eventsResponse = await axios.get( `${this.baseUrl}/organizations/${organizationSlug}/issues/${issueId}/events/latest/`, { headers: { Authorization: `Bearer ${this.authToken}`, "Content-Type": "application/json" } } ); // Extract stack trace let stacktrace: string | undefined; if (eventsResponse.data.entries) { const exceptionEntry = eventsResponse.data.entries.find( (entry: any) => entry.type === "exception" ); if (exceptionEntry && exceptionEntry.data && exceptionEntry.data.values) { const exceptions = exceptionEntry.data.values; stacktrace = exceptions .map((exception: any) => { let frames = ""; if (exception.stacktrace && exception.stacktrace.frames) { frames = exception.stacktrace.frames .map((frame: any) => { return ` at ${frame.function || "unknown"} (${frame.filename || "unknown"}:${frame.lineno || "?"}:${frame.colno || "?"})`; }) .reverse() .join("\n"); } return `${exception.type}: ${exception.value}\n${frames}`; }) .join("\n\nCaused by: "); } } // Build and return issue details return { id: issueResponse.data.id, title: issueResponse.data.title, status: issueResponse.data.status, level: issueResponse.data.level || "error", firstSeen: issueResponse.data.firstSeen, lastSeen: issueResponse.data.lastSeen, count: issueResponse.data.count, stacktrace }; } catch (error: unknown) { if (axios.isAxiosError(error) && error.response) { throw new Error(`Sentry API error: ${error.response.status} - ${error.response.data.detail || JSON.stringify(error.response.data)}`); } throw error; } } } ```