@webhooks-cc/mcp 1.13.0 → 1.15.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.js CHANGED
@@ -18,8 +18,9 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ REMOTE_MAX_WAIT_MS: () => REMOTE_MAX_WAIT_MS,
23
24
  createServer: () => createServer,
24
25
  registerAgentRegistrationTools: () => registerAgentRegistrationTools,
25
26
  registerPrompts: () => registerPrompts,
@@ -27,7 +28,7 @@ __export(index_exports, {
27
28
  registerSandboxTools: () => registerSandboxTools,
28
29
  registerTools: () => registerTools
29
30
  });
30
- module.exports = __toCommonJS(index_exports);
31
+ module.exports = __toCommonJS(src_exports);
31
32
  var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
32
33
  var import_sdk3 = require("@webhooks-cc/sdk");
33
34
 
@@ -441,28 +442,75 @@ var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HT
441
442
  var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
442
443
  var EMAIL_SCAN_LIMIT = 100;
443
444
  var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
444
- var ruleConditionSchema = import_zod2.z.object({
445
- field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
446
- op: import_zod2.z.enum(["eq", "contains", "starts_with", "matches", "exists"]).describe("Comparison operator"),
447
- value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(
448
- `Value to compare against (required unless op is "exists", max ${import_sdk.MAX_CONDITION_VALUE_LEN} chars)`
445
+ var CONDITION_FIELD_NAMES = Object.keys(import_sdk.CONDITION_FIELDS);
446
+ var CONDITION_OP_NAMES = import_sdk.CONDITION_OPS;
447
+ function conditionSchema(use) {
448
+ const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => import_sdk.CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
449
+ return import_zod2.z.object({
450
+ field: import_zod2.z.enum(fields).describe(
451
+ 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)"
452
+ ),
453
+ op: import_zod2.z.enum(CONDITION_OP_NAMES).describe(
454
+ "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)"
455
+ ),
456
+ value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
457
+ 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"),
458
+ 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"]')
459
+ }).superRefine((condition, ctx) => {
460
+ for (const issue of (0, import_sdk.conditionIssues)(condition, { use })) {
461
+ ctx.addIssue({ code: "custom", message: issue });
462
+ }
463
+ });
464
+ }
465
+ var ruleConditionSchema = conditionSchema("response");
466
+ var destinationFields = {
467
+ name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
468
+ enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
469
+ http: import_zod2.z.boolean().optional().describe("Takes HTTP requests (default true)"),
470
+ email: import_zod2.z.boolean().optional().describe("Takes emails (default true)"),
471
+ filter: import_zod2.z.object({
472
+ logic: import_zod2.z.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
473
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
474
+ }).nullable().optional().describe(
475
+ "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)"
449
476
  ),
450
- name: import_zod2.z.string().max(import_sdk.MAX_CONDITION_NAME_LEN).optional().describe(
451
- `Header name or query param name (required for header/query, max ${import_sdk.MAX_CONDITION_NAME_LEN} chars)`
477
+ format: import_zod2.z.enum([
478
+ "auto",
479
+ "as_received",
480
+ "json",
481
+ "template",
482
+ "chat",
483
+ "postmark",
484
+ "sendgrid",
485
+ "mailgun",
486
+ "cloudmailin"
487
+ ]).optional().describe(
488
+ "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"
452
489
  ),
453
- path: import_zod2.z.string().max(import_sdk.MAX_CONDITION_PATH_LEN).optional().describe(
454
- `JSON dot-notation path (required for body_path, max ${import_sdk.MAX_CONDITION_PATH_LEN} chars)`
455
- )
456
- }).refine((c) => c.op === "exists" || c.value !== void 0 && c.value.length > 0, {
457
- message: 'value is required when op is not "exists"'
458
- }).refine(
459
- (c) => !(c.field === "header" || c.field === "query") || c.name !== void 0 && c.name.length > 0,
460
- { message: "name is required for header and query conditions" }
461
- ).refine((c) => c.field !== "body_path" || c.path !== void 0 && c.path.length > 0, {
462
- message: "path is required for body_path conditions"
463
- }).refine((c) => c.op !== "matches" || !c.value || c.value.length <= import_sdk.MAX_GLOB_PATTERN_LEN, {
464
- message: `matches pattern must be ${import_sdk.MAX_GLOB_PATTERN_LEN} chars or less`
465
- });
490
+ mailgunSigningKey: import_zod2.z.string().max(200).nullable().optional().describe(
491
+ "For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
492
+ ),
493
+ bodyTemplate: import_zod2.z.unknown().optional().describe(
494
+ `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)`
495
+ ),
496
+ messageTemplate: import_zod2.z.string().max(4e3).nullable().optional().describe(
497
+ "For format chat: the message text with {{ placeholders }}; null sends the standard message"
498
+ ),
499
+ headers: import_zod2.z.array(
500
+ import_zod2.z.object({
501
+ name: import_zod2.z.string().describe("Header name, e.g. Authorization"),
502
+ value: import_zod2.z.string().nullable().describe("Value, or null to keep the stored one")
503
+ })
504
+ ).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
505
+ appendPath: import_zod2.z.boolean().optional().describe("Append the captured path to the URL (as_received)"),
506
+ 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"),
507
+ keepOrder: import_zod2.z.boolean().optional().describe("One delivery at a time, in capture order"),
508
+ sentField: import_zod2.z.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
509
+ throttleSeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe(
510
+ "At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
511
+ ),
512
+ delaySeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
513
+ };
466
514
  var responseRuleSchema = import_zod2.z.object({
467
515
  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)`),
468
516
  enabled: import_zod2.z.boolean().optional().default(true).describe("Whether this rule is active"),
@@ -473,8 +521,10 @@ var responseRuleSchema = import_zod2.z.object({
473
521
  var responseRulesSchema = import_zod2.z.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
474
522
  var mockResponseSchema = import_zod2.z.object({
475
523
  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})`),
476
- body: import_zod2.z.string().default("").describe("Response body string (default: empty)"),
477
- headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none)"),
524
+ body: import_zod2.z.string().default("").describe(
525
+ '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'
526
+ ),
527
+ headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
478
528
  delay: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_DELAY_MIN).max(import_sdk.MOCK_RESPONSE_DELAY_MAX).optional().describe(
479
529
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
480
530
  )
@@ -728,7 +778,9 @@ async function waitForMultipleRequests(client, endpointSlug, options) {
728
778
  };
729
779
  }
730
780
  }
731
- await sleep(Math.max(10, pollIntervalMs));
781
+ await sleep(
782
+ Math.min(Math.max(10, pollIntervalMs), Math.max(0, timeoutMs - (Date.now() - startedAt)))
783
+ );
732
784
  }
733
785
  return {
734
786
  requests,
@@ -829,6 +881,8 @@ var ESSENTIAL_DETAIL_FIELDS = [
829
881
  "tag",
830
882
  "subject",
831
883
  "from",
884
+ "fields",
885
+ "replyText",
832
886
  "size",
833
887
  "test",
834
888
  "spam",
@@ -838,6 +892,11 @@ var ESSENTIAL_DETAIL_FIELDS = [
838
892
  "html",
839
893
  "htmlTruncated"
840
894
  ];
895
+ function shrinkRendered(rendered, max) {
896
+ const body = typeof rendered.body === "string" ? rendered.body : "";
897
+ if (body.length <= max - 1024) return rendered;
898
+ return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
899
+ }
841
900
  function emailDetail(client, email, view, includeHtml) {
842
901
  const { data } = client.emails.toJson(email, {
843
902
  endpoint: view.endpoint,
@@ -879,7 +938,67 @@ function emailDetail(client, email, view, includeHtml) {
879
938
  ]);
880
939
  return detail;
881
940
  }
882
- function registerTools(server, client) {
941
+ var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
942
+ var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
943
+ var REMOTE_MAX_WAIT_MS = 5e4;
944
+ var REMOTE_MIN_POLL_MS = 1e3;
945
+ function durationMs(value) {
946
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
947
+ if (typeof value !== "string") return null;
948
+ try {
949
+ const ms = (0, import_sdk.parseDuration)(value);
950
+ return Number.isFinite(ms) && ms >= 0 ? ms : null;
951
+ } catch {
952
+ return null;
953
+ }
954
+ }
955
+ function toolTarget(target, remote, handles) {
956
+ return {
957
+ tool(name, description, shape, handler) {
958
+ if (remote && LOCAL_ONLY_TOOLS.has(name)) return { remove() {
959
+ } };
960
+ const wrapped = remote ? async (args) => {
961
+ const input = args;
962
+ if (TARGET_URL_TOOLS.has(name) && input.targetUrl !== void 0) {
963
+ return {
964
+ ...textContent(
965
+ "targetUrl needs the local MCP server (npx -y @webhooks-cc/mcp): the hosted server cannot reach your machine. Leave it out here."
966
+ ),
967
+ isError: true
968
+ };
969
+ }
970
+ const bounded = { ...input };
971
+ if ("pollInterval" in shape && input.pollInterval === void 0) {
972
+ bounded.pollInterval = REMOTE_MIN_POLL_MS;
973
+ }
974
+ for (const [key, bound] of [
975
+ ["timeout", (ms) => Math.min(ms, REMOTE_MAX_WAIT_MS)],
976
+ [
977
+ "pollInterval",
978
+ (ms) => Math.min(Math.max(ms, REMOTE_MIN_POLL_MS), REMOTE_MAX_WAIT_MS)
979
+ ]
980
+ ]) {
981
+ if (bounded[key] === void 0) continue;
982
+ const ms = durationMs(bounded[key]);
983
+ if (ms === null) {
984
+ return {
985
+ ...textContent(`${key} must be a duration such as "30s" or milliseconds.`),
986
+ isError: true
987
+ };
988
+ }
989
+ bounded[key] = bound(ms);
990
+ }
991
+ return handler(bounded);
992
+ } : handler;
993
+ const handle = target.tool(name, description, shape, wrapped);
994
+ handles.push(handle);
995
+ return handle;
996
+ }
997
+ };
998
+ }
999
+ function registerTools(target, client, options = {}) {
1000
+ const handles = [];
1001
+ const server = toolTarget(target, options.remote === true, handles);
883
1002
  server.tool(
884
1003
  "create_endpoint",
885
1004
  "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
@@ -1000,7 +1119,7 @@ function registerTools(server, client) {
1000
1119
  );
1001
1120
  server.tool(
1002
1121
  "update_endpoint",
1003
- "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
1122
+ "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.",
1004
1123
  {
1005
1124
  slug: import_zod2.z.string().describe("The endpoint slug to update"),
1006
1125
  name: import_zod2.z.string().optional().describe("New display name"),
@@ -1014,7 +1133,17 @@ function registerTools(server, client) {
1014
1133
  signingHeader: import_zod2.z.string().nullable().optional().describe(
1015
1134
  "Custom signature header name (only for generic-hmac provider), or null to clear"
1016
1135
  ),
1017
- showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
1136
+ showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
1137
+ emailFields: import_zod2.z.array(
1138
+ import_zod2.z.object({
1139
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
1140
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
1141
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
1142
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
1143
+ })
1144
+ ).max(20).nullable().optional().describe(
1145
+ "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."
1146
+ )
1018
1147
  },
1019
1148
  withErrorHandling(
1020
1149
  async ({
@@ -1026,7 +1155,8 @@ function registerTools(server, client) {
1026
1155
  signingProvider,
1027
1156
  signingSecret,
1028
1157
  signingHeader,
1029
- showEmailExtracts
1158
+ showEmailExtracts,
1159
+ emailFields
1030
1160
  }) => {
1031
1161
  const endpoint = await client.endpoints.update(slug, {
1032
1162
  name,
@@ -1036,12 +1166,33 @@ function registerTools(server, client) {
1036
1166
  signingProvider,
1037
1167
  signingSecret,
1038
1168
  signingHeader,
1039
- showEmailExtracts
1169
+ showEmailExtracts,
1170
+ emailFields
1040
1171
  });
1041
1172
  return jsonContent(endpoint);
1042
1173
  }
1043
1174
  )
1044
1175
  );
1176
+ server.tool(
1177
+ "preview_response",
1178
+ "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.",
1179
+ {
1180
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1181
+ requestId: import_zod2.z.string().optional().describe(
1182
+ "A captured request on the endpoint (default: the newest HTTP request, or a sample)"
1183
+ ),
1184
+ mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
1185
+ responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
1186
+ },
1187
+ withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
1188
+ const preview = await client.endpoints.previewResponse(slug, {
1189
+ ...requestId ? { requestId } : {},
1190
+ ...mockResponse !== void 0 ? { mockResponse } : {},
1191
+ ...responseRules !== void 0 ? { responseRules } : {}
1192
+ });
1193
+ return jsonContent(preview);
1194
+ })
1195
+ );
1045
1196
  server.tool(
1046
1197
  "delete_endpoint",
1047
1198
  "Delete a webhook endpoint and all its captured requests.",
@@ -1257,6 +1408,39 @@ function registerTools(server, client) {
1257
1408
  });
1258
1409
  })
1259
1410
  );
1411
+ server.tool(
1412
+ "render_email",
1413
+ "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.",
1414
+ {
1415
+ requestId: import_zod2.z.string().describe("The captured email's request ID"),
1416
+ format: import_zod2.z.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
1417
+ mailgunSigningKey: import_zod2.z.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
1418
+ targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
1419
+ },
1420
+ withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
1421
+ const rendered = await client.emails.render(requestId, {
1422
+ format,
1423
+ ...mailgunSigningKey ? { mailgunSigningKey } : {}
1424
+ });
1425
+ if (!targetUrl) {
1426
+ return jsonContent(
1427
+ shrinkRendered(rendered, MAX_BODY_SIZE)
1428
+ );
1429
+ }
1430
+ const response = await fetch(targetUrl, {
1431
+ method: "POST",
1432
+ headers: { "content-type": rendered.headers["content-type"] },
1433
+ body: rendered.body,
1434
+ redirect: "manual"
1435
+ });
1436
+ return jsonContent({
1437
+ format,
1438
+ status: response.status,
1439
+ statusText: response.statusText,
1440
+ body: await readBodyTruncated(response)
1441
+ });
1442
+ })
1443
+ );
1260
1444
  server.tool(
1261
1445
  "compare_requests",
1262
1446
  "Compare two captured requests and show the structured differences.",
@@ -1496,7 +1680,7 @@ function registerTools(server, client) {
1496
1680
  verifySignature: shouldVerify,
1497
1681
  cleanup
1498
1682
  }) => {
1499
- const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s" });
1683
+ const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s", pollInterval: "1s" });
1500
1684
  if (mockStatus !== void 0) {
1501
1685
  flow.setMock({
1502
1686
  status: mockStatus,
@@ -1621,10 +1805,14 @@ function registerTools(server, client) {
1621
1805
  includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
1622
1806
  },
1623
1807
  withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1624
- const [email, view] = await Promise.all([
1625
- client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1626
- emailEndpointView(client, endpointSlug)
1627
- ]);
1808
+ const view = await emailEndpointView(client, endpointSlug);
1809
+ const email = await client.emails.waitFor(endpointSlug, {
1810
+ tag,
1811
+ subject,
1812
+ from,
1813
+ timeout,
1814
+ since
1815
+ });
1628
1816
  return jsonContent(emailDetail(client, email, view, includeHtml));
1629
1817
  })
1630
1818
  );
@@ -1640,9 +1828,79 @@ function registerTools(server, client) {
1640
1828
  return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1641
1829
  })
1642
1830
  );
1831
+ server.tool(
1832
+ "list_destinations",
1833
+ "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.",
1834
+ {
1835
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1836
+ summary: import_zod2.z.boolean().default(true).describe("Add each destination's last 24 hours")
1837
+ },
1838
+ withErrorHandling(async ({ slug, summary }) => {
1839
+ return jsonContent(await client.destinations.list(slug, { summary }));
1840
+ })
1841
+ );
1842
+ server.tool(
1843
+ "add_destination",
1844
+ "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.",
1845
+ {
1846
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1847
+ url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
1848
+ ...destinationFields
1849
+ },
1850
+ withErrorHandling(async ({ slug, ...input }) => {
1851
+ return jsonContent(await client.destinations.create(slug, input));
1852
+ })
1853
+ );
1854
+ server.tool(
1855
+ "update_destination",
1856
+ "Change a destination: any of its fields; only the ones you pass change. Owner only.",
1857
+ {
1858
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1859
+ destinationId: import_zod2.z.string().describe("The destination's id (from list_destinations)"),
1860
+ url: httpUrlSchema.optional().describe("A new URL"),
1861
+ ...destinationFields
1862
+ },
1863
+ withErrorHandling(async ({ slug, destinationId, ...input }) => {
1864
+ return jsonContent(await client.destinations.update(slug, destinationId, input));
1865
+ })
1866
+ );
1867
+ server.tool(
1868
+ "remove_destination",
1869
+ "Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
1870
+ {
1871
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1872
+ destinationId: import_zod2.z.string().describe("The destination's id")
1873
+ },
1874
+ withErrorHandling(async ({ slug, destinationId }) => {
1875
+ await client.destinations.delete(slug, destinationId);
1876
+ return textContent(`Destination ${destinationId} removed.`);
1877
+ })
1878
+ );
1879
+ server.tool(
1880
+ "preview_destination",
1881
+ "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.",
1882
+ {
1883
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1884
+ destinationId: import_zod2.z.string().optional().describe("A saved destination"),
1885
+ destination: import_zod2.z.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
1886
+ requestId: import_zod2.z.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
1887
+ },
1888
+ withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
1889
+ if (!destinationId && !destination?.url) {
1890
+ throw new Error("Pass destinationId, or a destination with a url");
1891
+ }
1892
+ return jsonContent(
1893
+ await client.destinations.preview(slug, {
1894
+ ...destinationId ? { destinationId } : {},
1895
+ ...destination ? { destination } : {},
1896
+ ...requestId ? { requestId } : {}
1897
+ })
1898
+ );
1899
+ })
1900
+ );
1643
1901
  server.tool(
1644
1902
  "configure_forwarding",
1645
- "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.",
1903
+ "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.",
1646
1904
  {
1647
1905
  slug: import_zod2.z.string().describe("The endpoint slug"),
1648
1906
  url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
@@ -1667,8 +1925,8 @@ function registerTools(server, client) {
1667
1925
  "JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
1668
1926
  )
1669
1927
  },
1670
- withErrorHandling(async ({ slug, ...options }) => {
1671
- const endpoint = await client.forwarding.configure(slug, options);
1928
+ withErrorHandling(async ({ slug, ...options2 }) => {
1929
+ const endpoint = await client.forwarding.configure(slug, options2);
1672
1930
  return jsonContent({
1673
1931
  slug: endpoint.slug,
1674
1932
  forwardEnabled: endpoint.forwardEnabled ?? false,
@@ -1690,23 +1948,32 @@ function registerTools(server, client) {
1690
1948
  "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.",
1691
1949
  {
1692
1950
  slug: import_zod2.z.string().describe("The endpoint slug"),
1951
+ destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1693
1952
  rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1694
1953
  },
1695
- withErrorHandling(async ({ slug, rotate }) => {
1696
- const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1954
+ withErrorHandling(async ({ slug, destinationId, rotate }) => {
1955
+ 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);
1697
1956
  return jsonContent({ secret, rotated: rotate });
1698
1957
  })
1699
1958
  );
1700
1959
  server.tool(
1701
1960
  "test_forwarding",
1702
- "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.",
1961
+ "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.",
1703
1962
  {
1704
1963
  slug: import_zod2.z.string().describe("The endpoint slug"),
1705
- kind: import_zod2.z.enum(["http", "email"]).optional().describe(
1706
- "Which kind to send (default: http when HTTP requests are forwarded, else email)"
1707
- )
1964
+ destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1965
+ kind: import_zod2.z.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
1966
+ requestId: import_zod2.z.string().optional().describe("With destinationId: a captured request to send")
1708
1967
  },
1709
- withErrorHandling(async ({ slug, kind }) => {
1968
+ withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
1969
+ if (destinationId) {
1970
+ return jsonContent(
1971
+ await client.destinations.test(slug, destinationId, {
1972
+ ...kind ? { kind } : {},
1973
+ ...requestId ? { requestId } : {}
1974
+ })
1975
+ );
1976
+ }
1710
1977
  return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1711
1978
  })
1712
1979
  );
@@ -1717,17 +1984,25 @@ function registerTools(server, client) {
1717
1984
  endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's delivery log"),
1718
1985
  requestId: import_zod2.z.string().optional().describe("List every delivery and try of this request"),
1719
1986
  limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1720
- status: import_zod2.z.enum(["all", "pending", "failed"]).default("all").describe("With endpointSlug: all, pending (queued or retrying) or failed"),
1987
+ status: import_zod2.z.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
1988
+ "With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
1989
+ ),
1990
+ destinationId: import_zod2.z.string().optional().describe("With endpointSlug: only this destination"),
1721
1991
  before: import_zod2.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1722
1992
  },
1723
- withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
1993
+ withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
1724
1994
  if (requestId === void 0 === (endpointSlug === void 0)) {
1725
1995
  throw new Error("Pass exactly one of endpointSlug or requestId");
1726
1996
  }
1727
1997
  if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1728
1998
  const [summary, deliveries] = await Promise.all([
1729
1999
  client.forwarding.summary(endpointSlug),
1730
- client.forwarding.deliveries(endpointSlug, { limit, status, before })
2000
+ client.forwarding.deliveries(endpointSlug, {
2001
+ limit,
2002
+ status,
2003
+ before,
2004
+ ...destinationId ? { destinationId } : {}
2005
+ })
1731
2006
  ]);
1732
2007
  return jsonContent({
1733
2008
  summary,
@@ -1738,17 +2013,19 @@ function registerTools(server, client) {
1738
2013
  );
1739
2014
  server.tool(
1740
2015
  "redeliver",
1741
- "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.",
2016
+ "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.",
1742
2017
  {
1743
2018
  requestId: import_zod2.z.string().optional().describe("The request or email to send again"),
1744
- endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again")
2019
+ endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again"),
2020
+ destinationId: import_zod2.z.string().optional().describe("Only this destination")
1745
2021
  },
1746
- withErrorHandling(async ({ requestId, endpointSlug }) => {
2022
+ withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
1747
2023
  if (requestId === void 0 === (endpointSlug === void 0)) {
1748
2024
  throw new Error("Pass exactly one of requestId or endpointSlug");
1749
2025
  }
2026
+ const options2 = destinationId ? { destinationId } : {};
1750
2027
  return jsonContent(
1751
- requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
2028
+ requestId ? await client.forwarding.redeliver(requestId, options2) : await client.forwarding.redeliverFailed(endpointSlug, options2)
1752
2029
  );
1753
2030
  })
1754
2031
  );
@@ -1761,6 +2038,7 @@ function registerTools(server, client) {
1761
2038
  return jsonContent(description);
1762
2039
  })
1763
2040
  );
2041
+ return handles;
1764
2042
  }
1765
2043
 
1766
2044
  // src/sandbox-tools.ts
@@ -1772,7 +2050,6 @@ var SANDBOX = {
1772
2050
  };
1773
2051
  var DEFAULT_WAIT_SECONDS = 45;
1774
2052
  var MAX_WAIT_SECONDS = 600;
1775
- var TOKEN_REFRESH_MARGIN_MS = 6e4;
1776
2053
  var DOCS_URL = "https://webhooks.cc/docs/agents";
1777
2054
  var ACCOUNT_ONLY_OPTIONS = [
1778
2055
  "mockResponse",
@@ -1783,20 +2060,29 @@ var ACCOUNT_ONLY_OPTIONS = [
1783
2060
  "signingHeader"
1784
2061
  ];
1785
2062
  var AgentSession = class {
1786
- constructor(baseUrl, webhookUrl, clientName) {
2063
+ constructor(baseUrl, webhookUrl, clientName, credentialsFile) {
1787
2064
  this.baseUrl = baseUrl;
1788
2065
  this.webhookUrl = webhookUrl;
1789
2066
  this.clientName = clientName;
2067
+ this.credentialsFile = credentialsFile;
1790
2068
  this.sandbox = null;
1791
- this.claim = null;
1792
- this.connected = false;
2069
+ this.connect = null;
2070
+ }
2071
+ sandboxOptions() {
2072
+ return {
2073
+ baseUrl: this.baseUrl,
2074
+ webhookUrl: this.webhookUrl,
2075
+ clientName: this.clientName,
2076
+ credentialsFile: this.credentialsFile
2077
+ };
1793
2078
  }
1794
2079
  /**
1795
- * The sandbox, registered on first use and again after it expired. Tool
1796
- * calls can run in parallel: this.sandbox is read again after every await,
1797
- * and a new sandbox is assigned with no await in between, so parallel
1798
- * calls share one registration. Calls waiting on a registration that fails
1799
- * get its error (a 429 asks them to wait); the next call starts again.
2080
+ * The sandbox: the one saved by an earlier session when it is still live,
2081
+ * else a new registration (and again after it expired). Tool calls can run
2082
+ * in parallel: this.sandbox is read again after every await, and a new
2083
+ * sandbox is assigned with no await in between, so parallel calls share
2084
+ * one registration. Calls waiting on a registration that fails get its
2085
+ * error (a 429 asks them to wait); the next call starts again.
1800
2086
  */
1801
2087
  async sandboxClient() {
1802
2088
  while (this.sandbox) {
@@ -1804,35 +2090,36 @@ var AgentSession = class {
1804
2090
  const sandbox = await pending;
1805
2091
  if (!this.expired(pending, sandbox)) return sandbox;
1806
2092
  }
1807
- const created = import_sdk2.WebhooksCC.sandbox({
1808
- baseUrl: this.baseUrl,
1809
- webhookUrl: this.webhookUrl,
1810
- clientName: this.clientName
1811
- });
2093
+ const created = import_sdk2.WebhooksCC.sandbox(this.sandboxOptions());
1812
2094
  this.sandbox = created;
1813
2095
  created.catch(() => {
1814
2096
  if (this.sandbox === created) this.sandbox = null;
1815
2097
  });
1816
2098
  return created;
1817
2099
  }
1818
- /** The live sandbox, if one was registered; never registers one. */
2100
+ /** The live sandbox, from this session or the credential file; never registers one. */
1819
2101
  async existingSandbox() {
1820
2102
  const pending = this.sandbox;
1821
- if (!pending) return null;
1822
- let sandbox;
1823
- try {
1824
- sandbox = await pending;
1825
- } catch {
1826
- return null;
2103
+ if (pending) {
2104
+ let sandbox;
2105
+ try {
2106
+ sandbox = await pending;
2107
+ } catch {
2108
+ return null;
2109
+ }
2110
+ return this.expired(pending, sandbox) ? null : sandbox;
1827
2111
  }
1828
- return this.expired(pending, sandbox) ? null : sandbox;
2112
+ const resumed = await import_sdk2.SandboxClient.resume(this.sandboxOptions()).catch(() => null);
2113
+ if (!resumed) return null;
2114
+ if (!this.sandbox) this.sandbox = Promise.resolve(resumed);
2115
+ return this.sandbox;
1829
2116
  }
1830
2117
  /** True when the sandbox is past its 24 hours; then it is dropped. */
1831
2118
  expired(pending, sandbox) {
1832
2119
  if (sandbox.expiresAt.getTime() > Date.now()) return false;
1833
2120
  if (this.sandbox === pending) this.sandbox = null;
1834
- if (this.claim?.kind === "sandbox" && this.claim.claimToken === sandbox.registration.claimToken) {
1835
- this.claim = null;
2121
+ if (this.connect?.via === "sandbox" && this.connect.sandbox === sandbox) {
2122
+ this.connect = null;
1836
2123
  }
1837
2124
  return true;
1838
2125
  }
@@ -1843,26 +2130,11 @@ var AgentSession = class {
1843
2130
  }
1844
2131
  return sandbox;
1845
2132
  }
1846
- forgetSandbox() {
2133
+ reset() {
1847
2134
  this.sandbox = null;
2135
+ this.connect = null;
1848
2136
  }
1849
2137
  };
1850
- function claimedTokens(baseUrl, assertion, first) {
1851
- let current = first;
1852
- let pending = null;
1853
- return async (options) => {
1854
- if (!options?.forceRefresh && current.expiresAt.getTime() - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
1855
- return current.accessToken;
1856
- }
1857
- if (!pending) {
1858
- pending = import_sdk2.WebhooksCC.agent.exchange({ baseUrl, assertion }).finally(() => {
1859
- pending = null;
1860
- });
1861
- }
1862
- current = await pending;
1863
- return current.accessToken;
1864
- };
1865
- }
1866
2138
  function minutesLeft(until) {
1867
2139
  return Math.max(1, Math.round((until.getTime() - Date.now()) / 6e4));
1868
2140
  }
@@ -1870,16 +2142,53 @@ function onlyKind(requests, kind) {
1870
2142
  if (!kind) return requests;
1871
2143
  return requests.filter((request) => (request.kind ?? "http") === kind);
1872
2144
  }
2145
+ function savedTo(credentialsFile) {
2146
+ return import_sdk2.AgentCredentialStore.resolve(credentialsFile)?.path ?? null;
2147
+ }
1873
2148
  function registerSandboxTools(server, options = {}) {
1874
2149
  const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL ?? "https://webhooks.cc";
2150
+ const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
1875
2151
  const session = new AgentSession(
1876
2152
  baseUrl,
1877
- options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
1878
- options.clientName ?? "webhooks-cc-mcp"
2153
+ webhookUrl,
2154
+ options.clientName ?? "webhooks-cc-mcp",
2155
+ options.credentialsFile
1879
2156
  );
1880
- const registered = [];
2157
+ let handles = [];
2158
+ let shown = 0;
2159
+ const swap = (register) => {
2160
+ const id = ++shown;
2161
+ for (const handle of handles) handle.remove();
2162
+ handles = register(id);
2163
+ };
2164
+ const showSandbox = () => swap(() => sandboxToolSet(server, session, showAccount));
2165
+ const savedAccount = (id) => options.credentialsFile === false ? null : import_sdk2.WebhooksCC.fromSavedConnection({
2166
+ baseUrl,
2167
+ webhookUrl,
2168
+ credentialsFile: options.credentialsFile,
2169
+ // The human disconnected the agent: back to the sandbox, unless a
2170
+ // newer connection is already on display.
2171
+ onRevoked: () => queueMicrotask(() => {
2172
+ if (shown === id) showSandbox();
2173
+ })
2174
+ });
2175
+ function showAccount(client) {
2176
+ swap((id) => registerTools(server, savedAccount(id) ?? client));
2177
+ options.onConnected?.(client);
2178
+ }
2179
+ const saved = savedAccount(1);
2180
+ if (saved) {
2181
+ swap(() => registerTools(server, saved));
2182
+ options.onConnected?.(saved);
2183
+ } else {
2184
+ showSandbox();
2185
+ }
2186
+ }
2187
+ function sandboxToolSet(server, session, showAccount) {
2188
+ const tools = [];
1881
2189
  const limits = `up to ${SANDBOX.maxEndpoints} endpoints at a time, ${SANDBOX.requestsPerEndpoint} captured requests each and ${SANDBOX.requests} in all, deleted ${SANDBOX.lifetimeHours} hours after the sandbox was created`;
1882
- registered.push(
2190
+ const file = savedTo(session.credentialsFile);
2191
+ tools.push(
1883
2192
  server.tool(
1884
2193
  "create_endpoint",
1885
2194
  `Create a temporary webhook capture endpoint in the agent sandbox (no account needed): ${limits}. The first call registers the sandbox, which takes a few seconds of proof of work. Mock responses, notifications and signature verification need an account: see connect_account.`,
@@ -1903,7 +2212,7 @@ function registerSandboxTools(server, options = {}) {
1903
2212
  })
1904
2213
  )
1905
2214
  );
1906
- registered.push(
2215
+ tools.push(
1907
2216
  server.tool(
1908
2217
  "list_endpoints",
1909
2218
  "List this agent's sandbox endpoints. Empty until create_endpoint starts the sandbox.",
@@ -1917,7 +2226,7 @@ function registerSandboxTools(server, options = {}) {
1917
2226
  })
1918
2227
  )
1919
2228
  );
1920
- registered.push(
2229
+ tools.push(
1921
2230
  server.tool(
1922
2231
  "get_endpoint",
1923
2232
  "Get one of this agent's sandbox endpoints by slug, with its sandbox limits and usage.",
@@ -1927,7 +2236,7 @@ function registerSandboxTools(server, options = {}) {
1927
2236
  })
1928
2237
  )
1929
2238
  );
1930
- registered.push(
2239
+ tools.push(
1931
2240
  server.tool(
1932
2241
  "delete_endpoint",
1933
2242
  "Delete one of this agent's sandbox endpoints and its captured requests. Frees a slot; the sandbox's request budget is not refunded.",
@@ -1938,7 +2247,7 @@ function registerSandboxTools(server, options = {}) {
1938
2247
  })
1939
2248
  )
1940
2249
  );
1941
- registered.push(
2250
+ tools.push(
1942
2251
  server.tool(
1943
2252
  "list_requests",
1944
2253
  "List recent requests captured by a sandbox endpoint, newest first.",
@@ -1950,7 +2259,7 @@ function registerSandboxTools(server, options = {}) {
1950
2259
  })
1951
2260
  )
1952
2261
  );
1953
- registered.push(
2262
+ tools.push(
1954
2263
  server.tool(
1955
2264
  "get_request",
1956
2265
  "Get full details for a request captured by a sandbox endpoint.",
@@ -1961,7 +2270,7 @@ function registerSandboxTools(server, options = {}) {
1961
2270
  })
1962
2271
  )
1963
2272
  );
1964
- registered.push(
2273
+ tools.push(
1965
2274
  server.tool(
1966
2275
  "wait_for_request",
1967
2276
  "Wait for a request to arrive at a sandbox endpoint.",
@@ -1973,7 +2282,7 @@ function registerSandboxTools(server, options = {}) {
1973
2282
  })
1974
2283
  )
1975
2284
  );
1976
- registered.push(
2285
+ tools.push(
1977
2286
  server.tool(
1978
2287
  "generate_payload_schema",
1979
2288
  "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.",
@@ -1985,62 +2294,59 @@ function registerSandboxTools(server, options = {}) {
1985
2294
  })
1986
2295
  )
1987
2296
  );
1988
- registered.push(
2297
+ tools.push(
1989
2298
  server.tool(
1990
2299
  "connect_account",
1991
2300
  "Connect this agent to a human's webhooks.cc account, for the full tool set (mock responses, signatures, email, teams) and to keep the sandbox endpoints. Returns a link and a 6-digit code: show both to the human, who signs in or signs up with that email and enters the code. Then call wait_for_connection. Calling again gives a new code.",
1992
2301
  {
1993
- email: import_zod3.z.string().email({ message: "Invalid email address" }).describe("The human's email address; only someone signed in as it can connect")
2302
+ email: import_zod3.z.string().email({ message: "Invalid email address" }).describe(
2303
+ "The email the human signs in to webhooks.cc with (their GitHub or Google primary email if they sign in that way); only someone signed in as it can connect"
2304
+ )
1994
2305
  },
1995
2306
  withErrorHandling(async ({ email }) => {
1996
- if (session.connected) {
1997
- return jsonContent({ connected: true, message: "Already connected to an account." });
1998
- }
1999
2307
  const normalized = email.trim().toLowerCase();
2308
+ const previous = session.connect;
2000
2309
  const sandbox = await session.existingSandbox();
2001
- let claim;
2310
+ let attempt;
2311
+ let movesEndpoints = false;
2002
2312
  if (sandbox) {
2003
- claim = {
2004
- kind: "sandbox",
2005
- claimToken: sandbox.registration.claimToken,
2006
- email: normalized,
2007
- attempt: await sandbox.claim({ email: normalized }),
2008
- claimExpires: sandbox.expiresAt
2009
- };
2010
- } else if (session.claim?.kind === "service_auth" && session.claim.email === normalized && session.claim.claimExpires.getTime() > Date.now()) {
2011
- const attempt = await import_sdk2.WebhooksCC.agent.startClaim({
2012
- baseUrl,
2013
- claimToken: session.claim.claimToken
2014
- });
2015
- claim = { ...session.claim, attempt };
2313
+ const claim = await sandbox.claim({ email: normalized });
2314
+ session.connect = { via: "sandbox", sandbox, email: normalized, attempt: claim };
2315
+ attempt = claim;
2316
+ movesEndpoints = claim.movesEndpoints;
2016
2317
  } else {
2017
- const registration = await import_sdk2.WebhooksCC.agent.registerServiceAuth({
2018
- baseUrl,
2019
- email: normalized,
2020
- clientName: session.clientName
2021
- });
2022
- claim = {
2023
- kind: "service_auth",
2024
- claimToken: registration.claimToken,
2318
+ let pending = null;
2319
+ if (previous?.via === "connect" && previous.email === normalized) {
2320
+ pending = await previous.pending.restart().catch((error) => {
2321
+ if (error instanceof import_sdk2.AgentAuthError) return null;
2322
+ throw error;
2323
+ });
2324
+ }
2325
+ pending ?? (pending = await import_sdk2.WebhooksCC.connect({
2326
+ baseUrl: session.baseUrl,
2327
+ webhookUrl: session.webhookUrl,
2025
2328
  email: normalized,
2026
- attempt: registration.claim,
2027
- claimExpires: registration.claimTokenExpires
2028
- };
2329
+ clientName: session.clientName,
2330
+ credentialsFile: session.credentialsFile
2331
+ }));
2332
+ session.connect = { via: "connect", pending, email: normalized };
2333
+ attempt = pending;
2029
2334
  }
2030
- session.claim = claim;
2031
- const { verificationUri, userCode, expiresAt } = claim.attempt;
2335
+ const { verificationUri, userCode, expiresAt } = attempt;
2032
2336
  return jsonContent({
2033
2337
  verificationUri,
2034
2338
  userCode,
2035
2339
  expiresAt: expiresAt.toISOString(),
2036
2340
  tellTheHuman: `Open ${verificationUri}, sign in or sign up as ${normalized}, and enter the code ${userCode}.`,
2037
2341
  next: `Show the human that line, then call wait_for_connection. The code works for about ${minutesLeft(expiresAt)} minutes; connect_account gives a new one.`,
2038
- ...claim.kind === "sandbox" ? { endpoints: "The sandbox endpoints move into that account with what they captured." } : {}
2342
+ ...sandbox ? {
2343
+ endpoints: movesEndpoints ? "The sandbox endpoints move into that account with what they captured." : "The sandbox endpoints were made in an earlier session, so they stay in the sandbox until it expires; the account starts without them."
2344
+ } : {}
2039
2345
  });
2040
2346
  })
2041
2347
  )
2042
2348
  );
2043
- registered.push(
2349
+ tools.push(
2044
2350
  server.tool(
2045
2351
  "wait_for_connection",
2046
2352
  `Wait for the human to enter the code from connect_account. On success the full webhooks.cc tools for their account replace these sandbox tools. Returns after timeoutSeconds (default ${DEFAULT_WAIT_SECONDS}) if they have not finished yet; call it again to keep waiting.`,
@@ -2048,28 +2354,22 @@ function registerSandboxTools(server, options = {}) {
2048
2354
  timeoutSeconds: import_zod3.z.number().int().min(1).max(MAX_WAIT_SECONDS).optional().describe(`How long to wait, in seconds (default ${DEFAULT_WAIT_SECONDS})`)
2049
2355
  },
2050
2356
  withErrorHandling(async ({ timeoutSeconds }) => {
2051
- if (session.connected) {
2052
- return jsonContent({ connected: true, message: "Already connected to an account." });
2053
- }
2054
- const claim = session.claim;
2055
- if (!claim) {
2357
+ const pending = session.connect;
2358
+ if (!pending) {
2056
2359
  throw new Error("Nothing to wait for: call connect_account first.");
2057
2360
  }
2058
- let claimed;
2361
+ const timeout = (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
2362
+ const expiresAt = pending.via === "sandbox" ? pending.attempt.expiresAt : pending.pending.expiresAt;
2363
+ let client;
2059
2364
  try {
2060
- claimed = await import_sdk2.WebhooksCC.agent.waitForClaim({
2061
- baseUrl,
2062
- claimToken: claim.claimToken,
2063
- interval: claim.attempt.interval,
2064
- timeout: (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3
2065
- });
2365
+ client = pending.via === "sandbox" ? await pending.sandbox.waitForClaim({ timeout }) : await pending.pending.wait({ timeout });
2066
2366
  } catch (error) {
2067
2367
  if (!(error instanceof import_sdk2.AgentAuthError)) throw error;
2068
2368
  if (error.code === "timeout") {
2069
2369
  return jsonContent({
2070
2370
  connected: false,
2071
2371
  status: "waiting",
2072
- message: `Not connected yet. The code works until ${claim.attempt.expiresAt.toISOString()}; call wait_for_connection again.`
2372
+ message: `Not connected yet. The code works until ${expiresAt.toISOString()}; call wait_for_connection again.`
2073
2373
  });
2074
2374
  }
2075
2375
  if (error.code === "expired_token") {
@@ -2088,34 +2388,25 @@ function registerSandboxTools(server, options = {}) {
2088
2388
  }
2089
2389
  throw error;
2090
2390
  }
2091
- const client = new import_sdk2.WebhooksCC({
2092
- baseUrl,
2093
- webhookUrl: session.webhookUrl,
2094
- getAccessToken: claimedTokens(baseUrl, claimed.identityAssertion, claimed.token)
2095
- });
2096
- session.connected = true;
2097
- session.claim = null;
2098
- session.forgetSandbox();
2099
- for (const tool of registered) tool.remove();
2100
- registerTools(server, client);
2101
- options.onConnected?.(client);
2391
+ const movedEndpoints = pending.via === "sandbox" && pending.attempt.movesEndpoints;
2392
+ session.reset();
2393
+ showAccount(client);
2102
2394
  return jsonContent({
2103
2395
  connected: true,
2104
- email: claim.email,
2105
- scope: claimed.token.scope,
2106
- message: `Connected to the webhooks.cc account of ${claim.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${claim.kind === "sandbox" ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY.`
2396
+ email: pending.email,
2397
+ message: `Connected to the webhooks.cc account of ${pending.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${movedEndpoints ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} ${file ? `The connection is saved in ${file}, so later sessions, the whk CLI and the SDKs start connected; it lasts 90 days, and the human can remove it under Account, Connected agents.` : "This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY."}`
2107
2398
  });
2108
2399
  })
2109
2400
  )
2110
2401
  );
2111
- registered.push(
2402
+ tools.push(
2112
2403
  server.tool(
2113
2404
  "about_sandbox",
2114
2405
  "Explain what this agent can do without a webhooks.cc account, the sandbox limits, and how to connect an account. No network call.",
2115
2406
  {},
2116
2407
  withErrorHandling(async () => {
2117
2408
  return jsonContent({
2118
- mode: "sandbox (no WHK_API_KEY)",
2409
+ mode: "sandbox (no WHK_API_KEY, no saved connection)",
2119
2410
  sandbox: {
2120
2411
  maxEndpoints: SANDBOX.maxEndpoints,
2121
2412
  requestsPerEndpoint: SANDBOX.requestsPerEndpoint,
@@ -2126,24 +2417,26 @@ function registerSandboxTools(server, options = {}) {
2126
2417
  notAvailable: "Mock responses, response rules, notifications, signature verification, email capture, forwarding, teams and sending webhooks."
2127
2418
  },
2128
2419
  connectAnAccount: [
2129
- "Call connect_account with the human's email and show them the link and code it returns.",
2420
+ "Call connect_account with the email the human signs in to webhooks.cc with, and show them the link and code it returns.",
2130
2421
  "Call wait_for_connection until it reports connected; the full tools for their account then replace the sandbox tools.",
2131
- "Sandbox endpoints move into the account with their captured requests."
2422
+ "Sandbox endpoints registered by this server process move into the account with their captured requests; a sandbox resumed from an earlier session stays in the sandbox (connect_account says which)."
2132
2423
  ],
2424
+ savedIn: file ? `${file} (shared with the whk CLI and the SDKs): the sandbox and the connection survive restarts.` : "nowhere: this session only.",
2133
2425
  withAnApiKey: "Set WHK_API_KEY to a whcc_ key from the dashboard and restart for the full tools from the start.",
2134
2426
  docs: DOCS_URL,
2135
- protocol: `${baseUrl}/auth.md`
2427
+ protocol: `${session.baseUrl}/auth.md`
2136
2428
  });
2137
2429
  })
2138
2430
  )
2139
2431
  );
2432
+ return tools;
2140
2433
  }
2141
2434
  function registerAgentRegistrationTools(server) {
2142
2435
  registerSandboxTools(server);
2143
2436
  }
2144
2437
 
2145
2438
  // src/index.ts
2146
- var VERSION = true ? "1.13.0" : "0.0.0-dev";
2439
+ var VERSION = true ? "1.15.0" : "0.0.0-dev";
2147
2440
  function createServer(options = {}) {
2148
2441
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2149
2442
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -2154,7 +2447,7 @@ function createServer(options = {}) {
2154
2447
  { debouncedNotificationMethods: ["notifications/tools/list_changed"] }
2155
2448
  );
2156
2449
  if (!apiKey) {
2157
- registerSandboxTools(server, { baseUrl, webhookUrl });
2450
+ registerSandboxTools(server, { baseUrl, webhookUrl, credentialsFile: options.credentialsFile });
2158
2451
  registerPrompts(server);
2159
2452
  return server;
2160
2453
  }
@@ -2166,6 +2459,7 @@ function createServer(options = {}) {
2166
2459
  }
2167
2460
  // Annotate the CommonJS export names for ESM import in node:
2168
2461
  0 && (module.exports = {
2462
+ REMOTE_MAX_WAIT_MS,
2169
2463
  createServer,
2170
2464
  registerAgentRegistrationTools,
2171
2465
  registerPrompts,