@webhooks-cc/mcp 1.12.1 → 1.14.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/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
  }
@@ -440,28 +441,75 @@ var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HT
440
441
  var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
441
442
  var EMAIL_SCAN_LIMIT = 100;
442
443
  var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
443
- var ruleConditionSchema = import_zod2.z.object({
444
- field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
445
- op: import_zod2.z.enum(["eq", "contains", "starts_with", "matches", "exists"]).describe("Comparison operator"),
446
- value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(
447
- `Value to compare against (required unless op is "exists", max ${import_sdk.MAX_CONDITION_VALUE_LEN} chars)`
444
+ var CONDITION_FIELD_NAMES = Object.keys(import_sdk.CONDITION_FIELDS);
445
+ var CONDITION_OP_NAMES = import_sdk.CONDITION_OPS;
446
+ function conditionSchema(use) {
447
+ const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => import_sdk.CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
448
+ return import_zod2.z.object({
449
+ field: import_zod2.z.enum(fields).describe(
450
+ use === "response" ? "What to read: method, path, header (name), query (name), body (text), body_path (path into a JSON or form body), content_type, size" : "What to read: method, path, header (name), query (name), body, body_path (path), content_type, size, kind (http or email), email (name: subject, from, to, text or tag), spam_score, email_auth (name: spf, dkim or dmarc), signature (verified, failed or missing)"
451
+ ),
452
+ op: import_zod2.z.enum(CONDITION_OP_NAMES).describe(
453
+ "eq, neq, contains, not_contains, starts_with, ends_with, matches (glob: * one path segment, ** any, ? one character), regex (RE2, searched anywhere; (?i) ignores case), exists, not_exists, gt, gte, lt, lte (numbers)"
454
+ ),
455
+ value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
456
+ name: import_zod2.z.string().max(import_sdk.MAX_CONDITION_NAME_LEN).optional().describe("Header or query parameter name; the email part; spf, dkim or dmarc"),
457
+ path: import_zod2.z.string().max(import_sdk.MAX_CONDITION_PATH_LEN).optional().describe('Dot path for body_path, e.g. data.object.status or items.0.sku or a["odd.key"]')
458
+ }).superRefine((condition, ctx) => {
459
+ for (const issue of (0, import_sdk.conditionIssues)(condition, { use })) {
460
+ ctx.addIssue({ code: "custom", message: issue });
461
+ }
462
+ });
463
+ }
464
+ var ruleConditionSchema = conditionSchema("response");
465
+ var destinationFields = {
466
+ name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
467
+ enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
468
+ http: import_zod2.z.boolean().optional().describe("Takes HTTP requests (default true)"),
469
+ email: import_zod2.z.boolean().optional().describe("Takes emails (default true)"),
470
+ filter: import_zod2.z.object({
471
+ logic: import_zod2.z.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
472
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
473
+ }).nullable().optional().describe(
474
+ "Send only captures that match (null: everything it takes). Conditions like response rules plus kind, email (name: subject, from, to, text, tag), spam_score, email_auth (name: spf, dkim, dmarc) and signature (verified, failed, missing)"
448
475
  ),
449
- name: import_zod2.z.string().max(import_sdk.MAX_CONDITION_NAME_LEN).optional().describe(
450
- `Header name or query param name (required for header/query, max ${import_sdk.MAX_CONDITION_NAME_LEN} chars)`
476
+ format: import_zod2.z.enum([
477
+ "auto",
478
+ "as_received",
479
+ "json",
480
+ "template",
481
+ "chat",
482
+ "postmark",
483
+ "sendgrid",
484
+ "mailgun",
485
+ "cloudmailin"
486
+ ]).optional().describe(
487
+ "auto (pick from the URL), as_received (relay HTTP requests unchanged; emails as signed JSON), json (signed JSON envelope), template (your bodyTemplate, signed like json), chat (Slack or Discord message), or an inbound mail provider's shape for emails (postmark, sendgrid, mailgun, cloudmailin; HTTP requests then go as received), to test the inbound handler you wrote for that provider"
451
488
  ),
452
- path: import_zod2.z.string().max(import_sdk.MAX_CONDITION_PATH_LEN).optional().describe(
453
- `JSON dot-notation path (required for body_path, max ${import_sdk.MAX_CONDITION_PATH_LEN} chars)`
454
- )
455
- }).refine((c) => c.op === "exists" || c.value !== void 0 && c.value.length > 0, {
456
- message: 'value is required when op is not "exists"'
457
- }).refine(
458
- (c) => !(c.field === "header" || c.field === "query") || c.name !== void 0 && c.name.length > 0,
459
- { message: "name is required for header and query conditions" }
460
- ).refine((c) => c.field !== "body_path" || c.path !== void 0 && c.path.length > 0, {
461
- message: "path is required for body_path conditions"
462
- }).refine((c) => c.op !== "matches" || !c.value || c.value.length <= import_sdk.MAX_GLOB_PATTERN_LEN, {
463
- message: `matches pattern must be ${import_sdk.MAX_GLOB_PATTERN_LEN} chars or less`
464
- });
489
+ mailgunSigningKey: import_zod2.z.string().max(200).nullable().optional().describe(
490
+ "For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
491
+ ),
492
+ bodyTemplate: import_zod2.z.unknown().optional().describe(
493
+ `For format template: any JSON value whose strings hold {{ placeholders }}, e.g. {"event": "{{ json.type }}", "amount": "{{ json.data.amount }}", "code": "{{ codes.0 }}"}. A string that is only a placeholder keeps the value's type; a missing field becomes null. HTTP requests read method, path, query.x, headers.x, body, json.path, id, receivedAt, signature; emails read the email.received fields (subject, from.address, to, text, codes, links, auth, spam, tag)`
494
+ ),
495
+ messageTemplate: import_zod2.z.string().max(4e3).nullable().optional().describe(
496
+ "For format chat: the message text with {{ placeholders }}; null sends the standard message"
497
+ ),
498
+ headers: import_zod2.z.array(
499
+ import_zod2.z.object({
500
+ name: import_zod2.z.string().describe("Header name, e.g. Authorization"),
501
+ value: import_zod2.z.string().nullable().describe("Value, or null to keep the stored one")
502
+ })
503
+ ).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
504
+ appendPath: import_zod2.z.boolean().optional().describe("Append the captured path to the URL (as_received)"),
505
+ retrySeconds: import_zod2.z.union([import_zod2.z.literal(0), import_zod2.z.literal(3600), import_zod2.z.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
506
+ keepOrder: import_zod2.z.boolean().optional().describe("One delivery at a time, in capture order"),
507
+ sentField: import_zod2.z.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
508
+ throttleSeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe(
509
+ "At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
510
+ ),
511
+ delaySeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
512
+ };
465
513
  var responseRuleSchema = import_zod2.z.object({
466
514
  name: import_zod2.z.string().max(import_sdk.MAX_RULE_NAME_LEN).optional().describe(`Human-readable rule name (max ${import_sdk.MAX_RULE_NAME_LEN} chars)`),
467
515
  enabled: import_zod2.z.boolean().optional().default(true).describe("Whether this rule is active"),
@@ -472,8 +520,10 @@ var responseRuleSchema = import_zod2.z.object({
472
520
  var responseRulesSchema = import_zod2.z.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
473
521
  var mockResponseSchema = import_zod2.z.object({
474
522
  status: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_STATUS_MIN).max(import_sdk.MOCK_RESPONSE_STATUS_MAX).describe(`HTTP status code (${import_sdk.MOCK_RESPONSE_STATUS_MIN}-${import_sdk.MOCK_RESPONSE_STATUS_MAX})`),
475
- body: import_zod2.z.string().default("").describe("Response body string (default: empty)"),
476
- headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none)"),
523
+ body: import_zod2.z.string().default("").describe(
524
+ 'Response body (default: empty). {{ placeholders }} are filled in from the request: {{ json.id }} (a JSON or form body field), {{ body }}, {{ headers.user-agent }}, {{ query.page }}, {{ method }}, {{ path }}, {{ id }}, {{ uuid }}, {{ now }}; filters: | default: "x", | json, | lower, | upper, | truncate: 80'
525
+ ),
526
+ headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
477
527
  delay: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_DELAY_MIN).max(import_sdk.MOCK_RESPONSE_DELAY_MAX).optional().describe(
478
528
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
479
529
  )
@@ -516,6 +566,67 @@ var waitForRequestShape = {
516
566
  timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
517
567
  pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
518
568
  };
569
+ var payloadSchemaShape = {
570
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
571
+ format: import_zod2.z.enum(import_sdk.PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
572
+ 'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
573
+ ),
574
+ name: import_zod2.z.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
575
+ event: import_zod2.z.string().optional().describe(
576
+ 'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
577
+ ),
578
+ method: import_zod2.z.string().optional().describe("Only requests with this HTTP method"),
579
+ path: import_zod2.z.string().optional().describe("Only requests with this path"),
580
+ requestIds: import_zod2.z.array(import_zod2.z.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
581
+ limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
582
+ };
583
+ function eventName(event) {
584
+ const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
585
+ const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
586
+ if (!name) return "WebhookPayload";
587
+ return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
588
+ }
589
+ function payloadSchemaResult(requests, args) {
590
+ const method = args.method?.toUpperCase();
591
+ const matching = requests.filter(
592
+ (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)
593
+ );
594
+ if (matching.length === 0) {
595
+ throw new Error(
596
+ `None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
597
+ );
598
+ }
599
+ const events = [
600
+ ...new Set(
601
+ matching.map((request) => (0, import_sdk.detectWebhookInfo)(request)?.event).filter((e) => !!e)
602
+ )
603
+ ];
604
+ const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
605
+ const result = (0, import_sdk.generateRequestSchema)(matching, { format: args.format, name });
606
+ if (result.samples === 0) {
607
+ throw new Error(
608
+ `None of the ${matching.length} matching requests has a JSON or form body to learn from.`
609
+ );
610
+ }
611
+ const output = {
612
+ format: result.format,
613
+ name: result.name,
614
+ samples: result.samples,
615
+ skipped: result.skipped,
616
+ ...events.length > 0 ? { events } : {},
617
+ ...events.length > 1 ? {
618
+ warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
619
+ } : {},
620
+ code: result.code
621
+ };
622
+ const size = JSON.stringify(output, null, 2).length;
623
+ if (size > MAX_OUTPUT) {
624
+ throw new Error(
625
+ `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.`
626
+ );
627
+ }
628
+ return jsonContent(output);
629
+ }
519
630
  function textContent(text) {
520
631
  return { content: [{ type: "text", text }] };
521
632
  }
@@ -767,6 +878,8 @@ var ESSENTIAL_DETAIL_FIELDS = [
767
878
  "tag",
768
879
  "subject",
769
880
  "from",
881
+ "fields",
882
+ "replyText",
770
883
  "size",
771
884
  "test",
772
885
  "spam",
@@ -776,6 +889,11 @@ var ESSENTIAL_DETAIL_FIELDS = [
776
889
  "html",
777
890
  "htmlTruncated"
778
891
  ];
892
+ function shrinkRendered(rendered, max) {
893
+ const body = typeof rendered.body === "string" ? rendered.body : "";
894
+ if (body.length <= max - 1024) return rendered;
895
+ return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
896
+ }
779
897
  function emailDetail(client, email, view, includeHtml) {
780
898
  const { data } = client.emails.toJson(email, {
781
899
  endpoint: view.endpoint,
@@ -938,7 +1056,7 @@ function registerTools(server, client) {
938
1056
  );
939
1057
  server.tool(
940
1058
  "update_endpoint",
941
- "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
1059
+ "Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
942
1060
  {
943
1061
  slug: import_zod2.z.string().describe("The endpoint slug to update"),
944
1062
  name: import_zod2.z.string().optional().describe("New display name"),
@@ -952,7 +1070,17 @@ function registerTools(server, client) {
952
1070
  signingHeader: import_zod2.z.string().nullable().optional().describe(
953
1071
  "Custom signature header name (only for generic-hmac provider), or null to clear"
954
1072
  ),
955
- showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
1073
+ showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
1074
+ emailFields: import_zod2.z.array(
1075
+ import_zod2.z.object({
1076
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
1077
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
1078
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
1079
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
1080
+ })
1081
+ ).max(20).nullable().optional().describe(
1082
+ "Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
1083
+ )
956
1084
  },
957
1085
  withErrorHandling(
958
1086
  async ({
@@ -964,7 +1092,8 @@ function registerTools(server, client) {
964
1092
  signingProvider,
965
1093
  signingSecret,
966
1094
  signingHeader,
967
- showEmailExtracts
1095
+ showEmailExtracts,
1096
+ emailFields
968
1097
  }) => {
969
1098
  const endpoint = await client.endpoints.update(slug, {
970
1099
  name,
@@ -974,12 +1103,33 @@ function registerTools(server, client) {
974
1103
  signingProvider,
975
1104
  signingSecret,
976
1105
  signingHeader,
977
- showEmailExtracts
1106
+ showEmailExtracts,
1107
+ emailFields
978
1108
  });
979
1109
  return jsonContent(endpoint);
980
1110
  }
981
1111
  )
982
1112
  );
1113
+ server.tool(
1114
+ "preview_response",
1115
+ "Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
1116
+ {
1117
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1118
+ requestId: import_zod2.z.string().optional().describe(
1119
+ "A captured request on the endpoint (default: the newest HTTP request, or a sample)"
1120
+ ),
1121
+ mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
1122
+ responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
1123
+ },
1124
+ withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
1125
+ const preview = await client.endpoints.previewResponse(slug, {
1126
+ ...requestId ? { requestId } : {},
1127
+ ...mockResponse !== void 0 ? { mockResponse } : {},
1128
+ ...responseRules !== void 0 ? { responseRules } : {}
1129
+ });
1130
+ return jsonContent(preview);
1131
+ })
1132
+ );
983
1133
  server.tool(
984
1134
  "delete_endpoint",
985
1135
  "Delete a webhook endpoint and all its captured requests.",
@@ -1195,6 +1345,39 @@ function registerTools(server, client) {
1195
1345
  });
1196
1346
  })
1197
1347
  );
1348
+ server.tool(
1349
+ "render_email",
1350
+ "A captured email in the shape an inbound mail provider posts it (postmark, sendgrid, mailgun, cloudmailin), to test the inbound handler you wrote for that provider against real mail. Returns the method, headers and body; with targetUrl (your local handler), posts it there and returns the answer. A Mailgun form is signed when you pass the signing key your handler checks.",
1351
+ {
1352
+ requestId: import_zod2.z.string().describe("The captured email's request ID"),
1353
+ format: import_zod2.z.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
1354
+ mailgunSigningKey: import_zod2.z.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
1355
+ targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
1356
+ },
1357
+ withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
1358
+ const rendered = await client.emails.render(requestId, {
1359
+ format,
1360
+ ...mailgunSigningKey ? { mailgunSigningKey } : {}
1361
+ });
1362
+ if (!targetUrl) {
1363
+ return jsonContent(
1364
+ shrinkRendered(rendered, MAX_BODY_SIZE)
1365
+ );
1366
+ }
1367
+ const response = await fetch(targetUrl, {
1368
+ method: "POST",
1369
+ headers: { "content-type": rendered.headers["content-type"] },
1370
+ body: rendered.body,
1371
+ redirect: "manual"
1372
+ });
1373
+ return jsonContent({
1374
+ format,
1375
+ status: response.status,
1376
+ statusText: response.statusText,
1377
+ body: await readBodyTruncated(response)
1378
+ });
1379
+ })
1380
+ );
1198
1381
  server.tool(
1199
1382
  "compare_requests",
1200
1383
  "Compare two captured requests and show the structured differences.",
@@ -1212,6 +1395,15 @@ function registerTools(server, client) {
1212
1395
  return jsonContent(diff);
1213
1396
  })
1214
1397
  );
1398
+ server.tool(
1399
+ "generate_payload_schema",
1400
+ "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.",
1401
+ payloadSchemaShape,
1402
+ withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
1403
+ const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
1404
+ return payloadSchemaResult(requests, args);
1405
+ })
1406
+ );
1215
1407
  server.tool(
1216
1408
  "extract_from_request",
1217
1409
  "Extract specific JSON fields from a captured request body.",
@@ -1569,9 +1761,79 @@ function registerTools(server, client) {
1569
1761
  return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1570
1762
  })
1571
1763
  );
1764
+ server.tool(
1765
+ "list_destinations",
1766
+ "List where an endpoint's captures go: its destinations (up to 10) with their URL, kinds, filter, format, templates and timing, and with summary: true their last 24 hours. Owner only.",
1767
+ {
1768
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1769
+ summary: import_zod2.z.boolean().default(true).describe("Add each destination's last 24 hours")
1770
+ },
1771
+ withErrorHandling(async ({ slug, summary }) => {
1772
+ return jsonContent(await client.destinations.list(slug, { summary }));
1773
+ })
1774
+ );
1775
+ server.tool(
1776
+ "add_destination",
1777
+ "Add a destination for an endpoint's captures: your server (as received or signed JSON), a Slack or Discord channel (chat, optionally your own messageTemplate, throttleSeconds 1 for a heads-up), or a JSON body you shape with bodyTemplate (for Zapier, n8n or your API). filter sends only matching captures; delaySeconds makes it a callback. Each destination gets its own signing secret. Check it with preview_destination before relying on it. Owner only.",
1778
+ {
1779
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1780
+ url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
1781
+ ...destinationFields
1782
+ },
1783
+ withErrorHandling(async ({ slug, ...input }) => {
1784
+ return jsonContent(await client.destinations.create(slug, input));
1785
+ })
1786
+ );
1787
+ server.tool(
1788
+ "update_destination",
1789
+ "Change a destination: any of its fields; only the ones you pass change. Owner only.",
1790
+ {
1791
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1792
+ destinationId: import_zod2.z.string().describe("The destination's id (from list_destinations)"),
1793
+ url: httpUrlSchema.optional().describe("A new URL"),
1794
+ ...destinationFields
1795
+ },
1796
+ withErrorHandling(async ({ slug, destinationId, ...input }) => {
1797
+ return jsonContent(await client.destinations.update(slug, destinationId, input));
1798
+ })
1799
+ );
1800
+ server.tool(
1801
+ "remove_destination",
1802
+ "Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
1803
+ {
1804
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1805
+ destinationId: import_zod2.z.string().describe("The destination's id")
1806
+ },
1807
+ withErrorHandling(async ({ slug, destinationId }) => {
1808
+ await client.destinations.delete(slug, destinationId);
1809
+ return textContent(`Destination ${destinationId} removed.`);
1810
+ })
1811
+ );
1812
+ server.tool(
1813
+ "preview_destination",
1814
+ "Dry run of a destination against a captured request: whether its filter lets the request through and the exact request it would send (method, URL, headers, body with placeholders filled in). Nothing is sent. Use destinationId for a saved destination, destination for unsaved fields (on top of the saved ones, or with a url on their own), or both.",
1815
+ {
1816
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1817
+ destinationId: import_zod2.z.string().optional().describe("A saved destination"),
1818
+ destination: import_zod2.z.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
1819
+ requestId: import_zod2.z.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
1820
+ },
1821
+ withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
1822
+ if (!destinationId && !destination?.url) {
1823
+ throw new Error("Pass destinationId, or a destination with a url");
1824
+ }
1825
+ return jsonContent(
1826
+ await client.destinations.preview(slug, {
1827
+ ...destinationId ? { destinationId } : {},
1828
+ ...destination ? { destination } : {},
1829
+ ...requestId ? { requestId } : {}
1830
+ })
1831
+ );
1832
+ })
1833
+ );
1572
1834
  server.tool(
1573
1835
  "configure_forwarding",
1574
- "Change an endpoint's forwarding: the URL, on or off, which kinds (HTTP requests, emails), the format, your own headers, and the advanced options. Only the arguments you pass change. Owner only. Saving the first URL creates the signing secret. A Slack or Discord webhook URL gets a chat message automatically.",
1836
+ "Change an endpoint's primary forwarding destination (the one the endpoint's forward* fields stand for): the URL, on or off, which kinds, the format, your own headers, and the advanced options. Only the arguments you pass change. Owner only. For several destinations, filters, templates, throttling or delays, use add_destination and update_destination.",
1575
1837
  {
1576
1838
  slug: import_zod2.z.string().describe("The endpoint slug"),
1577
1839
  url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
@@ -1619,23 +1881,32 @@ function registerTools(server, client) {
1619
1881
  "Read (or with rotate: true, replace) the whsec_ secret forwarded deliveries are signed with. Owner only. Treat the value as a credential: put it in the handler's environment, not in code.",
1620
1882
  {
1621
1883
  slug: import_zod2.z.string().describe("The endpoint slug"),
1884
+ destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1622
1885
  rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1623
1886
  },
1624
- withErrorHandling(async ({ slug, rotate }) => {
1625
- const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1887
+ withErrorHandling(async ({ slug, destinationId, rotate }) => {
1888
+ const secret = destinationId ? rotate ? await client.destinations.rotateSecret(slug, destinationId) : await client.destinations.secret(slug, destinationId) : rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1626
1889
  return jsonContent({ secret, rotated: rotate });
1627
1890
  })
1628
1891
  );
1629
1892
  server.tool(
1630
1893
  "test_forwarding",
1631
- "Send the endpoint's newest captured request of a kind (or a sample) to its forwarding URL once, exactly as a delivery would go out, and report the answer. Works with forwarding on or off; nothing is retried or logged.",
1894
+ "Send the endpoint's newest captured request of a kind (or a sample, or requestId) to a destination once, exactly as a delivery would go out, and report the answer. Works whether it is on or off, and whatever its filter says; nothing is retried or logged.",
1632
1895
  {
1633
1896
  slug: import_zod2.z.string().describe("The endpoint slug"),
1634
- kind: import_zod2.z.enum(["http", "email"]).optional().describe(
1635
- "Which kind to send (default: http when HTTP requests are forwarded, else email)"
1636
- )
1897
+ destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1898
+ kind: import_zod2.z.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
1899
+ requestId: import_zod2.z.string().optional().describe("With destinationId: a captured request to send")
1637
1900
  },
1638
- withErrorHandling(async ({ slug, kind }) => {
1901
+ withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
1902
+ if (destinationId) {
1903
+ return jsonContent(
1904
+ await client.destinations.test(slug, destinationId, {
1905
+ ...kind ? { kind } : {},
1906
+ ...requestId ? { requestId } : {}
1907
+ })
1908
+ );
1909
+ }
1639
1910
  return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1640
1911
  })
1641
1912
  );
@@ -1646,17 +1917,25 @@ function registerTools(server, client) {
1646
1917
  endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's delivery log"),
1647
1918
  requestId: import_zod2.z.string().optional().describe("List every delivery and try of this request"),
1648
1919
  limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1649
- status: import_zod2.z.enum(["all", "pending", "failed"]).default("all").describe("With endpointSlug: all, pending (queued or retrying) or failed"),
1920
+ status: import_zod2.z.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
1921
+ "With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
1922
+ ),
1923
+ destinationId: import_zod2.z.string().optional().describe("With endpointSlug: only this destination"),
1650
1924
  before: import_zod2.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1651
1925
  },
1652
- withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
1926
+ withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
1653
1927
  if (requestId === void 0 === (endpointSlug === void 0)) {
1654
1928
  throw new Error("Pass exactly one of endpointSlug or requestId");
1655
1929
  }
1656
1930
  if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1657
1931
  const [summary, deliveries] = await Promise.all([
1658
1932
  client.forwarding.summary(endpointSlug),
1659
- client.forwarding.deliveries(endpointSlug, { limit, status, before })
1933
+ client.forwarding.deliveries(endpointSlug, {
1934
+ limit,
1935
+ status,
1936
+ before,
1937
+ ...destinationId ? { destinationId } : {}
1938
+ })
1660
1939
  ]);
1661
1940
  return jsonContent({
1662
1941
  summary,
@@ -1667,17 +1946,19 @@ function registerTools(server, client) {
1667
1946
  );
1668
1947
  server.tool(
1669
1948
  "redeliver",
1670
- "Forward again with the endpoint's current settings: one captured request or email (requestId; it also sends one that was never forwarded), or every request whose latest delivery failed (endpointSlug, oldest first, up to 1,000 at a time). Owner only; fails while forwarding is off.",
1949
+ "Forward again with the current settings: one captured request or email (requestId; to every destination that takes it, or with destinationId to that one whatever its filter says), or every request whose latest delivery failed (endpointSlug, optionally only destinationId, oldest first, up to 1,000 at a time per destination). Owner only.",
1671
1950
  {
1672
1951
  requestId: import_zod2.z.string().optional().describe("The request or email to send again"),
1673
- endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again")
1952
+ endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again"),
1953
+ destinationId: import_zod2.z.string().optional().describe("Only this destination")
1674
1954
  },
1675
- withErrorHandling(async ({ requestId, endpointSlug }) => {
1955
+ withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
1676
1956
  if (requestId === void 0 === (endpointSlug === void 0)) {
1677
1957
  throw new Error("Pass exactly one of requestId or endpointSlug");
1678
1958
  }
1959
+ const options = destinationId ? { destinationId } : {};
1679
1960
  return jsonContent(
1680
- requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
1961
+ requestId ? await client.forwarding.redeliver(requestId, options) : await client.forwarding.redeliverFailed(endpointSlug, options)
1681
1962
  );
1682
1963
  })
1683
1964
  );
@@ -1902,6 +2183,18 @@ function registerSandboxTools(server, options = {}) {
1902
2183
  })
1903
2184
  )
1904
2185
  );
2186
+ registered.push(
2187
+ server.tool(
2188
+ "generate_payload_schema",
2189
+ "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.",
2190
+ payloadSchemaShape,
2191
+ withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
2192
+ const sandbox = await session.requireSandbox();
2193
+ const requests = requestIds ? await Promise.all(requestIds.map((id) => sandbox.requests.get(id))) : await sandbox.requests.list(endpointSlug, { limit });
2194
+ return payloadSchemaResult(requests, args);
2195
+ })
2196
+ )
2197
+ );
1905
2198
  registered.push(
1906
2199
  server.tool(
1907
2200
  "connect_account",
@@ -2060,7 +2353,7 @@ function registerAgentRegistrationTools(server) {
2060
2353
  }
2061
2354
 
2062
2355
  // src/index.ts
2063
- var VERSION = true ? "1.12.1" : "0.0.0-dev";
2356
+ var VERSION = true ? "1.14.0" : "0.0.0-dev";
2064
2357
  function createServer(options = {}) {
2065
2358
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2066
2359
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;