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