# 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 1 | # MCP Server Sentry - TypeScript Implementation 2 | 3 | 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. 4 | 5 | ## Features 6 | 7 | 1. `get_sentry_issue` Tool 8 | * Retrieves and analyzes Sentry issues by ID or URL 9 | * Input: 10 | * `issue_id_or_url` (string): Sentry issue ID or URL to analyze 11 | * Returns: Issue details including: 12 | * Title 13 | * Issue ID 14 | * Status 15 | * Level 16 | * First seen timestamp 17 | * Last seen timestamp 18 | * Event count 19 | * Complete stack trace 20 | 21 | 2. `sentry-issue` Prompt Template 22 | * Retrieves issue details from Sentry 23 | * Input: 24 | * `issue_id_or_url` (string): Sentry issue ID or URL 25 | * Returns: Formatted issue details as conversation context 26 | 27 | ## Installation 28 | 29 | ```bash 30 | # Install dependencies 31 | npm install 32 | 33 | # Build the project 34 | npm run build 35 | ``` 36 | 37 | ## Configuration 38 | 39 | The server is configured using environment variables. Create a `.env` file in the project root directory: 40 | 41 | ``` 42 | # Required: Sentry authentication token 43 | SENTRY_AUTH_TOKEN=your_sentry_auth_token 44 | 45 | # Optional: Sentry organization name 46 | SENTRY_ORGANIZATION_SLUG=your_organization_slug 47 | 48 | # Optional: Sentry project name 49 | SENTRY_PROJECT_SLUG=your_project_slug 50 | 51 | # Optional: Sentry base url 52 | SENTRY_BASE_URL=https://sentry.com/api/0 53 | ``` 54 | 55 | Alternatively, you can set these environment variables at runtime. 56 | 57 | ## Running 58 | 59 | Run the server via standard IO: 60 | 61 | ```bash 62 | node dist/index.js 63 | ``` 64 | 65 | Debug with MCP Inspector: 66 | 67 | ```bash 68 | npx @modelcontextprotocol/inspector node dist/index.js 69 | ``` 70 | 71 | ## Environment Variables Description 72 | 73 | - `SENTRY_AUTH_TOKEN` (required): Your Sentry API access token 74 | - `SENTRY_PROJECT_SLUG` (optional): The slug of your Sentry project 75 | - `SENTRY_ORGANIZATION_SLUG` (optional): The slug of your Sentry organization 76 | 77 | The latter two variables can be omitted if project and organization information are provided in the URL. 78 | 79 | ## License 80 | 81 | This project is licensed under the MIT License. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true, 12 | "sourceMap": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-server-sentry", 3 | "version": "1.0.0", 4 | "description": "MCP Server for Sentry - TypeScript Implementation", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-server-sentry": "./bin/mcp-server-sentry" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "start": "node dist/index.js", 13 | "dev": "npm run build && npm run start", 14 | "lint": "eslint src --ext .ts", 15 | "debug": "npx @modelcontextprotocol/inspector node dist/index.js" 16 | }, 17 | "keywords": [ 18 | "mcp", 19 | "sentry", 20 | "typescript" 21 | ], 22 | "author": "", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@modelcontextprotocol/sdk": "^1.6.1", 26 | "axios": "^1.6.2", 27 | "dotenv": "^16.3.1", 28 | "zod": "^3.22.4" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20.10.0", 32 | "typescript": "^5.3.2", 33 | "eslint": "^8.54.0", 34 | "@typescript-eslint/eslint-plugin": "^6.12.0", 35 | "@typescript-eslint/parser": "^6.12.0" 36 | } 37 | } 38 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { z } from "zod"; 6 | import { SentryClient } from "./sentry-client.js"; 7 | import dotenv from "dotenv"; 8 | import path from "path"; 9 | import { fileURLToPath } from "url"; 10 | 11 | // Get the directory of the current file 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | // Load .env environment variables 16 | dotenv.config({ path: path.resolve(__dirname, "../.env") }); 17 | 18 | // Get configuration from environment variables 19 | const authToken = process.env.SENTRY_AUTH_TOKEN; 20 | const projectSlug = process.env.SENTRY_PROJECT_SLUG; 21 | const organizationSlug = process.env.SENTRY_ORGANIZATION_SLUG; 22 | 23 | if (!authToken) { 24 | console.error("Error: Missing required environment variable SENTRY_AUTH_TOKEN"); 25 | process.exit(1); 26 | } 27 | 28 | // Create Sentry client 29 | const sentryClient = new SentryClient(authToken, organizationSlug, projectSlug); 30 | 31 | // Create MCP server 32 | const server = new McpServer({ 33 | name: "Sentry", 34 | version: "1.0.0", 35 | description: "MCP Server for accessing and analyzing Sentry issues" 36 | }); 37 | 38 | // Add tool for getting Sentry issues 39 | server.tool( 40 | "get_sentry_issue", 41 | { issue_id_or_url: z.string().describe("Sentry issue ID or URL to analyze") }, 42 | async ({ issue_id_or_url }: { issue_id_or_url: string }) => { 43 | try { 44 | const issue = await sentryClient.getIssue(issue_id_or_url); 45 | 46 | return { 47 | content: [ 48 | { 49 | type: "text", 50 | text: JSON.stringify(issue, null, 2) 51 | } 52 | ] 53 | }; 54 | } catch (error) { 55 | const errorMessage = error instanceof Error 56 | ? error.message 57 | : "Unknown error when fetching Sentry issue"; 58 | 59 | return { 60 | content: [{ type: "text", text: errorMessage }], 61 | isError: true 62 | }; 63 | } 64 | } 65 | ); 66 | 67 | // Add Sentry issue prompt template 68 | server.prompt( 69 | "sentry-issue", 70 | { issue_id_or_url: z.string().describe("Sentry issue ID or URL") }, 71 | async ({ issue_id_or_url }: { issue_id_or_url: string }) => { 72 | try { 73 | const issue = await sentryClient.getIssue(issue_id_or_url); 74 | 75 | return { 76 | messages: [ 77 | { 78 | role: "user", 79 | content: { 80 | type: "text", 81 | text: `I need help analyzing this Sentry issue: 82 | 83 | Title: ${issue.title} 84 | Issue ID: ${issue.id} 85 | Status: ${issue.status} 86 | Level: ${issue.level} 87 | First seen: ${issue.firstSeen} 88 | Last seen: ${issue.lastSeen} 89 | Event count: ${issue.count} 90 | 91 | Stacktrace: 92 | ${issue.stacktrace || "No stacktrace available"} 93 | 94 | Please help me understand this error and suggest potential fixes.` 95 | } 96 | } 97 | ] 98 | }; 99 | } catch (error) { 100 | return { 101 | messages: [ 102 | { 103 | role: "user", 104 | content: { 105 | type: "text", 106 | text: `I tried to analyze a Sentry issue, but encountered an error: 107 | ${error instanceof Error ? error.message : "Unknown error"}` 108 | } 109 | } 110 | ] 111 | }; 112 | } 113 | } 114 | ); 115 | 116 | // Start server 117 | async function start() { 118 | const transport = new StdioServerTransport(); 119 | await server.connect(transport); 120 | } 121 | start(); 122 | ``` -------------------------------------------------------------------------------- /src/sentry-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios, { AxiosError } from "axios"; 2 | 3 | // Response type for Sentry API 4 | export interface SentryIssue { 5 | id: string; 6 | title: string; 7 | status: string; 8 | level: string; 9 | firstSeen: string; 10 | lastSeen: string; 11 | count: number; 12 | stacktrace?: string; 13 | } 14 | 15 | export class SentryClient { 16 | private readonly baseUrl = process.env.SENTRY_BASE_URL || "https://sentry.com/api/0"; 17 | private readonly authToken: string; 18 | private readonly organizationSlug?: string; 19 | private readonly projectSlug?: string; 20 | 21 | constructor(authToken: string, organizationSlug?: string, projectSlug?: string) { 22 | this.authToken = authToken; 23 | this.organizationSlug = organizationSlug; 24 | this.projectSlug = projectSlug; 25 | } 26 | 27 | /** 28 | * Parse Sentry issue ID or URL 29 | */ 30 | private parseIssueIdOrUrl(issueIdOrUrl: string): { issueId: string; organizationSlug?: string; projectSlug?: string } { 31 | // Check if it's a URL 32 | if (issueIdOrUrl.startsWith("http")) { 33 | try { 34 | const url = new URL(issueIdOrUrl); 35 | const pathParts = url.pathname.split("/").filter(part => part.length > 0); 36 | 37 | // Try to extract organization, project, and issue ID from URL path 38 | if (pathParts.length >= 4 && pathParts[0] === "organizations") { 39 | return { 40 | organizationSlug: pathParts[1], 41 | projectSlug: pathParts[3], 42 | issueId: pathParts[pathParts.length - 1] 43 | }; 44 | } 45 | 46 | // Some older Sentry URLs may have different formats 47 | if (pathParts.length >= 3) { 48 | return { 49 | organizationSlug: pathParts[0], 50 | projectSlug: pathParts[1], 51 | issueId: pathParts[2] 52 | }; 53 | } 54 | } catch (error) { 55 | // URL parsing failed, fallback to using original input as issue ID 56 | } 57 | } 58 | 59 | // If not a URL or unable to parse URL, use input directly as issue ID 60 | return { 61 | issueId: issueIdOrUrl, 62 | organizationSlug: this.organizationSlug, 63 | projectSlug: this.projectSlug 64 | }; 65 | } 66 | 67 | /** 68 | * Get Sentry issue details 69 | */ 70 | async getIssue(issueIdOrUrl: string): Promise<SentryIssue> { 71 | const { issueId, organizationSlug, projectSlug } = this.parseIssueIdOrUrl(issueIdOrUrl); 72 | 73 | if (!organizationSlug || !projectSlug) { 74 | throw new Error( 75 | "Organization slug and project slug are required. Provide them either in the constructor " + 76 | "or as part of the issue URL." 77 | ); 78 | } 79 | 80 | try { 81 | // Get basic issue information 82 | const issueResponse = await axios.get( 83 | `${this.baseUrl}/organizations/${organizationSlug}/issues/${issueId}/`, 84 | { 85 | headers: { 86 | Authorization: `Bearer ${this.authToken}`, 87 | "Content-Type": "application/json" 88 | } 89 | } 90 | ); 91 | 92 | // Get the latest event to extract stack trace information 93 | const eventsResponse = await axios.get( 94 | `${this.baseUrl}/organizations/${organizationSlug}/issues/${issueId}/events/latest/`, 95 | { 96 | headers: { 97 | Authorization: `Bearer ${this.authToken}`, 98 | "Content-Type": "application/json" 99 | } 100 | } 101 | ); 102 | 103 | // Extract stack trace 104 | let stacktrace: string | undefined; 105 | if (eventsResponse.data.entries) { 106 | const exceptionEntry = eventsResponse.data.entries.find( 107 | (entry: any) => entry.type === "exception" 108 | ); 109 | 110 | if (exceptionEntry && exceptionEntry.data && exceptionEntry.data.values) { 111 | const exceptions = exceptionEntry.data.values; 112 | stacktrace = exceptions 113 | .map((exception: any) => { 114 | let frames = ""; 115 | if (exception.stacktrace && exception.stacktrace.frames) { 116 | frames = exception.stacktrace.frames 117 | .map((frame: any) => { 118 | return ` at ${frame.function || "unknown"} (${frame.filename || "unknown"}:${frame.lineno || "?"}:${frame.colno || "?"})`; 119 | }) 120 | .reverse() 121 | .join("\n"); 122 | } 123 | 124 | return `${exception.type}: ${exception.value}\n${frames}`; 125 | }) 126 | .join("\n\nCaused by: "); 127 | } 128 | } 129 | 130 | // Build and return issue details 131 | return { 132 | id: issueResponse.data.id, 133 | title: issueResponse.data.title, 134 | status: issueResponse.data.status, 135 | level: issueResponse.data.level || "error", 136 | firstSeen: issueResponse.data.firstSeen, 137 | lastSeen: issueResponse.data.lastSeen, 138 | count: issueResponse.data.count, 139 | stacktrace 140 | }; 141 | } catch (error: unknown) { 142 | if (axios.isAxiosError(error) && error.response) { 143 | throw new Error(`Sentry API error: ${error.response.status} - ${error.response.data.detail || JSON.stringify(error.response.data)}`); 144 | } 145 | throw error; 146 | } 147 | } 148 | } ```