@webhooks-cc/mcp 1.13.0 → 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/README.md CHANGED
@@ -68,7 +68,7 @@ Get your API key at [webhooks.cc/account/api-keys](https://webhooks.cc/account/a
68
68
 
69
69
  ## What it exposes
70
70
 
71
- The server exposes `39` tools, `3` prompts, and `3` resource surfaces with an API key, and `11` sandbox tools without one.
71
+ The server exposes `46` tools, `3` prompts, and `3` resource surfaces with an API key, and `11` sandbox tools without one.
72
72
 
73
73
  ### Endpoint tools
74
74
 
@@ -76,6 +76,7 @@ The server exposes `39` tools, `3` prompts, and `3` resource surfaces with an AP
76
76
  - `list_endpoints`
77
77
  - `get_endpoint`
78
78
  - `update_endpoint`
79
+ - `preview_response`
79
80
  - `delete_endpoint`
80
81
  - `create_endpoints`
81
82
  - `delete_endpoints`
@@ -103,19 +104,24 @@ Every endpoint on an account receives email at `{slug}@mailhooks.cc` (and `{slug
103
104
  - `get_email`
104
105
  - `wait_for_email`
105
106
  - `send_test_email`
107
+ - `render_email` (an email in Postmark, SendGrid, Mailgun or CloudMailin shape, optionally posted to your local inbound handler)
106
108
 
107
109
  The request tools and resources return emails too (filter with `kind`), without the raw message or the HTML part and with the text cut at 8,000 characters; `get_email` returns the HTML.
108
110
 
109
111
  ### Forwarding tools
110
112
 
111
- Forward every captured HTTP request and email to a URL or a Slack or Discord channel, as received,
112
- as signed JSON or as a chat message, retried and logged (owner only):
113
-
114
- - `configure_forwarding` (URL, on/off, kinds, format, your headers, path, retries, order, sent time field)
115
- - `get_forwarding_secret` (returns the `whsec_` secret; with `rotate: true`, replaces it)
116
- - `test_forwarding`
117
- - `list_deliveries` (the delivery log with its counts, or every try of one request)
118
- - `redeliver` (one request, or every failed delivery of an endpoint)
113
+ Send captured HTTP requests and emails to up to 10 destinations (your servers, Slack or Discord
114
+ channels, Zapier or n8n), each with its own kinds, conditions, format (as received, signed JSON,
115
+ a JSON body you shape with placeholders, or a chat message), throttle, delay and signing secret,
116
+ retried and logged (owner only):
117
+
118
+ - `list_destinations`, `add_destination`, `update_destination`, `remove_destination`
119
+ - `preview_destination` (dry run: does the filter pass, and the exact request it would send)
120
+ - `configure_forwarding` (the primary destination: URL, on/off, kinds, format, your headers, path, retries, order, sent time field)
121
+ - `get_forwarding_secret` (returns a destination's `whsec_` secret; with `rotate: true`, replaces it)
122
+ - `test_forwarding` (sends one request to a destination and reports the answer)
123
+ - `list_deliveries` (the delivery log with its counts, by destination, or every try of one request)
124
+ - `redeliver` (one request, to one destination or all, or every failed delivery of an endpoint)
119
125
 
120
126
  ### Send and test tools
121
127
 
package/dist/bin/mcp.js CHANGED
@@ -418,28 +418,75 @@ var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HT
418
418
  var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
419
419
  var EMAIL_SCAN_LIMIT = 100;
420
420
  var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
421
- var ruleConditionSchema = import_zod2.z.object({
422
- field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
423
- op: import_zod2.z.enum(["eq", "contains", "starts_with", "matches", "exists"]).describe("Comparison operator"),
424
- value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(
425
- `Value to compare against (required unless op is "exists", max ${import_sdk.MAX_CONDITION_VALUE_LEN} chars)`
421
+ var CONDITION_FIELD_NAMES = Object.keys(import_sdk.CONDITION_FIELDS);
422
+ var CONDITION_OP_NAMES = import_sdk.CONDITION_OPS;
423
+ function conditionSchema(use) {
424
+ const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => import_sdk.CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
425
+ return import_zod2.z.object({
426
+ field: import_zod2.z.enum(fields).describe(
427
+ 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)"
428
+ ),
429
+ op: import_zod2.z.enum(CONDITION_OP_NAMES).describe(
430
+ "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)"
431
+ ),
432
+ value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
433
+ 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"),
434
+ 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"]')
435
+ }).superRefine((condition, ctx) => {
436
+ for (const issue of (0, import_sdk.conditionIssues)(condition, { use })) {
437
+ ctx.addIssue({ code: "custom", message: issue });
438
+ }
439
+ });
440
+ }
441
+ var ruleConditionSchema = conditionSchema("response");
442
+ var destinationFields = {
443
+ name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
444
+ enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
445
+ http: import_zod2.z.boolean().optional().describe("Takes HTTP requests (default true)"),
446
+ email: import_zod2.z.boolean().optional().describe("Takes emails (default true)"),
447
+ filter: import_zod2.z.object({
448
+ logic: import_zod2.z.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
449
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
450
+ }).nullable().optional().describe(
451
+ "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)"
426
452
  ),
427
- name: import_zod2.z.string().max(import_sdk.MAX_CONDITION_NAME_LEN).optional().describe(
428
- `Header name or query param name (required for header/query, max ${import_sdk.MAX_CONDITION_NAME_LEN} chars)`
453
+ format: import_zod2.z.enum([
454
+ "auto",
455
+ "as_received",
456
+ "json",
457
+ "template",
458
+ "chat",
459
+ "postmark",
460
+ "sendgrid",
461
+ "mailgun",
462
+ "cloudmailin"
463
+ ]).optional().describe(
464
+ "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"
429
465
  ),
430
- path: import_zod2.z.string().max(import_sdk.MAX_CONDITION_PATH_LEN).optional().describe(
431
- `JSON dot-notation path (required for body_path, max ${import_sdk.MAX_CONDITION_PATH_LEN} chars)`
432
- )
433
- }).refine((c) => c.op === "exists" || c.value !== void 0 && c.value.length > 0, {
434
- message: 'value is required when op is not "exists"'
435
- }).refine(
436
- (c) => !(c.field === "header" || c.field === "query") || c.name !== void 0 && c.name.length > 0,
437
- { message: "name is required for header and query conditions" }
438
- ).refine((c) => c.field !== "body_path" || c.path !== void 0 && c.path.length > 0, {
439
- message: "path is required for body_path conditions"
440
- }).refine((c) => c.op !== "matches" || !c.value || c.value.length <= import_sdk.MAX_GLOB_PATTERN_LEN, {
441
- message: `matches pattern must be ${import_sdk.MAX_GLOB_PATTERN_LEN} chars or less`
442
- });
466
+ mailgunSigningKey: import_zod2.z.string().max(200).nullable().optional().describe(
467
+ "For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
468
+ ),
469
+ bodyTemplate: import_zod2.z.unknown().optional().describe(
470
+ `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)`
471
+ ),
472
+ messageTemplate: import_zod2.z.string().max(4e3).nullable().optional().describe(
473
+ "For format chat: the message text with {{ placeholders }}; null sends the standard message"
474
+ ),
475
+ headers: import_zod2.z.array(
476
+ import_zod2.z.object({
477
+ name: import_zod2.z.string().describe("Header name, e.g. Authorization"),
478
+ value: import_zod2.z.string().nullable().describe("Value, or null to keep the stored one")
479
+ })
480
+ ).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
481
+ appendPath: import_zod2.z.boolean().optional().describe("Append the captured path to the URL (as_received)"),
482
+ 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"),
483
+ keepOrder: import_zod2.z.boolean().optional().describe("One delivery at a time, in capture order"),
484
+ sentField: import_zod2.z.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
485
+ throttleSeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe(
486
+ "At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
487
+ ),
488
+ delaySeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
489
+ };
443
490
  var responseRuleSchema = import_zod2.z.object({
444
491
  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)`),
445
492
  enabled: import_zod2.z.boolean().optional().default(true).describe("Whether this rule is active"),
@@ -450,8 +497,10 @@ var responseRuleSchema = import_zod2.z.object({
450
497
  var responseRulesSchema = import_zod2.z.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
451
498
  var mockResponseSchema = import_zod2.z.object({
452
499
  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})`),
453
- body: import_zod2.z.string().default("").describe("Response body string (default: empty)"),
454
- headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none)"),
500
+ body: import_zod2.z.string().default("").describe(
501
+ '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'
502
+ ),
503
+ headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
455
504
  delay: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_DELAY_MIN).max(import_sdk.MOCK_RESPONSE_DELAY_MAX).optional().describe(
456
505
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
457
506
  )
@@ -806,6 +855,8 @@ var ESSENTIAL_DETAIL_FIELDS = [
806
855
  "tag",
807
856
  "subject",
808
857
  "from",
858
+ "fields",
859
+ "replyText",
809
860
  "size",
810
861
  "test",
811
862
  "spam",
@@ -815,6 +866,11 @@ var ESSENTIAL_DETAIL_FIELDS = [
815
866
  "html",
816
867
  "htmlTruncated"
817
868
  ];
869
+ function shrinkRendered(rendered, max) {
870
+ const body = typeof rendered.body === "string" ? rendered.body : "";
871
+ if (body.length <= max - 1024) return rendered;
872
+ return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
873
+ }
818
874
  function emailDetail(client, email, view, includeHtml) {
819
875
  const { data } = client.emails.toJson(email, {
820
876
  endpoint: view.endpoint,
@@ -977,7 +1033,7 @@ function registerTools(server, client) {
977
1033
  );
978
1034
  server.tool(
979
1035
  "update_endpoint",
980
- "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
1036
+ "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.",
981
1037
  {
982
1038
  slug: import_zod2.z.string().describe("The endpoint slug to update"),
983
1039
  name: import_zod2.z.string().optional().describe("New display name"),
@@ -991,7 +1047,17 @@ function registerTools(server, client) {
991
1047
  signingHeader: import_zod2.z.string().nullable().optional().describe(
992
1048
  "Custom signature header name (only for generic-hmac provider), or null to clear"
993
1049
  ),
994
- showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
1050
+ showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
1051
+ emailFields: import_zod2.z.array(
1052
+ import_zod2.z.object({
1053
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
1054
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
1055
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
1056
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
1057
+ })
1058
+ ).max(20).nullable().optional().describe(
1059
+ "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."
1060
+ )
995
1061
  },
996
1062
  withErrorHandling(
997
1063
  async ({
@@ -1003,7 +1069,8 @@ function registerTools(server, client) {
1003
1069
  signingProvider,
1004
1070
  signingSecret,
1005
1071
  signingHeader,
1006
- showEmailExtracts
1072
+ showEmailExtracts,
1073
+ emailFields
1007
1074
  }) => {
1008
1075
  const endpoint = await client.endpoints.update(slug, {
1009
1076
  name,
@@ -1013,12 +1080,33 @@ function registerTools(server, client) {
1013
1080
  signingProvider,
1014
1081
  signingSecret,
1015
1082
  signingHeader,
1016
- showEmailExtracts
1083
+ showEmailExtracts,
1084
+ emailFields
1017
1085
  });
1018
1086
  return jsonContent(endpoint);
1019
1087
  }
1020
1088
  )
1021
1089
  );
1090
+ server.tool(
1091
+ "preview_response",
1092
+ "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.",
1093
+ {
1094
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1095
+ requestId: import_zod2.z.string().optional().describe(
1096
+ "A captured request on the endpoint (default: the newest HTTP request, or a sample)"
1097
+ ),
1098
+ mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
1099
+ responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
1100
+ },
1101
+ withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
1102
+ const preview = await client.endpoints.previewResponse(slug, {
1103
+ ...requestId ? { requestId } : {},
1104
+ ...mockResponse !== void 0 ? { mockResponse } : {},
1105
+ ...responseRules !== void 0 ? { responseRules } : {}
1106
+ });
1107
+ return jsonContent(preview);
1108
+ })
1109
+ );
1022
1110
  server.tool(
1023
1111
  "delete_endpoint",
1024
1112
  "Delete a webhook endpoint and all its captured requests.",
@@ -1234,6 +1322,39 @@ function registerTools(server, client) {
1234
1322
  });
1235
1323
  })
1236
1324
  );
1325
+ server.tool(
1326
+ "render_email",
1327
+ "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.",
1328
+ {
1329
+ requestId: import_zod2.z.string().describe("The captured email's request ID"),
1330
+ format: import_zod2.z.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
1331
+ mailgunSigningKey: import_zod2.z.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
1332
+ targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
1333
+ },
1334
+ withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
1335
+ const rendered = await client.emails.render(requestId, {
1336
+ format,
1337
+ ...mailgunSigningKey ? { mailgunSigningKey } : {}
1338
+ });
1339
+ if (!targetUrl) {
1340
+ return jsonContent(
1341
+ shrinkRendered(rendered, MAX_BODY_SIZE)
1342
+ );
1343
+ }
1344
+ const response = await fetch(targetUrl, {
1345
+ method: "POST",
1346
+ headers: { "content-type": rendered.headers["content-type"] },
1347
+ body: rendered.body,
1348
+ redirect: "manual"
1349
+ });
1350
+ return jsonContent({
1351
+ format,
1352
+ status: response.status,
1353
+ statusText: response.statusText,
1354
+ body: await readBodyTruncated(response)
1355
+ });
1356
+ })
1357
+ );
1237
1358
  server.tool(
1238
1359
  "compare_requests",
1239
1360
  "Compare two captured requests and show the structured differences.",
@@ -1617,9 +1738,79 @@ function registerTools(server, client) {
1617
1738
  return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1618
1739
  })
1619
1740
  );
1741
+ server.tool(
1742
+ "list_destinations",
1743
+ "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.",
1744
+ {
1745
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1746
+ summary: import_zod2.z.boolean().default(true).describe("Add each destination's last 24 hours")
1747
+ },
1748
+ withErrorHandling(async ({ slug, summary }) => {
1749
+ return jsonContent(await client.destinations.list(slug, { summary }));
1750
+ })
1751
+ );
1752
+ server.tool(
1753
+ "add_destination",
1754
+ "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.",
1755
+ {
1756
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1757
+ url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
1758
+ ...destinationFields
1759
+ },
1760
+ withErrorHandling(async ({ slug, ...input }) => {
1761
+ return jsonContent(await client.destinations.create(slug, input));
1762
+ })
1763
+ );
1764
+ server.tool(
1765
+ "update_destination",
1766
+ "Change a destination: any of its fields; only the ones you pass change. Owner only.",
1767
+ {
1768
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1769
+ destinationId: import_zod2.z.string().describe("The destination's id (from list_destinations)"),
1770
+ url: httpUrlSchema.optional().describe("A new URL"),
1771
+ ...destinationFields
1772
+ },
1773
+ withErrorHandling(async ({ slug, destinationId, ...input }) => {
1774
+ return jsonContent(await client.destinations.update(slug, destinationId, input));
1775
+ })
1776
+ );
1777
+ server.tool(
1778
+ "remove_destination",
1779
+ "Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
1780
+ {
1781
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1782
+ destinationId: import_zod2.z.string().describe("The destination's id")
1783
+ },
1784
+ withErrorHandling(async ({ slug, destinationId }) => {
1785
+ await client.destinations.delete(slug, destinationId);
1786
+ return textContent(`Destination ${destinationId} removed.`);
1787
+ })
1788
+ );
1789
+ server.tool(
1790
+ "preview_destination",
1791
+ "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.",
1792
+ {
1793
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1794
+ destinationId: import_zod2.z.string().optional().describe("A saved destination"),
1795
+ destination: import_zod2.z.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
1796
+ requestId: import_zod2.z.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
1797
+ },
1798
+ withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
1799
+ if (!destinationId && !destination?.url) {
1800
+ throw new Error("Pass destinationId, or a destination with a url");
1801
+ }
1802
+ return jsonContent(
1803
+ await client.destinations.preview(slug, {
1804
+ ...destinationId ? { destinationId } : {},
1805
+ ...destination ? { destination } : {},
1806
+ ...requestId ? { requestId } : {}
1807
+ })
1808
+ );
1809
+ })
1810
+ );
1620
1811
  server.tool(
1621
1812
  "configure_forwarding",
1622
- "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.",
1813
+ "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.",
1623
1814
  {
1624
1815
  slug: import_zod2.z.string().describe("The endpoint slug"),
1625
1816
  url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
@@ -1667,23 +1858,32 @@ function registerTools(server, client) {
1667
1858
  "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.",
1668
1859
  {
1669
1860
  slug: import_zod2.z.string().describe("The endpoint slug"),
1861
+ destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1670
1862
  rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1671
1863
  },
1672
- withErrorHandling(async ({ slug, rotate }) => {
1673
- const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1864
+ withErrorHandling(async ({ slug, destinationId, rotate }) => {
1865
+ 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);
1674
1866
  return jsonContent({ secret, rotated: rotate });
1675
1867
  })
1676
1868
  );
1677
1869
  server.tool(
1678
1870
  "test_forwarding",
1679
- "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.",
1871
+ "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.",
1680
1872
  {
1681
1873
  slug: import_zod2.z.string().describe("The endpoint slug"),
1682
- kind: import_zod2.z.enum(["http", "email"]).optional().describe(
1683
- "Which kind to send (default: http when HTTP requests are forwarded, else email)"
1684
- )
1874
+ destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1875
+ kind: import_zod2.z.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
1876
+ requestId: import_zod2.z.string().optional().describe("With destinationId: a captured request to send")
1685
1877
  },
1686
- withErrorHandling(async ({ slug, kind }) => {
1878
+ withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
1879
+ if (destinationId) {
1880
+ return jsonContent(
1881
+ await client.destinations.test(slug, destinationId, {
1882
+ ...kind ? { kind } : {},
1883
+ ...requestId ? { requestId } : {}
1884
+ })
1885
+ );
1886
+ }
1687
1887
  return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1688
1888
  })
1689
1889
  );
@@ -1694,17 +1894,25 @@ function registerTools(server, client) {
1694
1894
  endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's delivery log"),
1695
1895
  requestId: import_zod2.z.string().optional().describe("List every delivery and try of this request"),
1696
1896
  limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1697
- status: import_zod2.z.enum(["all", "pending", "failed"]).default("all").describe("With endpointSlug: all, pending (queued or retrying) or failed"),
1897
+ status: import_zod2.z.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
1898
+ "With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
1899
+ ),
1900
+ destinationId: import_zod2.z.string().optional().describe("With endpointSlug: only this destination"),
1698
1901
  before: import_zod2.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1699
1902
  },
1700
- withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
1903
+ withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
1701
1904
  if (requestId === void 0 === (endpointSlug === void 0)) {
1702
1905
  throw new Error("Pass exactly one of endpointSlug or requestId");
1703
1906
  }
1704
1907
  if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1705
1908
  const [summary, deliveries] = await Promise.all([
1706
1909
  client.forwarding.summary(endpointSlug),
1707
- client.forwarding.deliveries(endpointSlug, { limit, status, before })
1910
+ client.forwarding.deliveries(endpointSlug, {
1911
+ limit,
1912
+ status,
1913
+ before,
1914
+ ...destinationId ? { destinationId } : {}
1915
+ })
1708
1916
  ]);
1709
1917
  return jsonContent({
1710
1918
  summary,
@@ -1715,17 +1923,19 @@ function registerTools(server, client) {
1715
1923
  );
1716
1924
  server.tool(
1717
1925
  "redeliver",
1718
- "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.",
1926
+ "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.",
1719
1927
  {
1720
1928
  requestId: import_zod2.z.string().optional().describe("The request or email to send again"),
1721
- endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again")
1929
+ endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again"),
1930
+ destinationId: import_zod2.z.string().optional().describe("Only this destination")
1722
1931
  },
1723
- withErrorHandling(async ({ requestId, endpointSlug }) => {
1932
+ withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
1724
1933
  if (requestId === void 0 === (endpointSlug === void 0)) {
1725
1934
  throw new Error("Pass exactly one of requestId or endpointSlug");
1726
1935
  }
1936
+ const options = destinationId ? { destinationId } : {};
1727
1937
  return jsonContent(
1728
- requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
1938
+ requestId ? await client.forwarding.redeliver(requestId, options) : await client.forwarding.redeliverFailed(endpointSlug, options)
1729
1939
  );
1730
1940
  })
1731
1941
  );
@@ -2117,7 +2327,7 @@ function registerSandboxTools(server, options = {}) {
2117
2327
  }
2118
2328
 
2119
2329
  // src/index.ts
2120
- var VERSION = true ? "1.13.0" : "0.0.0-dev";
2330
+ var VERSION = true ? "1.14.0" : "0.0.0-dev";
2121
2331
  function createServer(options = {}) {
2122
2332
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2123
2333
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;