# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README_cn.md ├── README.md ├── smithery.yaml ├── src │ ├── fetcher.ts │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | /node_modules 2 | /build 3 | .env.development.local ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # sentry-issue-mcp 2 | 3 | [](README_cn.md) 4 | [](README.md) 5 | [](https://smithery.ai/server/@Leee62/sentry-issues-mcp) 6 | 7 | ## Description 8 | 9 | > ⚠️ Getting issues is deprecated as of version 1.0.5. The response structure is so similar to the event API that it's considered redundant. 10 | 11 | This is a mcp for sentry issue.\ 12 | It supports 2 tools to get a issue or list of issues.\ 13 | U can let LLM analysis the Res, or u want to do. 14 | 15 | ## Feature 16 | 17 | - EZ size 18 | - EZ understand 19 | - EZ tiny 20 | 21 | ## Tools 22 | 23 | - get_single_event 24 | - get a event detail infos, tiny mode return stack info, huge mode return all info 25 | - inputs: 26 | - url_or_id: sentry event url or sentry event id 27 | - organization_id_or_slug: sentry organization id or slug, it can be undefined 28 | - project_id_or_slug: sentry project id or slug, it can be undefined 29 | - mode: tiny or huge, it can be undefined 30 | - get_project_events 31 | - get list of events, tiny mode return id and title, huge mode return all info 32 | - inputs: 33 | - project_id_or_slug: sentry project id or slug 34 | - organization_id_or_slug: sentry organization id or slug, it can be undefined 35 | - mode: tiny or huge, it can be undefined 36 | 37 | ## QuickStart 38 | 39 | this is MCP Server Config 40 | 41 | ```json 42 | "mcpServers": { 43 | "sentry-issue-mcp": { 44 | "type": "stdio", 45 | "command": "npx", 46 | "args": [ 47 | "-y", 48 | "sentry-issues-mcp@latest" 49 | ], 50 | "env": { 51 | "SENTRY_HOST": "<your_sentry_host>", 52 | "SENTRY_ORG": "<your_sentry_org>", 53 | "SENTRY_PROJ": "<your_sentry_proj>", 54 | "SENTRY_USER_TOKEN": "<your_sentry_user_token>" 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## Case 61 | 62 | - Ask LLM to analysis one issue by url or id 63 | 1. input "analysis the issue, and give me the reason of it, and tell me how to fix it, {sentry_issue_url}" 64 | 2. if ur LLM is SMART🧠, it will call tools 65 | 3. u will get result 66 | - Ask LLM to find Today most dangerous issue (PS: default val of sentry time period is "24h") 67 | 1. input "find today most dangerous issue, and give me the reason of it, and tell me how to fix it" 68 | 2. if ur LLM is SMART🧠, it will call tools 69 | 3. u will get result 70 | 71 | ## License 72 | 73 | MIT 74 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config 2 | # syntax=docker/dockerfile:1 3 | FROM node:lts-alpine 4 | WORKDIR /app 5 | # Install dependencies without running prepare scripts 6 | COPY package.json package-lock.json ./ 7 | RUN npm ci --ignore-scripts 8 | # Copy source code 9 | COPY . . 10 | # Build the project 11 | RUN npm run build 12 | # Default command to start the MCP server 13 | CMD ["node", "build/index.js"] 14 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config 2 | 3 | startCommand: 4 | type: stdio 5 | commandFunction: 6 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 7 | |- 8 | (config) => ({ command: 'node', args: ['build/index.js'], env: { SENTRY_HOST: config.sentryHost, SENTRY_ORG: config.sentryOrg, SENTRY_PROJ: config.sentryProj, SENTRY_USER_TOKEN: config.sentryUserToken } }) 9 | configSchema: 10 | # JSON Schema defining the configuration options for the MCP. 11 | type: object 12 | required: 13 | - sentryHost 14 | - sentryOrg 15 | - sentryProj 16 | - sentryUserToken 17 | properties: 18 | sentryHost: 19 | type: string 20 | description: Sentry host URL, e.g., https://sentry.io 21 | sentryOrg: 22 | type: string 23 | description: Sentry organization slug or ID 24 | sentryProj: 25 | type: string 26 | description: Sentry project slug or ID 27 | sentryUserToken: 28 | type: string 29 | description: Sentry user auth token 30 | exampleConfig: 31 | sentryHost: https://sentry.io 32 | sentryOrg: my-org 33 | sentryProj: my-project 34 | sentryUserToken: abcd1234efgh5678 35 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "sentry-issues-mcp", 3 | "version": "1.0.5", 4 | "description": "a mcp server to get sentry issue by id or url for LLM", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc && chmod 755 build/index.js", 9 | "start": "node build/index.js", 10 | "debug": "npm run build && npx -y @modelcontextprotocol/inspector -e SENTRY_PROJ=<your_sentry_proj> -e SENTRY_HOST=<your_sentry_host> -e SENTRY_ORG=<your_sentry_org> -e SENTRY_USER_TOKEN=<your_sentry_user_token> node ./build/index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Leee62/sentry-issues-mcp.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/Leee62/sentry-issues-mcp/issues" 18 | }, 19 | "homepage": "https://github.com/Leee62/sentry-issues-mcp#readme", 20 | "files": [ 21 | "build" 22 | ], 23 | "bin": { 24 | "sentry-issues-mcp": "./build/index.js" 25 | }, 26 | "keywords": [ 27 | "mcp", 28 | "sentry", 29 | "llm", 30 | "modelcontextprotocol" 31 | ], 32 | "author": "lee62", 33 | "license": "MIT", 34 | "dependencies": { 35 | "@modelcontextprotocol/sdk": "^1.9.0", 36 | "zod": "^3.24.2" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^22.14.1", 40 | "typescript": "^5.8.3" 41 | } 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/fetcher.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** get sentry event by <ID> */ 2 | export async function fetchSentryEvent<T>( 3 | eventId: string, 4 | organization_id_or_slug: string, 5 | project_id_or_slug: string 6 | ): Promise<T | null> { 7 | try { 8 | const issueRes = await fetch( 9 | `https://${process.env.SENTRY_HOST}/api/0/projects/${organization_id_or_slug}/${project_id_or_slug}/events/${eventId}/`, 10 | { 11 | method: "GET", 12 | headers: { 13 | Authorization: `Bearer ${process.env.SENTRY_USER_TOKEN}`, 14 | }, 15 | } 16 | ); 17 | 18 | if (!issueRes.ok) { 19 | throw new Error(`HTTP error! status: ${issueRes.status}`); 20 | } 21 | return (await issueRes.json()) as T; 22 | } catch (error) { 23 | console.error("Error making request:", error); 24 | return null; 25 | } 26 | } 27 | 28 | /** get sentry events */ 29 | export async function fetchSentryEvents<T>( 30 | organization_id_or_slug: string, 31 | project_id_or_slug: string 32 | ): Promise<T | null> { 33 | try { 34 | const issueRes = await fetch( 35 | `https://${process.env.SENTRY_HOST}/api/0/projects/${organization_id_or_slug}/${project_id_or_slug}/events/`, 36 | { 37 | method: "GET", 38 | headers: { 39 | Authorization: `Bearer ${process.env.SENTRY_USER_TOKEN}`, 40 | }, 41 | } 42 | ); 43 | 44 | if (!issueRes.ok) { 45 | throw new Error(`HTTP error! status: ${issueRes.status}`); 46 | } 47 | return (await issueRes.json()) as T; 48 | } catch (error) { 49 | console.error("Error making request:", error); 50 | return null; 51 | } 52 | } 53 | 54 | // /** get sentry issue by <ID> */ 55 | // export async function fetchSentryIssue<T>( 56 | // issueId: string, 57 | // organization_id_or_slug: string 58 | // ): Promise<T | null> { 59 | // try { 60 | // const issueRes = await fetch( 61 | // `https://${process.env.SENTRY_HOST}/api/0/organizations/${organization_id_or_slug}/issues/${issueId}/`, 62 | // { 63 | // method: "GET", 64 | // headers: { 65 | // Authorization: `Bearer ${process.env.SENTRY_USER_TOKEN}`, 66 | // }, 67 | // } 68 | // ); 69 | 70 | // if (!issueRes.ok) { 71 | // throw new Error(`HTTP error! status: ${issueRes.status}`); 72 | // } 73 | // return (await issueRes.json()) as T; 74 | // } catch (error) { 75 | // console.error("Error making request:", error); 76 | // return null; 77 | // } 78 | // } 79 | 80 | // /** get sentry issue */ 81 | // export async function fetchSentryIssues<T>( 82 | // project_id_or_slug: string, 83 | // organization_id_or_slug: string 84 | // ): Promise<T | null> { 85 | // const path = project_id_or_slug 86 | // ? `projects/${organization_id_or_slug}/${project_id_or_slug}/issues/` 87 | // : `organizations/${organization_id_or_slug}/issues/`; 88 | 89 | // try { 90 | // const issueRes = await fetch( 91 | // `https://${process.env.SENTRY_HOST}/api/0/${path}`, 92 | // { 93 | // method: "GET", 94 | // headers: { 95 | // Authorization: `Bearer ${process.env.SENTRY_USER_TOKEN}`, 96 | // }, 97 | // } 98 | // ); 99 | 100 | // if (!issueRes.ok) { 101 | // throw new Error(`HTTP error! status: ${issueRes.status}`); 102 | // } 103 | // return (await issueRes.json()) as T; 104 | // } catch (error) { 105 | // console.error("Error making request:", error); 106 | // return null; 107 | // } 108 | // } 109 | ``` -------------------------------------------------------------------------------- /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 { 7 | fetchSentryEvent, 8 | fetchSentryEvents, 9 | // fetchSentryIssue, 10 | // fetchSentryIssues, 11 | } from "./fetcher"; 12 | 13 | /** Create server instance 14 | * 15 | * Watch out LLM TOKEN usage, too much information returned 16 | */ 17 | const server = new McpServer({ 18 | name: "sentry-issue-mcp", 19 | version: "1.0.0", 20 | }); 21 | 22 | /** Register tool for getting single event by id */ 23 | server.tool( 24 | "get_single_event", 25 | "get issue event by inputting sentry issue event url or sentry issue event id", 26 | { 27 | url_or_id: z 28 | .string() 29 | .describe("sentry issue event url or sentry issue event id"), 30 | organization_id_or_slug: z 31 | .string() 32 | .optional() 33 | .default(process.env.SENTRY_ORG as string) 34 | .describe("sentry organization id or slug, it can be undefined"), 35 | project_id_or_slug: z 36 | .string() 37 | .optional() 38 | .default(process.env.SENTRY_PROJ as string) 39 | .describe("sentry project name or slug, it can be undefined"), 40 | mode: z 41 | .enum(["tiny", "huge"]) 42 | .optional() 43 | .default("tiny") 44 | .describe( 45 | "mode for output, it can be undefined, it used to control LLM token usage" 46 | ), 47 | }, 48 | async ({ url_or_id, organization_id_or_slug, project_id_or_slug, mode }) => { 49 | let EVENT_ID = ""; 50 | 51 | if (url_or_id.includes("http") || url_or_id.includes("https")) { 52 | EVENT_ID = url_or_id.match(/events\/([a-f0-9]+)/)?.[1] || ""; 53 | } else { 54 | EVENT_ID = url_or_id; 55 | } 56 | 57 | if (!EVENT_ID) { 58 | return { 59 | content: [ 60 | { 61 | type: "text", 62 | text: "Invalid Event ID", 63 | }, 64 | ], 65 | }; 66 | } 67 | 68 | const eventEventData = await fetchSentryEvent<{ 69 | entries: any[]; 70 | }>(EVENT_ID, organization_id_or_slug, project_id_or_slug); 71 | 72 | if (!eventEventData) { 73 | return { 74 | content: [ 75 | { 76 | type: "text", 77 | text: "Failed to Get Event", 78 | }, 79 | ], 80 | }; 81 | } 82 | 83 | return { 84 | content: [ 85 | { 86 | type: "text", 87 | text: JSON.stringify( 88 | mode === "tiny" ? eventEventData.entries : eventEventData 89 | ), 90 | }, 91 | ], 92 | }; 93 | } 94 | ); 95 | 96 | /** Register tool for getting events */ 97 | server.tool( 98 | "get_project_events", 99 | "get issue events by inputting sentry organization id or slug and sentry project name or slug", 100 | { 101 | organization_id_or_slug: z 102 | .string() 103 | .optional() 104 | .default(process.env.SENTRY_ORG as string) 105 | .describe("sentry organization id or slug, it can be undefined"), 106 | project_id_or_slug: z 107 | .string() 108 | .optional() 109 | .default(process.env.SENTRY_PROJ as string) 110 | .describe("sentry project name or slug, it can be undefined"), 111 | mode: z 112 | .enum(["tiny", "huge"]) 113 | .optional() 114 | .default("tiny") 115 | .describe( 116 | "mode for output, it can be undefined, it used to control LLM token usage" 117 | ), 118 | }, 119 | async ({ organization_id_or_slug, project_id_or_slug, mode }) => { 120 | const eventsData = await fetchSentryEvents< 121 | { id: string; title: string; dateCreated: string }[] 122 | >(organization_id_or_slug, project_id_or_slug); 123 | 124 | if (!eventsData) { 125 | return { 126 | content: [ 127 | { 128 | type: "text", 129 | text: "Failed to Get Issue", 130 | }, 131 | ], 132 | }; 133 | } 134 | 135 | return { 136 | content: [ 137 | { 138 | type: "text", 139 | text: JSON.stringify( 140 | mode === "tiny" 141 | ? eventsData.map(({ id, title, dateCreated }) => ({ 142 | id, 143 | title, 144 | dateCreated, 145 | })) 146 | : eventsData 147 | ), 148 | }, 149 | ], 150 | }; 151 | } 152 | ); 153 | 154 | // /** Register sentry tool for getting single issue by id */ 155 | // server.tool( 156 | // "get_single_issue", 157 | // "get issue by inputting sentry issue url or sentry issue id", 158 | // { 159 | // url_or_id: z.string().describe("sentry issue url or sentry issue id"), 160 | // organization_id_or_slug: z 161 | // .string() 162 | // .optional() 163 | // .describe("sentry organization id or slug, it can be undefined"), 164 | // }, 165 | // async ({ 166 | // url_or_id, 167 | // organization_id_or_slug = process.env.SENTRY_ORG as string, 168 | // }) => { 169 | // let ISSUE_ID = ""; 170 | 171 | // if (url_or_id.includes("http") || url_or_id.includes("https")) { 172 | // ISSUE_ID = url_or_id.match(/issues\/(\d+)/)?.[1] || ""; 173 | // } else { 174 | // ISSUE_ID = url_or_id; 175 | // } 176 | 177 | // if (!ISSUE_ID || Number.isNaN(Number(ISSUE_ID))) { 178 | // return { 179 | // content: [ 180 | // { 181 | // type: "text", 182 | // text: "Invalid Issue ID", 183 | // }, 184 | // ], 185 | // }; 186 | // } 187 | 188 | // const issueData = await fetchSentryIssue(ISSUE_ID, organization_id_or_slug); 189 | 190 | // if (!issueData) { 191 | // return { 192 | // content: [ 193 | // { 194 | // type: "text", 195 | // text: "Failed to Get Issue", 196 | // }, 197 | // ], 198 | // }; 199 | // } 200 | 201 | // return { 202 | // content: [ 203 | // { 204 | // type: "text", 205 | // text: JSON.stringify(issueData), 206 | // }, 207 | // ], 208 | // }; 209 | // } 210 | // ); 211 | 212 | // /** Register Sentry get issues (Watch out LLM TOKEN usage) */ 213 | // server.tool( 214 | // "get_project_issues", 215 | // "get issues by inputting", 216 | // { 217 | // organization_id_or_slug: z 218 | // .string() 219 | // .optional() 220 | // .describe("sentry organization id or slug, it can be undefined"), 221 | // project_id_or_slug: z 222 | // .string() 223 | // .optional() 224 | // .describe("sentry project name or slug, it can be undefined"), 225 | // }, 226 | // async ({ 227 | // project_id_or_slug = process.env.SENTRY_PROJ as string, 228 | // organization_id_or_slug = process.env.SENTRY_ORG as string, 229 | // }) => { 230 | // const issuesData = await fetchSentryIssues( 231 | // project_id_or_slug, 232 | // organization_id_or_slug 233 | // ); 234 | 235 | // if (!issuesData) { 236 | // return { 237 | // content: [ 238 | // { 239 | // type: "text", 240 | // text: "Failed to Get Issue", 241 | // }, 242 | // ], 243 | // }; 244 | // } 245 | 246 | // return { 247 | // content: [ 248 | // { 249 | // type: "text", 250 | // text: JSON.stringify(issuesData), 251 | // }, 252 | // ], 253 | // }; 254 | // } 255 | // ); 256 | 257 | async function main() { 258 | const transport = new StdioServerTransport(); 259 | await server.connect(transport); 260 | } 261 | 262 | main().catch((error) => { 263 | console.error("Fatal error in main():", error); 264 | process.exit(1); 265 | }); 266 | ```