Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions mcp-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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"
Expand Down
110 changes: 64 additions & 46 deletions mcp-server/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -14,6 +14,7 @@ import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import WebSocket from "ws";

const WORKER_URL =
process.env.WEBHOOK_WORKER_URL ||
Expand Down Expand Up @@ -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 ────────────────────────────────────────────────────────────────────
Expand All @@ -255,5 +273,5 @@ const transport = new StdioServerTransport();
await server.connect(transport);

if (CHANNEL_ENABLED) {
connectSSE();
connectWebSocket();
}
Loading