@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 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 `38` tools, `3` prompts, and `3` resource surfaces with an API key, and `10` sandbox tools without one.
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.12.0" : "0.0.0-dev";
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 { type CreateServerOptions, type SandboxToolOptions, createServer, registerAgentRegistrationTools, registerPrompts, registerResources, registerSandboxTools, registerTools };
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 { type CreateServerOptions, type SandboxToolOptions, createServer, registerAgentRegistrationTools, registerPrompts, registerResources, registerSandboxTools, registerTools };
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.12.0" : "0.0.0-dev";
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.12.0" : "0.0.0-dev";
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.12.0",
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/webhooks-cc/webhooks-cc",
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.31.0",
40
- "zod": "^4.6.5",
41
- "@webhooks-cc/sdk": "1.14.0"
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.4",
45
- "tsup": "^8.5.1",
45
+ "@types/node": "^26.6.5",
46
+ "tsup": "^9.0.0",
46
47
  "typescript": "^6.0.3",
47
- "vitest": "^5.0.2"
48
+ "vitest": "^5.0.5"
48
49
  },
49
50
  "scripts": {
50
51
  "build": "tsup",