#
tokens: 3816/50000 14/14 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
  ![image](https://github.com/user-attachments/assets/3c560ecd-190f-4810-b5e5-4233d9451249)
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;
  }
} 
```