From 8091c54528478e2cf45b3ff441226d05552827a9 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Wed, 18 Mar 2026 02:50:00 -0300 Subject: [PATCH 1/2] feat(github-mcp): publish webhook events to Event Bus - Add EVENT_BUS binding to StateSchema with scope - Publish webhook events to Event Bus after signature verification - Rename app from github-mcp to mcp-github Co-Authored-By: Claude Opus 4.6 --- github/app.json | 2 +- github/server/main.ts | 3 ++- github/server/types/env.ts | 10 ++++------ github/server/webhook.ts | 23 +++++++++++++++++++++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/github/app.json b/github/app.json index 738d52c8..17962913 100644 --- a/github/app.json +++ b/github/app.json @@ -1,6 +1,6 @@ { "scopeName": "deco", - "name": "github-mcp", + "name": "mcp-github", "friendlyName": "GitHub", "connection": { "type": "HTTP", diff --git a/github/server/main.ts b/github/server/main.ts index de81fd2a..6fcac92c 100644 --- a/github/server/main.ts +++ b/github/server/main.ts @@ -78,6 +78,7 @@ const runtime = withRuntime({ await captureInstallationMappings(token, connectionId); } }, + scopes: ["EVENT_BUS::*"], state: StateSchema, }, @@ -96,7 +97,7 @@ const wrappedFetch: typeof runtime.fetch = async (req, env, ctx) => { // GitHub webhook endpoint (unauthenticated — signature-verified instead) if (req.method === "POST" && url.pathname === "/webhooks/github") { - return handleGitHubWebhook(req); + return handleGitHubWebhook(req, env); } // Proxy MCP resource requests to upstream diff --git a/github/server/types/env.ts b/github/server/types/env.ts index 9f379728..07bc080c 100644 --- a/github/server/types/env.ts +++ b/github/server/types/env.ts @@ -3,14 +3,12 @@ */ import type { Registry } from "@decocms/mcps-shared/registry"; -import { type DefaultEnv } from "@decocms/runtime"; +import { BindingOf, type DefaultEnv } from "@decocms/runtime"; import { z } from "zod"; -/** - * State schema — no user-configurable options needed. - * The upstream URL is always https://api.githubcopilot.com/mcp/. - */ -export const StateSchema = z.object({}); +export const StateSchema = z.object({ + EVENT_BUS: BindingOf("@deco/event-bus"), +}); /** * Environment type combining Deco bindings with shared Registry diff --git a/github/server/webhook.ts b/github/server/webhook.ts index 43c93cd6..a4165d4c 100644 --- a/github/server/webhook.ts +++ b/github/server/webhook.ts @@ -2,16 +2,20 @@ * GitHub Webhook HTTP Handler * * Receives GitHub App webhook events, verifies signatures, - * and routes them to the correct connection. + * and publishes them to the Event Bus. */ import { getConnectionForInstallation } from "./lib/installation-map.ts"; import { verifyGitHubWebhook } from "./lib/webhook.ts"; import { hasMatchingTrigger } from "./lib/trigger-store.ts"; +import type { Env } from "./types/env.ts"; const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || ""; -export async function handleGitHubWebhook(req: Request): Promise { +export async function handleGitHubWebhook( + req: Request, + env: Env, +): Promise { const rawBody = await req.text(); const signatureHeader = req.headers.get("x-hub-signature-256"); @@ -49,6 +53,21 @@ export async function handleGitHubWebhook(req: Request): Promise { `[Webhook] ${fullEventType} | subject=${subject} | connection=${connectionId} | sender=${payload.sender?.login}`, ); + // Publish to Event Bus + const eventBus = env.MESH_REQUEST_CONTEXT?.state?.EVENT_BUS; + if (eventBus) { + try { + await eventBus.EVENT_PUBLISH({ + type: fullEventType, + subject, + data: payload, + }); + console.log(`[Webhook] Published to Event Bus: ${fullEventType}`); + } catch (error) { + console.error("[Webhook] Failed to publish to Event Bus:", error); + } + } + if ( hasMatchingTrigger( fullEventType, From 8d7bd5e1419138d784a08c078182106e374eea44 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Wed, 18 Mar 2026 02:56:24 -0300 Subject: [PATCH 2/2] fix(github-mcp): capture Event Bus ref in onChange for webhook publishing Co-Authored-By: Claude Opus 4.6 --- github/server/lib/event-bus.ts | 38 ++++++++++++++++++++++++++++++++++ github/server/main.ts | 9 +++++++- github/server/webhook.ts | 30 +++++++++++---------------- 3 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 github/server/lib/event-bus.ts diff --git a/github/server/lib/event-bus.ts b/github/server/lib/event-bus.ts new file mode 100644 index 00000000..e3ee633a --- /dev/null +++ b/github/server/lib/event-bus.ts @@ -0,0 +1,38 @@ +/** + * Event Bus reference cache. + * + * Captured during onChange (when the runtime has full state), + * so the webhook handler can publish events without going through + * the authenticated runtime path. + */ + +type EventPublishFn = (event: { + type: string; + subject: string; + data?: Record; +}) => Promise; + +let cachedPublish: EventPublishFn | null = null; + +export function setEventPublisher(publish: EventPublishFn): void { + cachedPublish = publish; +} + +export function clearEventPublisher(): void { + cachedPublish = null; +} + +export async function publishEvent(event: { + type: string; + subject: string; + data?: Record; +}): Promise { + if (!cachedPublish) { + console.warn( + "[EventBus] No publisher available — EVENT_BUS not yet configured", + ); + return; + } + + await cachedPublish(event); +} diff --git a/github/server/main.ts b/github/server/main.ts index 6fcac92c..caf193fe 100644 --- a/github/server/main.ts +++ b/github/server/main.ts @@ -9,6 +9,7 @@ import type { Registry } from "@decocms/mcps-shared/registry"; import { serve } from "@decocms/mcps-shared/serve"; import { withRuntime } from "@decocms/runtime"; import { exchangeCodeForToken } from "./lib/github-client.ts"; +import { setEventPublisher } from "./lib/event-bus.ts"; import { captureInstallationMappings } from "./lib/installation-map.ts"; import { handleProxiedRequest, @@ -72,6 +73,12 @@ const runtime = withRuntime({ onChange: async (env) => { invalidateUpstreamCache(); + // Capture Event Bus reference for webhook handler + const eventBus = env.MESH_REQUEST_CONTEXT?.state?.EVENT_BUS; + if (eventBus) { + setEventPublisher((event) => eventBus.EVENT_PUBLISH(event)); + } + const token = env.MESH_REQUEST_CONTEXT?.authorization; const connectionId = env.MESH_REQUEST_CONTEXT?.connectionId; if (token && connectionId) { @@ -97,7 +104,7 @@ const wrappedFetch: typeof runtime.fetch = async (req, env, ctx) => { // GitHub webhook endpoint (unauthenticated — signature-verified instead) if (req.method === "POST" && url.pathname === "/webhooks/github") { - return handleGitHubWebhook(req, env); + return handleGitHubWebhook(req); } // Proxy MCP resource requests to upstream diff --git a/github/server/webhook.ts b/github/server/webhook.ts index a4165d4c..d41e3205 100644 --- a/github/server/webhook.ts +++ b/github/server/webhook.ts @@ -5,17 +5,14 @@ * and publishes them to the Event Bus. */ +import { publishEvent } from "./lib/event-bus.ts"; import { getConnectionForInstallation } from "./lib/installation-map.ts"; -import { verifyGitHubWebhook } from "./lib/webhook.ts"; import { hasMatchingTrigger } from "./lib/trigger-store.ts"; -import type { Env } from "./types/env.ts"; +import { verifyGitHubWebhook } from "./lib/webhook.ts"; const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || ""; -export async function handleGitHubWebhook( - req: Request, - env: Env, -): Promise { +export async function handleGitHubWebhook(req: Request): Promise { const rawBody = await req.text(); const signatureHeader = req.headers.get("x-hub-signature-256"); @@ -54,18 +51,15 @@ export async function handleGitHubWebhook( ); // Publish to Event Bus - const eventBus = env.MESH_REQUEST_CONTEXT?.state?.EVENT_BUS; - if (eventBus) { - try { - await eventBus.EVENT_PUBLISH({ - type: fullEventType, - subject, - data: payload, - }); - console.log(`[Webhook] Published to Event Bus: ${fullEventType}`); - } catch (error) { - console.error("[Webhook] Failed to publish to Event Bus:", error); - } + try { + await publishEvent({ + type: fullEventType, + subject, + data: payload as Record, + }); + } catch (error) { + console.error("[Webhook] Failed to publish to Event Bus:", error); + return Response.json({ error: "Failed to publish event" }, { status: 502 }); } if (