All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m44s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m55s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
190 lines
6.7 KiB
TypeScript
190 lines
6.7 KiB
TypeScript
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<string, SSEServerTransport>();
|
|
|
|
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);
|
|
});
|