diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index 21f797d..71df240 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -1,16 +1,16 @@ { "name": "github-webhook-mcp", - "version": "1.0.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-webhook-mcp", - "version": "1.0.0", + "version": "0.5.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", - "eventsource": "^2.0.2" + "ws": "^8.18.0" }, "bin": { "github-webhook-mcp": "server/index.js" @@ -746,15 +746,6 @@ "node": ">= 0.6" } }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -1778,6 +1769,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", diff --git a/mcp-server/package.json b/mcp-server/package.json index cc67465..2bf528d 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "github-webhook-mcp", - "version": "1.0.0", + "version": "0.5.1", "description": "MCP server bridging GitHub webhooks via Cloudflare Worker", "type": "module", "bin": { @@ -18,7 +18,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", - "eventsource": "^2.0.2" + "ws": "^8.18.0" }, "devDependencies": { "@anthropic-ai/mcpb": "^2.1.0" diff --git a/mcp-server/server/index.js b/mcp-server/server/index.js index e89b59d..1803b54 100644 --- a/mcp-server/server/index.js +++ b/mcp-server/server/index.js @@ -4,7 +4,7 @@ * * Thin stdio MCP server that proxies tool calls to a remote * Cloudflare Worker + Durable Object backend via Streamable HTTP. - * Optionally listens to SSE for real-time channel notifications. + * Listens to WebSocket for real-time channel notifications. * * Discord MCP pattern: data lives in the cloud, local MCP is a thin bridge. */ @@ -14,6 +14,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; +import WebSocket from "ws"; const WORKER_URL = process.env.WEBHOOK_WORKER_URL || @@ -199,54 +200,71 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => { } }); -// ── SSE Listener → Channel Notifications ───────────────────────────────────── +// ── WebSocket Listener → Channel Notifications ────────────────────────────── -async function connectSSE() { - let EventSourceImpl; - try { - EventSourceImpl = (await import("eventsource")).default; - } catch { - // eventsource not installed — skip SSE - return; - } +function connectWebSocket() { + const wsUrl = WORKER_URL.replace(/^http/, "ws") + "/events"; + let ws; + let pingTimer = null; + + function connect() { + ws = new WebSocket(wsUrl); + + ws.on("open", () => { + // Send periodic pings to keep connection alive + pingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send("ping"); + } + }, 25000); + }); + + ws.on("message", (raw) => { + try { + const data = JSON.parse(raw.toString()); + + // Skip status, pong, heartbeat messages + if ("status" in data || "pong" in data || "heartbeat" in data) return; + if (!data.summary) return; - const es = new EventSourceImpl(`${WORKER_URL}/events`); - - es.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - if ("heartbeat" in data || "status" in data) return; - if (!data.summary) return; - - const s = data.summary; - const lines = [ - `[${s.type}] ${s.repo ?? ""}`, - s.action ? `action: ${s.action}` : null, - s.title ? `#${s.number ?? ""} ${s.title}` : null, - s.sender ? `by ${s.sender}` : null, - s.url ?? null, - ].filter(Boolean); - - server.notification({ - method: "notifications/claude/channel", - params: { - content: lines.join("\n"), - meta: { - chat_id: "github", - message_id: s.id, - user: s.sender ?? "github", - ts: s.received_at, + const s = data.summary; + const lines = [ + `[${s.type}] ${s.repo ?? ""}`, + s.action ? `action: ${s.action}` : null, + s.title ? `#${s.number ?? ""} ${s.title}` : null, + s.sender ? `by ${s.sender}` : null, + s.url ?? null, + ].filter(Boolean); + + server.notification({ + method: "notifications/claude/channel", + params: { + content: lines.join("\n"), + meta: { + chat_id: "github", + message_id: s.id, + user: s.sender ?? "github", + ts: s.received_at, + }, }, - }, - }); - } catch { - // Ignore parse errors - } - }; + }); + } catch { + // Ignore parse errors + } + }); + + ws.on("close", () => { + if (pingTimer) clearInterval(pingTimer); + // Reconnect after 5 seconds + setTimeout(connect, 5000); + }); + + ws.on("error", () => { + // Will trigger close event, which handles reconnect + }); + } - es.onerror = () => { - // EventSource auto-reconnects - }; + connect(); } // ── Start ──────────────────────────────────────────────────────────────────── @@ -255,5 +273,5 @@ const transport = new StdioServerTransport(); await server.connect(transport); if (CHANNEL_ENABLED) { - connectSSE(); + connectWebSocket(); }