#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { registerTools } from "./tools.js";
import {
verifyWebhookSignature,
parseWebhookEvent,
formatEventMessage,
shouldProcessEvent,
} from "./webhooks.js";
const app = express();
// Load configuration from environment
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
if (!WEBHOOK_SECRET) {
console.error(
"WARNING: GITHUB_WEBHOOK_SECRET not set - webhooks will be rejected"
);
console.error("Set it in ~/.openclaw/secrets/github.env");
}
if (!DISCORD_WEBHOOK_URL) {
console.error(
"WARNING: DISCORD_WEBHOOK_URL not set - notifications will not be sent"
);
console.error("Set it in ~/.openclaw/secrets/github.env");
}
/**
* Send a message to Discord via webhook
*/
async function sendToDiscord(message: string): Promise<boolean> {
if (!DISCORD_WEBHOOK_URL) {
console.error("Cannot send to Discord: DISCORD_WEBHOOK_URL not configured");
return false;
}
try {
const response = await fetch(DISCORD_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: message,
username: "GitHub",
avatar_url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error(
`Discord webhook failed: ${response.status} ${response.statusText}`
);
console.error(`Response: ${errorText}`);
return false;
}
console.error("✓ Message sent to Discord");
return true;
} catch (error) {
console.error("Error sending to Discord:", error);
return false;
}
}
// Create MCP server
const server = new Server(
{
name: "github-mcp",
version: "0.3.0",
},
{
capabilities: {
tools: {},
},
}
);
// Register all GitHub tools
registerTools(server);
// SSE transport for MCP protocol
app.get("/sse", async (req, res) => {
console.error("Client connected via SSE");
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
// SSEServerTransport handles POST messages internally
// This is a placeholder - the SDK handles the routing
res.status(404).json({ error: "Use SSE endpoint" });
});
// Health check endpoint
app.get("/health", (req, res) => {
res.json({
status: "ok",
mode: "http",
server: "github-mcp",
version: "0.3.0",
});
});
// Webhook receiver endpoint
app.post(
"/webhooks/github",
express.json({ verify: (req: any, res, buf) => {
// Store raw body for signature verification
req.rawBody = buf.toString('utf8');
}}),
async (req: any, res) => {
const signature = req.headers["x-hub-signature-256"] as string;
const eventType = req.headers["x-github-event"] as string;
const deliveryId = req.headers["x-github-delivery"] as string;
console.error(
`Webhook received: ${eventType} (delivery: ${deliveryId})`
);
// Verify webhook secret is configured
if (!WEBHOOK_SECRET) {
console.error("Webhook rejected: GITHUB_WEBHOOK_SECRET not configured");
return res.status(500).json({
error: "Server configuration error",
message: "Webhook secret not configured",
});
}
// Verify signature
if (!verifyWebhookSignature(req.rawBody, signature, WEBHOOK_SECRET)) {
console.error(
`Webhook rejected: Invalid signature (delivery: ${deliveryId})`
);
return res.status(401).json({
error: "Unauthorized",
message: "Invalid webhook signature",
});
}
console.error("✓ Signature verified");
// Parse event
const event = parseWebhookEvent(eventType, req.body);
if (!event) {
console.error("Webhook rejected: Failed to parse event");
return res.status(400).json({
error: "Bad Request",
message: "Invalid webhook payload",
});
}
console.error(
`Event: ${event.type} / ${event.action || "no action"} from ${event.repository.full_name}`
);
// Check if we should process this event
if (!shouldProcessEvent(event)) {
console.error(`Event filtered out (noisy event type/action)`);
return res.status(200).json({
received: true,
processed: false,
reason: "Event filtered by configuration",
});
}
// Format message
const message = formatEventMessage(event);
console.error("Formatted message:");
console.error(message);
// Send to Discord
const sent = await sendToDiscord(message);
res.status(200).json({
received: true,
processed: true,
delivered: sent,
event: eventType,
action: event.action,
repository: event.repository.full_name,
});
}
);
// Start HTTP server
const port = parseInt(process.env.PORT || "9999");
app.listen(port, () => {
console.error(`GitHub MCP HTTP server listening on port ${port}`);
console.error(`SSE endpoint: http://localhost:${port}/sse`);
console.error(`Health check: http://localhost:${port}/health`);
console.error(`Webhooks: http://localhost:${port}/webhooks/github`);
});