From 29423123b3737153c83d961b287c7b813476ab3d Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 5 Mar 2026 11:16:23 +0100 Subject: [PATCH] feat(mcps): add glitchtip-mcp on port 3005 --- ecosystem.mcps.config.cjs | 8 ++ packages/glitchtip-mcp/package.json | 25 +++++ packages/glitchtip-mcp/src/index.ts | 157 +++++++++++++++++++++++++++ packages/glitchtip-mcp/src/start.ts | 13 +++ packages/glitchtip-mcp/tsconfig.json | 16 +++ pnpm-lock.yaml | 31 ++++++ 6 files changed, 250 insertions(+) create mode 100644 packages/glitchtip-mcp/package.json create mode 100644 packages/glitchtip-mcp/src/index.ts create mode 100644 packages/glitchtip-mcp/src/start.ts create mode 100644 packages/glitchtip-mcp/tsconfig.json diff --git a/ecosystem.mcps.config.cjs b/ecosystem.mcps.config.cjs index 1ca1cae..c8e91d2 100644 --- a/ecosystem.mcps.config.cjs +++ b/ecosystem.mcps.config.cjs @@ -28,5 +28,13 @@ module.exports = { cwd: './packages/serpbear-mcp', watch: false, }, + { + name: 'glitchtip-mcp', + script: 'node', + args: 'dist/start.js', + cwd: './packages/glitchtip-mcp', + watch: false, + }, ] }; + diff --git a/packages/glitchtip-mcp/package.json b/packages/glitchtip-mcp/package.json new file mode 100644 index 0000000..d008606 --- /dev/null +++ b/packages/glitchtip-mcp/package.json @@ -0,0 +1,25 @@ +{ + "name": "@mintel/glitchtip-mcp", + "version": "1.9.10", + "description": "GlitchTip Error Tracking MCP server for Mintel infrastructure", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/start.js", + "dev": "tsx watch src/index.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.5.0", + "axios": "^1.7.2", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.14.10", + "typescript": "^5.5.3", + "tsx": "^4.19.2" + } +} \ No newline at end of file diff --git a/packages/glitchtip-mcp/src/index.ts b/packages/glitchtip-mcp/src/index.ts new file mode 100644 index 0000000..fff499f --- /dev/null +++ b/packages/glitchtip-mcp/src/index.ts @@ -0,0 +1,157 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import express from 'express'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + Tool, +} from "@modelcontextprotocol/sdk/types.js"; +import axios from "axios"; +import https from "https"; + +const GLITCHTIP_BASE_URL = process.env.GLITCHTIP_BASE_URL || "https://glitchtip.infra.mintel.me"; +const GLITCHTIP_API_KEY = process.env.GLITCHTIP_API_KEY; + +if (!GLITCHTIP_API_KEY) { + console.error("Warning: GLITCHTIP_API_KEY is not set. API calls will fail."); +} + +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, // For internal infra +}); + +const glitchtipClient = axios.create({ + baseURL: `${GLITCHTIP_BASE_URL}/api/0`, + headers: { Authorization: `Bearer ${GLITCHTIP_API_KEY}` }, + httpsAgent +}); + +const LIST_PROJECTS_TOOL: Tool = { + name: "glitchtip_list_projects", + description: "List all projects and organizations in GlitchTip", + inputSchema: { type: "object", properties: {} }, +}; + +const LIST_ISSUES_TOOL: Tool = { + name: "glitchtip_list_issues", + description: "List issues (errors) for a specific project", + inputSchema: { + type: "object", + properties: { + organization_slug: { type: "string", description: "The organization slug" }, + project_slug: { type: "string", description: "The project slug" }, + query: { type: "string", description: "Optional query filter (e.g., 'is:unresolved')" }, + limit: { type: "number", description: "Maximum number of issues to return (default: 20)" }, + }, + required: ["organization_slug", "project_slug"], + }, +}; + +const GET_ISSUE_DETAILS_TOOL: Tool = { + name: "glitchtip_get_issue_details", + description: "Get detailed information about a specific issue, including stack trace", + inputSchema: { + type: "object", + properties: { + issue_id: { type: "string", description: "The ID of the issue" }, + }, + required: ["issue_id"], + }, +}; + +const UPDATE_ISSUE_TOOL: Tool = { + name: "glitchtip_update_issue", + description: "Update the status of an issue (e.g., resolve it)", + inputSchema: { + type: "object", + properties: { + issue_id: { type: "string", description: "The ID of the issue" }, + status: { type: "string", enum: ["resolved", "unresolved", "ignored"], description: "The new status" }, + }, + required: ["issue_id", "status"], + }, +}; + +const server = new Server( + { name: "glitchtip-mcp", version: "1.0.0" }, + { capabilities: { tools: {} } } +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + LIST_PROJECTS_TOOL, + LIST_ISSUES_TOOL, + GET_ISSUE_DETAILS_TOOL, + UPDATE_ISSUE_TOOL, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "glitchtip_list_projects") { + try { + const res = await glitchtipClient.get('/projects/'); + return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; + } catch (e: any) { + return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] }; + } + } + + if (request.params.name === "glitchtip_list_issues") { + const { organization_slug, project_slug, query, limit = 20 } = request.params.arguments as any; + try { + const res = await glitchtipClient.get(`/projects/${organization_slug}/${project_slug}/issues/`, { + params: { query, limit } + }); + return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; + } catch (e: any) { + return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] }; + } + } + + if (request.params.name === "glitchtip_get_issue_details") { + const { issue_id } = request.params.arguments as any; + try { + const res = await glitchtipClient.get(`/issues/${issue_id}/`); + return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; + } catch (e: any) { + return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] }; + } + } + + if (request.params.name === "glitchtip_update_issue") { + const { issue_id, status } = request.params.arguments as any; + try { + const res = await glitchtipClient.put(`/issues/${issue_id}/`, { status }); + return { content: [{ type: "text", text: `Issue ${issue_id} status updated to ${status}.` }] }; + } catch (e: any) { + return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] }; + } + } + + throw new Error(`Unknown tool: ${request.params.name}`); +}); + +async function run() { + const app = express(); + let transport: SSEServerTransport | null = null; + + app.get('/sse', async (req, res) => { + transport = new SSEServerTransport('/message', res); + await server.connect(transport); + }); + + app.post('/message', async (req, res) => { + if (!transport) { res.status(400).send('No active SSE connection'); return; } + await transport.handlePostMessage(req, res); + }); + + const PORT = process.env.GLITCHTIP_MCP_PORT || 3005; + app.listen(PORT, () => { + console.error(`GlitchTip MCP server running on http://localhost:${PORT}/sse`); + }); +} + +run().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/packages/glitchtip-mcp/src/start.ts b/packages/glitchtip-mcp/src/start.ts new file mode 100644 index 0000000..70a7a51 --- /dev/null +++ b/packages/glitchtip-mcp/src/start.ts @@ -0,0 +1,13 @@ +import { config } from 'dotenv'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +config({ path: resolve(__dirname, '../../../.env.local') }); +config({ path: resolve(__dirname, '../../../.env') }); + +import('./index.js').catch(err => { + console.error('Failed to start GlitchTip MCP Server:', err); + process.exit(1); +}); diff --git a/packages/glitchtip-mcp/tsconfig.json b/packages/glitchtip-mcp/tsconfig.json new file mode 100644 index 0000000..0e88912 --- /dev/null +++ b/packages/glitchtip-mcp/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc0df2f..5f8de54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -409,6 +409,37 @@ importers: specifier: ^5.5.3 version: 5.9.3 + packages/glitchtip-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.5.0 + version: 1.27.1(zod@3.25.76) + axios: + specifier: ^1.7.2 + version: 1.13.5 + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + express: + specifier: ^5.2.1 + version: 5.2.1 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^20.14.10 + version: 20.19.33 + tsx: + specifier: ^4.19.2 + version: 4.21.0 + typescript: + specifier: ^5.5.3 + version: 5.9.3 + packages/husky-config: dependencies: '@commitlint/config-conventional':