github-webhook-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +60 -0
- package/package.json +40 -0
- package/server/event-store.js +152 -0
- package/server/index.js +107 -0
package/manifest.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.3",
|
|
3
|
+
"name": "github-webhook-mcp",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Browse pending GitHub webhook events. Pairs with a webhook receiver that writes events.json.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Liplus Project"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"server": {
|
|
11
|
+
"type": "node",
|
|
12
|
+
"entry_point": "server/index.js",
|
|
13
|
+
"mcp_config": {
|
|
14
|
+
"command": "node",
|
|
15
|
+
"args": [
|
|
16
|
+
"${__dirname}/server/index.js"
|
|
17
|
+
],
|
|
18
|
+
"env": {
|
|
19
|
+
"EVENTS_JSON_PATH": "${user_config.events_json_path}"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"user_config": {
|
|
24
|
+
"events_json_path": {
|
|
25
|
+
"description": "Absolute path to the events.json file written by the webhook receiver.",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"required": true,
|
|
28
|
+
"title": "Events JSON Path"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"tools": [
|
|
32
|
+
{
|
|
33
|
+
"name": "get_pending_status",
|
|
34
|
+
"description": "Get a lightweight snapshot of pending GitHub webhook events."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "list_pending_events",
|
|
38
|
+
"description": "List lightweight summaries for pending GitHub webhook events."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "get_event",
|
|
42
|
+
"description": "Get the full payload for a single webhook event by ID."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "get_webhook_events",
|
|
46
|
+
"description": "Get pending (unprocessed) GitHub webhook events with full payloads."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "mark_processed",
|
|
50
|
+
"description": "Mark a webhook event as processed so it won't appear again."
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"compatibility": {
|
|
54
|
+
"platforms": [
|
|
55
|
+
"win32",
|
|
56
|
+
"darwin",
|
|
57
|
+
"linux"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "github-webhook-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for browsing GitHub webhook events",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"github-webhook-mcp": "server/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./server/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"server/",
|
|
12
|
+
"manifest.json"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node server/index.js",
|
|
16
|
+
"test": "node --test test/",
|
|
17
|
+
"pack:mcpb": "mcpb pack"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
|
+
"iconv-lite": "^0.6.3",
|
|
22
|
+
"zod": "^3.22.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@anthropic-ai/mcpb": "^2.1.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"github",
|
|
33
|
+
"webhook"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/Liplus-Project/github-webhook-mcp.git"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import iconv from "iconv-lite";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DEFAULT_DATA_FILE = resolve(__dirname, "..", "..", "events.json");
|
|
8
|
+
const PRIMARY_ENCODING = "utf-8";
|
|
9
|
+
const LEGACY_ENCODINGS = ["utf-8", "cp932", "shift_jis"];
|
|
10
|
+
|
|
11
|
+
function dataFilePath() {
|
|
12
|
+
return process.env.EVENTS_JSON_PATH || DEFAULT_DATA_FILE;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Load / Save ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function load() {
|
|
18
|
+
const filePath = dataFilePath();
|
|
19
|
+
if (!existsSync(filePath)) return [];
|
|
20
|
+
|
|
21
|
+
const raw = readFileSync(filePath);
|
|
22
|
+
|
|
23
|
+
// Try UTF-8 first (with BOM stripping)
|
|
24
|
+
try {
|
|
25
|
+
let text = raw.toString("utf-8");
|
|
26
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
27
|
+
const events = JSON.parse(text);
|
|
28
|
+
return events;
|
|
29
|
+
} catch {
|
|
30
|
+
// fall through to legacy encodings
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Try legacy encodings
|
|
34
|
+
for (const encoding of LEGACY_ENCODINGS) {
|
|
35
|
+
try {
|
|
36
|
+
const text = iconv.decode(raw, encoding);
|
|
37
|
+
const events = JSON.parse(text);
|
|
38
|
+
// Migrate to UTF-8
|
|
39
|
+
save(events);
|
|
40
|
+
return events;
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error(`Unable to decode event store: ${filePath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function save(events) {
|
|
50
|
+
const filePath = dataFilePath();
|
|
51
|
+
writeFileSync(filePath, JSON.stringify(events, null, 2), PRIMARY_ENCODING);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Query ───────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export function getPending() {
|
|
57
|
+
return load().filter((e) => !e.processed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getEvent(eventId) {
|
|
61
|
+
for (const event of load()) {
|
|
62
|
+
if (event.id === eventId) return event;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getPendingStatus() {
|
|
68
|
+
const pending = getPending();
|
|
69
|
+
const types = {};
|
|
70
|
+
for (const event of pending) {
|
|
71
|
+
types[event.type] = (types[event.type] || 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
pending_count: pending.length,
|
|
75
|
+
latest_received_at: pending.length > 0 ? pending[pending.length - 1].received_at : null,
|
|
76
|
+
types,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function eventNumber(payload) {
|
|
83
|
+
return (
|
|
84
|
+
payload.number ??
|
|
85
|
+
payload.issue?.number ??
|
|
86
|
+
payload.pull_request?.number ??
|
|
87
|
+
null
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function eventTitle(payload) {
|
|
92
|
+
return (
|
|
93
|
+
payload.issue?.title ??
|
|
94
|
+
payload.pull_request?.title ??
|
|
95
|
+
payload.discussion?.title ??
|
|
96
|
+
payload.check_run?.name ??
|
|
97
|
+
payload.workflow_run?.name ??
|
|
98
|
+
payload.workflow_job?.name ??
|
|
99
|
+
null
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function eventUrl(payload) {
|
|
104
|
+
return (
|
|
105
|
+
payload.issue?.html_url ??
|
|
106
|
+
payload.pull_request?.html_url ??
|
|
107
|
+
payload.discussion?.html_url ??
|
|
108
|
+
payload.check_run?.html_url ??
|
|
109
|
+
payload.workflow_run?.html_url ??
|
|
110
|
+
null
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function summarizeEvent(event) {
|
|
115
|
+
const payload = event.payload || {};
|
|
116
|
+
return {
|
|
117
|
+
id: event.id,
|
|
118
|
+
type: event.type,
|
|
119
|
+
received_at: event.received_at,
|
|
120
|
+
processed: event.processed,
|
|
121
|
+
trigger_status: event.trigger_status ?? null,
|
|
122
|
+
last_triggered_at: event.last_triggered_at ?? null,
|
|
123
|
+
action: payload.action ?? null,
|
|
124
|
+
repo: payload.repository?.full_name ?? null,
|
|
125
|
+
sender: payload.sender?.login ?? null,
|
|
126
|
+
number: eventNumber(payload),
|
|
127
|
+
title: eventTitle(payload),
|
|
128
|
+
url: eventUrl(payload),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getPendingSummaries(limit = 20) {
|
|
133
|
+
let pending = getPending();
|
|
134
|
+
if (limit > 0) {
|
|
135
|
+
pending = pending.slice(-limit);
|
|
136
|
+
}
|
|
137
|
+
return pending.map(summarizeEvent);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Mutation ────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export function markDone(eventId) {
|
|
143
|
+
const events = load();
|
|
144
|
+
for (const event of events) {
|
|
145
|
+
if (event.id === eventId) {
|
|
146
|
+
event.processed = true;
|
|
147
|
+
save(events);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
package/server/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
getPendingStatus,
|
|
7
|
+
getPendingSummaries,
|
|
8
|
+
getEvent,
|
|
9
|
+
getPending,
|
|
10
|
+
markDone,
|
|
11
|
+
} from "./event-store.js";
|
|
12
|
+
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "github-webhook-mcp",
|
|
15
|
+
version: "0.2.0",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// ── Tools ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
server.tool(
|
|
21
|
+
"get_pending_status",
|
|
22
|
+
"Get a lightweight snapshot of pending GitHub webhook events. Use this for periodic polling before requesting details.",
|
|
23
|
+
{},
|
|
24
|
+
async () => {
|
|
25
|
+
const status = getPendingStatus();
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
server.tool(
|
|
33
|
+
"list_pending_events",
|
|
34
|
+
"List lightweight summaries for pending GitHub webhook events. Returns metadata only, without full payloads.",
|
|
35
|
+
{
|
|
36
|
+
limit: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(100)
|
|
41
|
+
.default(20)
|
|
42
|
+
.describe("Maximum number of pending events to return"),
|
|
43
|
+
},
|
|
44
|
+
async ({ limit }) => {
|
|
45
|
+
const summaries = getPendingSummaries(limit);
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(summaries, null, 2) }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
server.tool(
|
|
53
|
+
"get_event",
|
|
54
|
+
"Get the full payload for a single webhook event by ID.",
|
|
55
|
+
{
|
|
56
|
+
event_id: z.string().describe("The event ID to retrieve"),
|
|
57
|
+
},
|
|
58
|
+
async ({ event_id }) => {
|
|
59
|
+
const event = getEvent(event_id);
|
|
60
|
+
if (event === null) {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: JSON.stringify({ error: "not_found", event_id }),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
server.tool(
|
|
77
|
+
"get_webhook_events",
|
|
78
|
+
"Get pending (unprocessed) GitHub webhook events with full payloads. Prefer get_pending_status or list_pending_events for polling.",
|
|
79
|
+
{},
|
|
80
|
+
async () => {
|
|
81
|
+
const events = getPending();
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
server.tool(
|
|
89
|
+
"mark_processed",
|
|
90
|
+
"Mark a webhook event as processed so it won't appear again.",
|
|
91
|
+
{
|
|
92
|
+
event_id: z.string().describe("The event ID to mark as processed"),
|
|
93
|
+
},
|
|
94
|
+
async ({ event_id }) => {
|
|
95
|
+
const success = markDone(event_id);
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{ type: "text", text: JSON.stringify({ success, event_id }) },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// ── Start ───────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const transport = new StdioServerTransport();
|
|
107
|
+
await server.connect(transport);
|