# Directory Structure
```
├── .env.example
├── .gitignore
├── .nvmrc
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── config.ts
│ ├── errors
│ │ └── sentry.ts
│ ├── index.ts
│ ├── prompts
│ │ └── sentry-issue.ts
│ ├── services
│ │ └── sentry.ts
│ ├── tools
│ │ └── SentryIssue.ts
│ ├── types
│ │ └── sentry.ts
│ └── utils
│ └── sentry.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
```
v22.0.0
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
.env
node_modules
dist
logs
.vscode
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
# Required: Your Sentry authentication token
SENTRY_AUTH_TOKEN="sntryu_your_token"
# Optional: Override the default Sentry API base URL
# SENTRY_API_BASE=https://sentry.io/api/0/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Sentry MCP Server 🔍
A TypeScript implementation of a Sentry MCP (Modern Context Protocol) tool that allows AI agents to access and analyze Sentry error data. 🤖
## ✨ Features
- 🎯 Retrieve and analyze Sentry issues
- 📊 Get formatted issue details and metadata
- 🔬 View detailed stacktraces
- 🛠️ Support for both tool and prompt interfaces
- 🛡️ Robust error handling
- 🔄 Real-time communication
## 📦 Installation
```bash
pnpm install
```
## 🔧 Configuration
Create a `.env` file in the root directory with your Sentry auth token:
```env
SENTRY_AUTH_TOKEN=your_sentry_auth_token
SENTRY_API_BASE=https://sentry.io/api/0/ # Optional, defaults to this value
```
## 📚 Usage
### Starting the Server 🚀
```bash
pnpm build && pnpm start
```
The server will start on port 1337 by default.
### Using with MCP 🛠️
The server provides two MCP interfaces:
1. Tool Interface: `get_sentry_issue`
```json
{
"issue_id_or_url": "12345"
}
```
2. Prompt Interface: `sentry-issue`
```json
{
"issue_id_or_url": "https://sentry.io/organizations/your-org/issues/12345/"
}
```
## 💡 Integrating with Cursor IDE
The Sentry MCP Server can be integrated with Cursor IDE for enhanced development experience:
1. 🚀 Start the MCP server locally using `pnpm start`
2. 🔧 Configure Cursor to use the local MCP server:

3. 🎉 Enjoy seamless Sentry issue analysis directly in your IDE!
## 🤝 Contributing
1. 🔀 Fork the repository
2. 🌿 Create your feature branch
3. 💾 Commit your changes
4. 🚀 Push to the branch
5. 📬 Create a new Pull Request
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { MCPServer } from "mcp-framework";
const transport = 'sse'
const server = new MCPServer({
name: "sentry-mcp-server",
version: "0.0.1",
transport: {
type: transport,
options: {
port: 1337,
},
},
});
server.start();
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "sentry-mcp-server",
"version": "0.0.1",
"description": "sentry-mcp-server MCP server",
"type": "module",
"bin": {
"sentry-mcp-server": "./dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "mcp-build",
"prepare": "npm run build",
"watch": "tsc --watch",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.6.1",
"axios": "^1.8.1",
"dotenv": "^16.4.7",
"mcp-framework": "^0.1.25"
},
"devDependencies": {
"@types/node": "^20.11.24",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
import dotenv from 'dotenv';
import { SentryConfig } from './types/sentry.js';
import { SentryValidationError } from './errors/sentry.js';
dotenv.config();
export const SENTRY_API_BASE = 'https://us.sentry.io/api/0/';
export const MISSING_AUTH_TOKEN_MESSAGE = 'Sentry authentication token is required';
export function getSentryConfig(): SentryConfig {
const authToken = process.env.SENTRY_AUTH_TOKEN;
if (!authToken) {
throw new SentryValidationError(MISSING_AUTH_TOKEN_MESSAGE);
}
return {
authToken,
apiBase: process.env.SENTRY_API_BASE || SENTRY_API_BASE,
};
}
```
--------------------------------------------------------------------------------
/src/errors/sentry.ts:
--------------------------------------------------------------------------------
```typescript
export class SentryError extends Error {
constructor(message: string, public code?: string) {
super(message);
this.name = 'SentryError';
Object.setPrototypeOf(this, SentryError.prototype);
}
}
export class SentryAuthError extends SentryError {
constructor(message: string = 'Authentication failed') {
super(message, 'AUTH_ERROR');
this.name = 'SentryAuthError';
}
}
export class SentryNotFoundError extends SentryError {
constructor(message: string = 'Resource not found') {
super(message, 'NOT_FOUND');
this.name = 'SentryNotFoundError';
}
}
export class SentryValidationError extends SentryError {
constructor(message: string = 'Invalid input') {
super(message, 'VALIDATION_ERROR');
this.name = 'SentryValidationError';
}
}
```
--------------------------------------------------------------------------------
/src/prompts/sentry-issue.ts:
--------------------------------------------------------------------------------
```typescript
import { MCPPrompt } from "mcp-framework";
import { z } from "zod";
import { SentryService } from "../services/sentry.js";
import { getSentryConfig } from "../config.js";
import { SentryError } from "../errors/sentry.js";
interface SentryPromptInput {
issue_id_or_url: string;
}
export default class SentryIssuePrompt extends MCPPrompt<SentryPromptInput> {
name = "sentry-issue";
description = "Get details about a Sentry issue";
private sentryService: SentryService;
constructor() {
super();
this.sentryService = new SentryService(getSentryConfig());
}
schema = {
issue_id_or_url: {
type: z.string(),
description: "Sentry issue ID or URL to analyze",
},
};
async generateMessages(input: SentryPromptInput) {
try {
const issue = await this.sentryService.getIssue(input.issue_id_or_url);
return [
{
role: "assistant",
content: {
type: "text",
text: issue.to_text(),
},
},
];
} catch (error) {
if (error instanceof SentryError) {
throw error;
}
throw new Error(`Failed to retrieve Sentry issue: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
```
--------------------------------------------------------------------------------
/src/utils/sentry.ts:
--------------------------------------------------------------------------------
```typescript
import { SentryValidationError } from '../errors/sentry.js';
import { SentryEvent } from '../types/sentry.js';
export function extractIssueId(issueIdOrUrl: string): string {
// Handle direct numeric IDs
if (/^\d+$/.test(issueIdOrUrl)) {
return issueIdOrUrl;
}
try {
const url = new URL(issueIdOrUrl);
// Extract ID from Sentry URL patterns
const matches = url.pathname.match(/issues?\/(\d+)/i);
if (matches && matches[1]) {
return matches[1];
}
} catch (error) {
// URL parsing failed
}
throw new SentryValidationError(`Invalid Sentry issue ID or URL: ${issueIdOrUrl}`);
}
export function createStacktrace(event: SentryEvent): string {
if (!event?.entries?.length) {
return 'No stacktrace available';
}
const exceptionEntry = event.entries.find(entry => entry.type === 'exception');
if (!exceptionEntry?.data?.values?.length) {
return 'No exception data available';
}
return exceptionEntry.data.values
.map(value => {
const frames = value.stacktrace?.frames || [];
const stackLines = frames
.reverse()
.map(frame => {
const location = `${frame.filename}:${frame.lineno}${frame.colno ? `:${frame.colno}` : ''}`;
return ` at ${frame.function} (${location})`;
})
.join('\n');
return `${value.type}: ${value.value}\n${stackLines}`;
})
.join('\n\nCaused by: ');
}
export function formatDateTime(isoString: string): string {
return new Date(isoString).toLocaleString();
}
```
--------------------------------------------------------------------------------
/src/tools/SentryIssue.ts:
--------------------------------------------------------------------------------
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
import { SentryService } from "../services/sentry.js";
import { getSentryConfig } from "../config.js";
import { SentryError } from "../errors/sentry.js";
interface SentryToolInput {
issue_id_or_url: string;
}
class GetSentryIssueTool extends MCPTool<SentryToolInput> {
name = "get_sentry_issue";
description = "Retrieve and analyze a Sentry issue by ID or URL";
private sentryService: SentryService;
constructor() {
super();
this.sentryService = new SentryService(getSentryConfig());
}
schema = {
issue_id_or_url: {
type: z.string(),
description: "Sentry issue ID or URL to analyze",
},
};
async execute(input: SentryToolInput) {
try {
console.log("Executing SentryIssue tool with input:", input);
const issue = await this.sentryService.getIssue(input.issue_id_or_url);
console.log("Sentry issue retrieved:", issue);
return {
type: "object",
value: {
issue_id: issue.issue_id,
title: issue.title,
status: issue.status,
level: issue.level,
filename: issue.filename,
function: issue.function,
type: issue.type,
first_seen: issue.first_seen,
last_seen: issue.last_seen,
count: issue.count,
stacktrace: issue.stacktrace,
},
};
} catch (error) {
if (error instanceof SentryError) {
throw error;
}
throw new Error(`Failed to retrieve Sentry issue: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
export default GetSentryIssueTool;
```
--------------------------------------------------------------------------------
/src/types/sentry.ts:
--------------------------------------------------------------------------------
```typescript
import { MCPPrompt, MCPTool } from 'mcp-framework';
type MCPPromptResult = {
type: string;
text: string;
}
type MCPToolResult = {
type: string;
value: Record<string, unknown>;
}
export interface SentryIssueData {
title: string;
issue_id: string;
status: string;
level: string;
filename: string;
function: string;
type: string;
first_seen: string;
last_seen: string;
count: number;
stacktrace: string;
to_text(): string;
to_prompt_result(): MCPPromptResult;
to_tool_result(): MCPToolResult;
}
export interface SentryEvent {
id: string;
entries: Array<{
type: string;
data: {
values: Array<{
type: string;
value: string;
stacktrace?: {
frames: Array<{
filename: string;
function: string;
lineno: number;
colno?: number;
}>;
};
}>;
};
}>;
}
export interface SentryIssue {
id: string;
title: string;
status: string;
level: string;
firstSeen: string;
lastSeen: string;
metadata: {
filename: string;
function: string;
type: string;
};
count: string;
latestEvent: SentryEvent;
}
export interface SentryConfig {
authToken: string;
apiBase: string;
}
export interface SentryIssueData {
title: string;
issue_id: string;
status: string;
level: string;
first_seen: string;
last_seen: string;
count: number;
stacktrace: string;
to_text(): string;
to_prompt_result(): { type: string; text: string };
to_tool_result(): { type: string; value: Record<string, unknown> };
}
export interface SentryEvent {
id: string;
entries: Array<{
type: string;
data: {
values: Array<{
type: string;
value: string;
stacktrace?: {
frames: Array<{
filename: string;
function: string;
lineno: number;
colno?: number;
}>;
};
}>;
};
}>;
}
export interface SentryIssue {
id: string;
title: string;
status: string;
level: string;
firstSeen: string;
lastSeen: string;
count: string;
latestEvent: SentryEvent;
}
export interface SentryConfig {
authToken: string;
apiBase: string;
}
```
--------------------------------------------------------------------------------
/src/services/sentry.ts:
--------------------------------------------------------------------------------
```typescript
import axios, { AxiosInstance } from 'axios';
import { SentryConfig, SentryIssue, SentryIssueData } from '../types/sentry.js';
import { SentryAuthError, SentryNotFoundError } from '../errors/sentry.js';
import { extractIssueId, createStacktrace, formatDateTime } from '../utils/sentry.js';
export class SentryService {
private client: AxiosInstance;
constructor(private config: SentryConfig) {
this.client = axios.create({
baseURL: config.apiBase,
headers: {
'Authorization': `Bearer ${config.authToken}`,
'Content-Type': 'application/json',
},
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => {
if (error.response) {
switch (error.response.status) {
case 401:
throw new SentryAuthError();
case 404:
throw new SentryNotFoundError();
default:
throw new Error(`Sentry API error: ${error.response.data.detail || error.message}`);
}
}
throw error;
}
);
}
async getIssue(issueIdOrUrl: string): Promise<SentryIssueData> {
const issueId = extractIssueId(issueIdOrUrl);
console.log("Extracted issue ID:", issueId);
const response = await this.client.get<SentryIssue>(`/issues/${issueId}/`);
const issue = response.data;
console.log("Sentry issue retrieved:", JSON.stringify(issue, null, 2));
const data: SentryIssueData = {
title: issue.title,
issue_id: issue.id,
status: issue.status,
level: issue.level,
filename: issue.metadata.filename,
function: issue.metadata.function,
type: issue.metadata.type,
first_seen: issue.firstSeen,
last_seen: issue.lastSeen,
count: parseInt(issue.count, 10),
stacktrace: createStacktrace(issue.latestEvent),
to_text(): string {
return `Sentry Issue ${this.issue_id}
Title: ${this.title}
Status: ${this.status}
Level: ${this.level}
Filename: ${this.filename}
Function: ${this.function}
Type: ${this.type}
First seen: ${formatDateTime(this.first_seen)}
Last seen: ${formatDateTime(this.last_seen)}
Count: ${this.count}
Stacktrace:
${this.stacktrace}`;
},
to_prompt_result() {
return {
type: "text",
text: this.to_text(),
};
},
to_tool_result() {
return {
type: "object",
value: {
issue_id: this.issue_id,
title: this.title,
status: this.status,
level: this.level,
filename: this.filename,
function: this.function,
type: this.type,
first_seen: this.first_seen,
last_seen: this.last_seen,
count: this.count,
stacktrace: this.stacktrace,
},
};
},
};
return data;
}
}
```