#
tokens: 3946/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | } 
```