@webhooks-cc/mcp 1.12.0 → 1.13.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/README.md +14 -3
- package/dist/bin/mcp.js +98 -4
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +96 -2
- package/dist/index.mjs +100 -3
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -11,6 +11,16 @@ The server reads `WHK_API_KEY` from the environment. It also supports:
|
|
|
11
11
|
- `WHK_BASE_URL` for a custom API base URL
|
|
12
12
|
- `WHK_WEBHOOK_URL` for a custom receiver base URL
|
|
13
13
|
|
|
14
|
+
### Claude Code plugin
|
|
15
|
+
|
|
16
|
+
The plugin installs the server together with a webhook testing skill that tells Claude when to use it:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
/plugin install webhooks-cc --marketplace kroqdotdev/webhooks-cc
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For other agents, `npx skills add kroqdotdev/webhooks-cc` installs the same skill. See [Agent skill](https://webhooks.cc/docs/mcp/skills).
|
|
23
|
+
|
|
14
24
|
### One-click
|
|
15
25
|
|
|
16
26
|
- **Cursor**: [Add to Cursor](https://cursor.com/en/install-mcp?name=webhooks-cc&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3ZWJob29rcy1jYy9tY3AiXSwiZW52Ijp7IldIS19BUElfS0VZIjoid2hjY18uLi4ifX0=)
|
|
@@ -54,11 +64,11 @@ npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
|
|
|
54
64
|
}
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
Get your API key at [webhooks.cc/account](https://webhooks.cc/account).
|
|
67
|
+
Get your API key at [webhooks.cc/account/api-keys](https://webhooks.cc/account/api-keys).
|
|
58
68
|
|
|
59
69
|
## What it exposes
|
|
60
70
|
|
|
61
|
-
The server exposes `
|
|
71
|
+
The server exposes `39` tools, `3` prompts, and `3` resource surfaces with an API key, and `11` sandbox tools without one.
|
|
62
72
|
|
|
63
73
|
### Endpoint tools
|
|
64
74
|
|
|
@@ -80,6 +90,7 @@ The server exposes `38` tools, `3` prompts, and `3` resource surfaces with an AP
|
|
|
80
90
|
- `wait_for_requests`
|
|
81
91
|
- `replay_request`
|
|
82
92
|
- `compare_requests`
|
|
93
|
+
- `generate_payload_schema` (TypeScript, Zod, Pydantic, or JSON Schema from captured bodies)
|
|
83
94
|
- `extract_from_request`
|
|
84
95
|
- `verify_signature`
|
|
85
96
|
- `clear_requests`
|
|
@@ -132,7 +143,7 @@ Endpoints shared with you already appear in `list_endpoints` (with `fromTeam`) a
|
|
|
132
143
|
|
|
133
144
|
Started without `WHK_API_KEY`, the server registers sandbox tools instead ([auth.md](https://webhooks.cc/auth.md)):
|
|
134
145
|
|
|
135
|
-
- `create_endpoint`, `list_endpoints`, `get_endpoint`, `delete_endpoint`, `list_requests`, `get_request`, `wait_for_request`: the same names and arguments as the full tools, in a sandbox of up to 3 endpoints, 25 captured requests each and 100 in all, for 24 hours. The first `create_endpoint` registers it (a few seconds of proof of work).
|
|
146
|
+
- `create_endpoint`, `list_endpoints`, `get_endpoint`, `delete_endpoint`, `list_requests`, `get_request`, `wait_for_request`, `generate_payload_schema`: the same names and arguments as the full tools, in a sandbox of up to 3 endpoints, 25 captured requests each and 100 in all, for 24 hours. The first `create_endpoint` registers it (a few seconds of proof of work).
|
|
136
147
|
- `connect_account`: returns a link and a 6-digit code for a human to connect the agent to their account.
|
|
137
148
|
- `wait_for_connection`: waits for the human, then replaces the sandbox tools with the full tools for that account (`notifications/tools/list_changed`).
|
|
138
149
|
- `about_sandbox`: the limits and how to connect, without a network call.
|
package/dist/bin/mcp.js
CHANGED
|
@@ -76,6 +76,7 @@ function registerPrompts(server) {
|
|
|
76
76
|
"If a target URL is provided, use preview_webhook before send_to so the request shape is visible.",
|
|
77
77
|
targetUrl ? `Target URL: ${targetUrl}.` : null,
|
|
78
78
|
"Send a realistic provider webhook, wait for capture, and verify the signature if a secret is available.",
|
|
79
|
+
"Once real deliveries are captured, use generate_payload_schema to write the payload types for the handler.",
|
|
79
80
|
"Return the endpoint URL, the exact tools used, and the next step for the developer."
|
|
80
81
|
].filter(Boolean).join(" ")
|
|
81
82
|
}
|
|
@@ -157,6 +158,13 @@ function omitHeaders(holder) {
|
|
|
157
158
|
holder.headers = {};
|
|
158
159
|
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
159
160
|
}
|
|
161
|
+
function trimSpam(holder) {
|
|
162
|
+
const spam = holder.spam;
|
|
163
|
+
if (spam && typeof spam === "object" && !Array.isArray(spam)) {
|
|
164
|
+
const { score, required, action, error } = spam;
|
|
165
|
+
holder.spam = { score, required, action, error };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
160
168
|
function capExtracts(holder) {
|
|
161
169
|
if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
|
|
162
170
|
holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
|
|
@@ -221,7 +229,8 @@ var ESSENTIAL_EMAIL_FIELDS = [
|
|
|
221
229
|
"date",
|
|
222
230
|
"text",
|
|
223
231
|
"textTruncated",
|
|
224
|
-
"htmlSize"
|
|
232
|
+
"htmlSize",
|
|
233
|
+
"spam"
|
|
225
234
|
];
|
|
226
235
|
function compactRequest(request) {
|
|
227
236
|
if (request.kind !== "email") return request;
|
|
@@ -248,6 +257,7 @@ function compactRequest(request) {
|
|
|
248
257
|
const budget = MAX_OUTPUT - 128;
|
|
249
258
|
shrinkToFit(root, budget, [
|
|
250
259
|
() => omitHeaders(compact),
|
|
260
|
+
() => trimSpam(email),
|
|
251
261
|
() => capLists(email),
|
|
252
262
|
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
253
263
|
// Down to the essentials there may be room for more of the text again.
|
|
@@ -484,6 +494,67 @@ var waitForRequestShape = {
|
|
|
484
494
|
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
485
495
|
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
|
|
486
496
|
};
|
|
497
|
+
var payloadSchemaShape = {
|
|
498
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
499
|
+
format: import_zod2.z.enum(import_sdk.PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
|
|
500
|
+
'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
|
|
501
|
+
),
|
|
502
|
+
name: import_zod2.z.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
|
|
503
|
+
event: import_zod2.z.string().optional().describe(
|
|
504
|
+
'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
|
|
505
|
+
),
|
|
506
|
+
method: import_zod2.z.string().optional().describe("Only requests with this HTTP method"),
|
|
507
|
+
path: import_zod2.z.string().optional().describe("Only requests with this path"),
|
|
508
|
+
requestIds: import_zod2.z.array(import_zod2.z.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
|
|
509
|
+
limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
|
|
510
|
+
};
|
|
511
|
+
function eventName(event) {
|
|
512
|
+
const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
513
|
+
const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
514
|
+
if (!name) return "WebhookPayload";
|
|
515
|
+
return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
|
|
516
|
+
}
|
|
517
|
+
function payloadSchemaResult(requests, args2) {
|
|
518
|
+
const method = args2.method?.toUpperCase();
|
|
519
|
+
const matching = requests.filter(
|
|
520
|
+
(request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args2.path || request.path === args2.path) && (!args2.event || (0, import_sdk.detectWebhookInfo)(request)?.event === args2.event)
|
|
521
|
+
);
|
|
522
|
+
if (matching.length === 0) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const events = [
|
|
528
|
+
...new Set(
|
|
529
|
+
matching.map((request) => (0, import_sdk.detectWebhookInfo)(request)?.event).filter((e) => !!e)
|
|
530
|
+
)
|
|
531
|
+
];
|
|
532
|
+
const name = args2.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
|
|
533
|
+
const result = (0, import_sdk.generateRequestSchema)(matching, { format: args2.format, name });
|
|
534
|
+
if (result.samples === 0) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`None of the ${matching.length} matching requests has a JSON or form body to learn from.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const output = {
|
|
540
|
+
format: result.format,
|
|
541
|
+
name: result.name,
|
|
542
|
+
samples: result.samples,
|
|
543
|
+
skipped: result.skipped,
|
|
544
|
+
...events.length > 0 ? { events } : {},
|
|
545
|
+
...events.length > 1 ? {
|
|
546
|
+
warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
|
|
547
|
+
} : {},
|
|
548
|
+
code: result.code
|
|
549
|
+
};
|
|
550
|
+
const size = JSON.stringify(output, null, 2).length;
|
|
551
|
+
if (size > MAX_OUTPUT) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`The schema is ${result.code.length} characters, more than this tool can return. Narrow it with event or requestIds, or call generateRequestSchema() from @webhooks-cc/sdk in code.`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return jsonContent(output);
|
|
557
|
+
}
|
|
487
558
|
function textContent(text) {
|
|
488
559
|
return { content: [{ type: "text", text }] };
|
|
489
560
|
}
|
|
@@ -737,6 +808,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
737
808
|
"from",
|
|
738
809
|
"size",
|
|
739
810
|
"test",
|
|
811
|
+
"spam",
|
|
740
812
|
"text",
|
|
741
813
|
"textTruncated",
|
|
742
814
|
"htmlSize",
|
|
@@ -765,6 +837,7 @@ function emailDetail(client, email, view, includeHtml) {
|
|
|
765
837
|
};
|
|
766
838
|
shrinkToFit(detail, MAX_BODY_SIZE, [
|
|
767
839
|
() => omitHeaders(detail),
|
|
840
|
+
() => trimSpam(detail),
|
|
768
841
|
() => capExtracts(detail),
|
|
769
842
|
() => capLists(detail),
|
|
770
843
|
() => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
|
|
@@ -1178,6 +1251,15 @@ function registerTools(server, client) {
|
|
|
1178
1251
|
return jsonContent(diff);
|
|
1179
1252
|
})
|
|
1180
1253
|
);
|
|
1254
|
+
server.tool(
|
|
1255
|
+
"generate_payload_schema",
|
|
1256
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from captured request bodies. Several requests of one event teach which fields are optional or nullable, so filter with event (or pass requestIds) and write the result into the codebase instead of hand-typing the payload.",
|
|
1257
|
+
payloadSchemaShape,
|
|
1258
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args2 }) => {
|
|
1259
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
|
|
1260
|
+
return payloadSchemaResult(requests, args2);
|
|
1261
|
+
})
|
|
1262
|
+
);
|
|
1181
1263
|
server.tool(
|
|
1182
1264
|
"extract_from_request",
|
|
1183
1265
|
"Extract specific JSON fields from a captured request body.",
|
|
@@ -1868,6 +1950,18 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1868
1950
|
})
|
|
1869
1951
|
)
|
|
1870
1952
|
);
|
|
1953
|
+
registered.push(
|
|
1954
|
+
server.tool(
|
|
1955
|
+
"generate_payload_schema",
|
|
1956
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from the bodies of requests captured by a sandbox endpoint. Several requests of one event teach which fields are optional or nullable.",
|
|
1957
|
+
payloadSchemaShape,
|
|
1958
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args2 }) => {
|
|
1959
|
+
const sandbox = await session.requireSandbox();
|
|
1960
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => sandbox.requests.get(id))) : await sandbox.requests.list(endpointSlug, { limit });
|
|
1961
|
+
return payloadSchemaResult(requests, args2);
|
|
1962
|
+
})
|
|
1963
|
+
)
|
|
1964
|
+
);
|
|
1871
1965
|
registered.push(
|
|
1872
1966
|
server.tool(
|
|
1873
1967
|
"connect_account",
|
|
@@ -2023,7 +2117,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
2023
2117
|
}
|
|
2024
2118
|
|
|
2025
2119
|
// src/index.ts
|
|
2026
|
-
var VERSION = true ? "1.
|
|
2120
|
+
var VERSION = true ? "1.13.0" : "0.0.0-dev";
|
|
2027
2121
|
function createServer(options = {}) {
|
|
2028
2122
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2029
2123
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
|
@@ -2244,7 +2338,7 @@ function runSetup(args2) {
|
|
|
2244
2338
|
const apiKey = resolveApiKey(flags);
|
|
2245
2339
|
if (!apiKey) {
|
|
2246
2340
|
console.error("Error: API key required. Pass --api-key or set WHK_API_KEY env var.");
|
|
2247
|
-
console.error("Get your API key at https://webhooks.cc/account\n");
|
|
2341
|
+
console.error("Get your API key at https://webhooks.cc/account/api-keys\n");
|
|
2248
2342
|
process.exit(1);
|
|
2249
2343
|
}
|
|
2250
2344
|
validateApiKeyFormat(apiKey);
|
|
@@ -2299,7 +2393,7 @@ Setup:
|
|
|
2299
2393
|
npx @webhooks-cc/mcp setup windsurf --api-key whcc_...
|
|
2300
2394
|
npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
|
|
2301
2395
|
|
|
2302
|
-
Get your API key at https://webhooks.cc/account
|
|
2396
|
+
Get your API key at https://webhooks.cc/account/api-keys
|
|
2303
2397
|
`);
|
|
2304
2398
|
} else if (command && command !== "--") {
|
|
2305
2399
|
console.error(`Unknown command: "${command}". Run with --help for usage.`);
|
package/dist/index.d.mts
CHANGED
|
@@ -53,4 +53,5 @@ interface CreateServerOptions {
|
|
|
53
53
|
*/
|
|
54
54
|
declare function createServer(options?: CreateServerOptions): McpServer;
|
|
55
55
|
|
|
56
|
-
export {
|
|
56
|
+
export { createServer, registerAgentRegistrationTools, registerPrompts, registerResources, registerSandboxTools, registerTools };
|
|
57
|
+
export type { CreateServerOptions, SandboxToolOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -53,4 +53,5 @@ interface CreateServerOptions {
|
|
|
53
53
|
*/
|
|
54
54
|
declare function createServer(options?: CreateServerOptions): McpServer;
|
|
55
55
|
|
|
56
|
-
export {
|
|
56
|
+
export { createServer, registerAgentRegistrationTools, registerPrompts, registerResources, registerSandboxTools, registerTools };
|
|
57
|
+
export type { CreateServerOptions, SandboxToolOptions };
|
package/dist/index.js
CHANGED
|
@@ -99,6 +99,7 @@ function registerPrompts(server) {
|
|
|
99
99
|
"If a target URL is provided, use preview_webhook before send_to so the request shape is visible.",
|
|
100
100
|
targetUrl ? `Target URL: ${targetUrl}.` : null,
|
|
101
101
|
"Send a realistic provider webhook, wait for capture, and verify the signature if a secret is available.",
|
|
102
|
+
"Once real deliveries are captured, use generate_payload_schema to write the payload types for the handler.",
|
|
102
103
|
"Return the endpoint URL, the exact tools used, and the next step for the developer."
|
|
103
104
|
].filter(Boolean).join(" ")
|
|
104
105
|
}
|
|
@@ -180,6 +181,13 @@ function omitHeaders(holder) {
|
|
|
180
181
|
holder.headers = {};
|
|
181
182
|
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
182
183
|
}
|
|
184
|
+
function trimSpam(holder) {
|
|
185
|
+
const spam = holder.spam;
|
|
186
|
+
if (spam && typeof spam === "object" && !Array.isArray(spam)) {
|
|
187
|
+
const { score, required, action, error } = spam;
|
|
188
|
+
holder.spam = { score, required, action, error };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
183
191
|
function capExtracts(holder) {
|
|
184
192
|
if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
|
|
185
193
|
holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
|
|
@@ -244,7 +252,8 @@ var ESSENTIAL_EMAIL_FIELDS = [
|
|
|
244
252
|
"date",
|
|
245
253
|
"text",
|
|
246
254
|
"textTruncated",
|
|
247
|
-
"htmlSize"
|
|
255
|
+
"htmlSize",
|
|
256
|
+
"spam"
|
|
248
257
|
];
|
|
249
258
|
function compactRequest(request) {
|
|
250
259
|
if (request.kind !== "email") return request;
|
|
@@ -271,6 +280,7 @@ function compactRequest(request) {
|
|
|
271
280
|
const budget = MAX_OUTPUT - 128;
|
|
272
281
|
shrinkToFit(root, budget, [
|
|
273
282
|
() => omitHeaders(compact),
|
|
283
|
+
() => trimSpam(email),
|
|
274
284
|
() => capLists(email),
|
|
275
285
|
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
276
286
|
// Down to the essentials there may be room for more of the text again.
|
|
@@ -507,6 +517,67 @@ var waitForRequestShape = {
|
|
|
507
517
|
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
508
518
|
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
|
|
509
519
|
};
|
|
520
|
+
var payloadSchemaShape = {
|
|
521
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
522
|
+
format: import_zod2.z.enum(import_sdk.PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
|
|
523
|
+
'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
|
|
524
|
+
),
|
|
525
|
+
name: import_zod2.z.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
|
|
526
|
+
event: import_zod2.z.string().optional().describe(
|
|
527
|
+
'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
|
|
528
|
+
),
|
|
529
|
+
method: import_zod2.z.string().optional().describe("Only requests with this HTTP method"),
|
|
530
|
+
path: import_zod2.z.string().optional().describe("Only requests with this path"),
|
|
531
|
+
requestIds: import_zod2.z.array(import_zod2.z.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
|
|
532
|
+
limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
|
|
533
|
+
};
|
|
534
|
+
function eventName(event) {
|
|
535
|
+
const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
536
|
+
const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
537
|
+
if (!name) return "WebhookPayload";
|
|
538
|
+
return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
|
|
539
|
+
}
|
|
540
|
+
function payloadSchemaResult(requests, args) {
|
|
541
|
+
const method = args.method?.toUpperCase();
|
|
542
|
+
const matching = requests.filter(
|
|
543
|
+
(request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args.path || request.path === args.path) && (!args.event || (0, import_sdk.detectWebhookInfo)(request)?.event === args.event)
|
|
544
|
+
);
|
|
545
|
+
if (matching.length === 0) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
const events = [
|
|
551
|
+
...new Set(
|
|
552
|
+
matching.map((request) => (0, import_sdk.detectWebhookInfo)(request)?.event).filter((e) => !!e)
|
|
553
|
+
)
|
|
554
|
+
];
|
|
555
|
+
const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
|
|
556
|
+
const result = (0, import_sdk.generateRequestSchema)(matching, { format: args.format, name });
|
|
557
|
+
if (result.samples === 0) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`None of the ${matching.length} matching requests has a JSON or form body to learn from.`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const output = {
|
|
563
|
+
format: result.format,
|
|
564
|
+
name: result.name,
|
|
565
|
+
samples: result.samples,
|
|
566
|
+
skipped: result.skipped,
|
|
567
|
+
...events.length > 0 ? { events } : {},
|
|
568
|
+
...events.length > 1 ? {
|
|
569
|
+
warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
|
|
570
|
+
} : {},
|
|
571
|
+
code: result.code
|
|
572
|
+
};
|
|
573
|
+
const size = JSON.stringify(output, null, 2).length;
|
|
574
|
+
if (size > MAX_OUTPUT) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`The schema is ${result.code.length} characters, more than this tool can return. Narrow it with event or requestIds, or call generateRequestSchema() from @webhooks-cc/sdk in code.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return jsonContent(output);
|
|
580
|
+
}
|
|
510
581
|
function textContent(text) {
|
|
511
582
|
return { content: [{ type: "text", text }] };
|
|
512
583
|
}
|
|
@@ -760,6 +831,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
760
831
|
"from",
|
|
761
832
|
"size",
|
|
762
833
|
"test",
|
|
834
|
+
"spam",
|
|
763
835
|
"text",
|
|
764
836
|
"textTruncated",
|
|
765
837
|
"htmlSize",
|
|
@@ -788,6 +860,7 @@ function emailDetail(client, email, view, includeHtml) {
|
|
|
788
860
|
};
|
|
789
861
|
shrinkToFit(detail, MAX_BODY_SIZE, [
|
|
790
862
|
() => omitHeaders(detail),
|
|
863
|
+
() => trimSpam(detail),
|
|
791
864
|
() => capExtracts(detail),
|
|
792
865
|
() => capLists(detail),
|
|
793
866
|
() => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
|
|
@@ -1201,6 +1274,15 @@ function registerTools(server, client) {
|
|
|
1201
1274
|
return jsonContent(diff);
|
|
1202
1275
|
})
|
|
1203
1276
|
);
|
|
1277
|
+
server.tool(
|
|
1278
|
+
"generate_payload_schema",
|
|
1279
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from captured request bodies. Several requests of one event teach which fields are optional or nullable, so filter with event (or pass requestIds) and write the result into the codebase instead of hand-typing the payload.",
|
|
1280
|
+
payloadSchemaShape,
|
|
1281
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
|
|
1282
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
|
|
1283
|
+
return payloadSchemaResult(requests, args);
|
|
1284
|
+
})
|
|
1285
|
+
);
|
|
1204
1286
|
server.tool(
|
|
1205
1287
|
"extract_from_request",
|
|
1206
1288
|
"Extract specific JSON fields from a captured request body.",
|
|
@@ -1891,6 +1973,18 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1891
1973
|
})
|
|
1892
1974
|
)
|
|
1893
1975
|
);
|
|
1976
|
+
registered.push(
|
|
1977
|
+
server.tool(
|
|
1978
|
+
"generate_payload_schema",
|
|
1979
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from the bodies of requests captured by a sandbox endpoint. Several requests of one event teach which fields are optional or nullable.",
|
|
1980
|
+
payloadSchemaShape,
|
|
1981
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
|
|
1982
|
+
const sandbox = await session.requireSandbox();
|
|
1983
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => sandbox.requests.get(id))) : await sandbox.requests.list(endpointSlug, { limit });
|
|
1984
|
+
return payloadSchemaResult(requests, args);
|
|
1985
|
+
})
|
|
1986
|
+
)
|
|
1987
|
+
);
|
|
1894
1988
|
registered.push(
|
|
1895
1989
|
server.tool(
|
|
1896
1990
|
"connect_account",
|
|
@@ -2049,7 +2143,7 @@ function registerAgentRegistrationTools(server) {
|
|
|
2049
2143
|
}
|
|
2050
2144
|
|
|
2051
2145
|
// src/index.ts
|
|
2052
|
-
var VERSION = true ? "1.
|
|
2146
|
+
var VERSION = true ? "1.13.0" : "0.0.0-dev";
|
|
2053
2147
|
function createServer(options = {}) {
|
|
2054
2148
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2055
2149
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
package/dist/index.mjs
CHANGED
|
@@ -70,6 +70,7 @@ function registerPrompts(server) {
|
|
|
70
70
|
"If a target URL is provided, use preview_webhook before send_to so the request shape is visible.",
|
|
71
71
|
targetUrl ? `Target URL: ${targetUrl}.` : null,
|
|
72
72
|
"Send a realistic provider webhook, wait for capture, and verify the signature if a secret is available.",
|
|
73
|
+
"Once real deliveries are captured, use generate_payload_schema to write the payload types for the handler.",
|
|
73
74
|
"Return the endpoint URL, the exact tools used, and the next step for the developer."
|
|
74
75
|
].filter(Boolean).join(" ")
|
|
75
76
|
}
|
|
@@ -151,6 +152,13 @@ function omitHeaders(holder) {
|
|
|
151
152
|
holder.headers = {};
|
|
152
153
|
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
153
154
|
}
|
|
155
|
+
function trimSpam(holder) {
|
|
156
|
+
const spam = holder.spam;
|
|
157
|
+
if (spam && typeof spam === "object" && !Array.isArray(spam)) {
|
|
158
|
+
const { score, required, action, error } = spam;
|
|
159
|
+
holder.spam = { score, required, action, error };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
154
162
|
function capExtracts(holder) {
|
|
155
163
|
if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
|
|
156
164
|
holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
|
|
@@ -215,7 +223,8 @@ var ESSENTIAL_EMAIL_FIELDS = [
|
|
|
215
223
|
"date",
|
|
216
224
|
"text",
|
|
217
225
|
"textTruncated",
|
|
218
|
-
"htmlSize"
|
|
226
|
+
"htmlSize",
|
|
227
|
+
"spam"
|
|
219
228
|
];
|
|
220
229
|
function compactRequest(request) {
|
|
221
230
|
if (request.kind !== "email") return request;
|
|
@@ -242,6 +251,7 @@ function compactRequest(request) {
|
|
|
242
251
|
const budget = MAX_OUTPUT - 128;
|
|
243
252
|
shrinkToFit(root, budget, [
|
|
244
253
|
() => omitHeaders(compact),
|
|
254
|
+
() => trimSpam(email),
|
|
245
255
|
() => capLists(email),
|
|
246
256
|
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
247
257
|
// Down to the essentials there may be room for more of the text again.
|
|
@@ -408,7 +418,10 @@ import {
|
|
|
408
418
|
verifySignature,
|
|
409
419
|
WebhooksCCError,
|
|
410
420
|
extractCode,
|
|
411
|
-
extractLink
|
|
421
|
+
extractLink,
|
|
422
|
+
detectWebhookInfo,
|
|
423
|
+
generateRequestSchema,
|
|
424
|
+
PAYLOAD_SCHEMA_FORMATS
|
|
412
425
|
} from "@webhooks-cc/sdk";
|
|
413
426
|
var MAX_BODY_SIZE = MAX_OUTPUT;
|
|
414
427
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
@@ -504,6 +517,67 @@ var waitForRequestShape = {
|
|
|
504
517
|
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
505
518
|
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
|
|
506
519
|
};
|
|
520
|
+
var payloadSchemaShape = {
|
|
521
|
+
endpointSlug: z2.string().describe("The endpoint slug"),
|
|
522
|
+
format: z2.enum(PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
|
|
523
|
+
'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
|
|
524
|
+
),
|
|
525
|
+
name: z2.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
|
|
526
|
+
event: z2.string().optional().describe(
|
|
527
|
+
'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
|
|
528
|
+
),
|
|
529
|
+
method: z2.string().optional().describe("Only requests with this HTTP method"),
|
|
530
|
+
path: z2.string().optional().describe("Only requests with this path"),
|
|
531
|
+
requestIds: z2.array(z2.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
|
|
532
|
+
limit: z2.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
|
|
533
|
+
};
|
|
534
|
+
function eventName(event) {
|
|
535
|
+
const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
536
|
+
const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
537
|
+
if (!name) return "WebhookPayload";
|
|
538
|
+
return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
|
|
539
|
+
}
|
|
540
|
+
function payloadSchemaResult(requests, args) {
|
|
541
|
+
const method = args.method?.toUpperCase();
|
|
542
|
+
const matching = requests.filter(
|
|
543
|
+
(request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args.path || request.path === args.path) && (!args.event || detectWebhookInfo(request)?.event === args.event)
|
|
544
|
+
);
|
|
545
|
+
if (matching.length === 0) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
const events = [
|
|
551
|
+
...new Set(
|
|
552
|
+
matching.map((request) => detectWebhookInfo(request)?.event).filter((e) => !!e)
|
|
553
|
+
)
|
|
554
|
+
];
|
|
555
|
+
const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
|
|
556
|
+
const result = generateRequestSchema(matching, { format: args.format, name });
|
|
557
|
+
if (result.samples === 0) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`None of the ${matching.length} matching requests has a JSON or form body to learn from.`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const output = {
|
|
563
|
+
format: result.format,
|
|
564
|
+
name: result.name,
|
|
565
|
+
samples: result.samples,
|
|
566
|
+
skipped: result.skipped,
|
|
567
|
+
...events.length > 0 ? { events } : {},
|
|
568
|
+
...events.length > 1 ? {
|
|
569
|
+
warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
|
|
570
|
+
} : {},
|
|
571
|
+
code: result.code
|
|
572
|
+
};
|
|
573
|
+
const size = JSON.stringify(output, null, 2).length;
|
|
574
|
+
if (size > MAX_OUTPUT) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`The schema is ${result.code.length} characters, more than this tool can return. Narrow it with event or requestIds, or call generateRequestSchema() from @webhooks-cc/sdk in code.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return jsonContent(output);
|
|
580
|
+
}
|
|
507
581
|
function textContent(text) {
|
|
508
582
|
return { content: [{ type: "text", text }] };
|
|
509
583
|
}
|
|
@@ -757,6 +831,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
757
831
|
"from",
|
|
758
832
|
"size",
|
|
759
833
|
"test",
|
|
834
|
+
"spam",
|
|
760
835
|
"text",
|
|
761
836
|
"textTruncated",
|
|
762
837
|
"htmlSize",
|
|
@@ -785,6 +860,7 @@ function emailDetail(client, email, view, includeHtml) {
|
|
|
785
860
|
};
|
|
786
861
|
shrinkToFit(detail, MAX_BODY_SIZE, [
|
|
787
862
|
() => omitHeaders(detail),
|
|
863
|
+
() => trimSpam(detail),
|
|
788
864
|
() => capExtracts(detail),
|
|
789
865
|
() => capLists(detail),
|
|
790
866
|
() => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
|
|
@@ -1198,6 +1274,15 @@ function registerTools(server, client) {
|
|
|
1198
1274
|
return jsonContent(diff);
|
|
1199
1275
|
})
|
|
1200
1276
|
);
|
|
1277
|
+
server.tool(
|
|
1278
|
+
"generate_payload_schema",
|
|
1279
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from captured request bodies. Several requests of one event teach which fields are optional or nullable, so filter with event (or pass requestIds) and write the result into the codebase instead of hand-typing the payload.",
|
|
1280
|
+
payloadSchemaShape,
|
|
1281
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
|
|
1282
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
|
|
1283
|
+
return payloadSchemaResult(requests, args);
|
|
1284
|
+
})
|
|
1285
|
+
);
|
|
1201
1286
|
server.tool(
|
|
1202
1287
|
"extract_from_request",
|
|
1203
1288
|
"Extract specific JSON fields from a captured request body.",
|
|
@@ -1888,6 +1973,18 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1888
1973
|
})
|
|
1889
1974
|
)
|
|
1890
1975
|
);
|
|
1976
|
+
registered.push(
|
|
1977
|
+
server.tool(
|
|
1978
|
+
"generate_payload_schema",
|
|
1979
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from the bodies of requests captured by a sandbox endpoint. Several requests of one event teach which fields are optional or nullable.",
|
|
1980
|
+
payloadSchemaShape,
|
|
1981
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
|
|
1982
|
+
const sandbox = await session.requireSandbox();
|
|
1983
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => sandbox.requests.get(id))) : await sandbox.requests.list(endpointSlug, { limit });
|
|
1984
|
+
return payloadSchemaResult(requests, args);
|
|
1985
|
+
})
|
|
1986
|
+
)
|
|
1987
|
+
);
|
|
1891
1988
|
registered.push(
|
|
1892
1989
|
server.tool(
|
|
1893
1990
|
"connect_account",
|
|
@@ -2046,7 +2143,7 @@ function registerAgentRegistrationTools(server) {
|
|
|
2046
2143
|
}
|
|
2047
2144
|
|
|
2048
2145
|
// src/index.ts
|
|
2049
|
-
var VERSION = true ? "1.
|
|
2146
|
+
var VERSION = true ? "1.13.0" : "0.0.0-dev";
|
|
2050
2147
|
function createServer(options = {}) {
|
|
2051
2148
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2052
2149
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webhooks-cc/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"mcpName": "io.github.kroqdotdev/webhooks-cc",
|
|
4
5
|
"description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"module": "dist/index.mjs",
|
|
@@ -31,20 +32,20 @@
|
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/kroqdotdev/webhooks-cc",
|
|
35
36
|
"directory": "packages/mcp"
|
|
36
37
|
},
|
|
37
38
|
"homepage": "https://webhooks.cc",
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
-
"zod": "^4.6.
|
|
41
|
-
"@webhooks-cc/sdk": "1.
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
41
|
+
"zod": "^4.6.7",
|
|
42
|
+
"@webhooks-cc/sdk": "1.17.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^26.6.
|
|
45
|
-
"tsup": "^
|
|
45
|
+
"@types/node": "^26.6.5",
|
|
46
|
+
"tsup": "^9.0.0",
|
|
46
47
|
"typescript": "^6.0.3",
|
|
47
|
-
"vitest": "^5.0.
|
|
48
|
+
"vitest": "^5.0.5"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsup",
|