import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express, { Request, Response } from 'express'; import crypto from 'crypto'; 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 isStdio = process.argv.includes('--stdio'); if (isStdio) { const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); const transport = new StdioServerTransport(); await server.connect(transport); console.error('GlitchTip MCP server is running on stdio'); } else { const app = express(); const transports = new Map(); app.use((req, _res, next) => { console.error(`${req.method} ${req.url}`); next(); }); app.get('/sse', async (req, res) => { const sessionId = crypto.randomUUID(); console.error(`New SSE connection: ${sessionId}`); const transport = new SSEServerTransport(`/message/${sessionId}`, res); transports.set(sessionId, transport); req.on('close', () => { console.error(`SSE connection closed: ${sessionId}`); transports.delete(sessionId); }); await server.connect(transport); }); app.post('/message/:sessionId', async (req: Request, res: Response) => { const sessionId = req.params.sessionId; const transport = transports.get(sessionId as string); if (!transport) { console.error(`No transport found for session: ${sessionId}`); res.status(400).send('No active SSE connection for this session'); 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); });