@webhooks-cc/mcp 1.9.0 → 1.10.1

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
@@ -58,7 +58,7 @@ Get your API key at [webhooks.cc/account](https://webhooks.cc/account).
58
58
 
59
59
  ## What it exposes
60
60
 
61
- The server exposes `35` tools, `3` prompts, and `3` resource surfaces.
61
+ The server exposes `44` tools, `3` prompts, and `3` resource surfaces.
62
62
 
63
63
  ### Endpoint tools
64
64
 
@@ -84,6 +84,27 @@ The server exposes `35` tools, `3` prompts, and `3` resource surfaces.
84
84
  - `verify_signature`
85
85
  - `clear_requests`
86
86
 
87
+ ### Email tools
88
+
89
+ Every endpoint on an account receives email at `{slug}@mailhooks.cc` (and `{slug}+{tag}@mailhooks.cc`). These tools read it, with the one-time code and main link already picked out, unless the endpoint's owner turned that off:
90
+
91
+ - `list_emails`
92
+ - `get_email`
93
+ - `wait_for_email`
94
+ - `send_test_email`
95
+
96
+ 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.
97
+
98
+ ### Forwarding tools
99
+
100
+ Forward every captured email to a URL as signed JSON (owner only):
101
+
102
+ - `configure_forwarding`
103
+ - `get_forwarding_secret` (returns the `whsec_` secret; with `rotate: true`, replaces it)
104
+ - `test_forwarding`
105
+ - `list_deliveries`
106
+ - `redeliver_email`
107
+
87
108
  ### Send and test tools
88
109
 
89
110
  - `send_webhook`
package/dist/bin/mcp.js CHANGED
@@ -120,6 +120,149 @@ function registerPrompts(server) {
120
120
 
121
121
  // src/resources.ts
122
122
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
123
+
124
+ // src/compact.ts
125
+ var MAX_OUTPUT = 32768;
126
+ var MAX_EMAIL_TEXT = 8e3;
127
+ var MAX_EXTRACTS = 10;
128
+ var MAX_LINK_TEXT = 200;
129
+ var MAX_LINK_URL = 2e3;
130
+ var MAX_LIST_ITEMS = 5;
131
+ var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
132
+ var TEXT_FLOOR = 2e3;
133
+ function jsonSize(value) {
134
+ return JSON.stringify(value, null, 2).length;
135
+ }
136
+ function sliceText(text, end) {
137
+ if (end <= 0) return "";
138
+ if (end >= text.length) return text;
139
+ const last = text.charCodeAt(end - 1);
140
+ return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
141
+ }
142
+ function cutEmailText(text) {
143
+ if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
144
+ return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
145
+ }
146
+ function shrinkToFit(root, budget, steps) {
147
+ for (const step of steps) {
148
+ if (jsonSize(root) <= budget) return;
149
+ step();
150
+ }
151
+ }
152
+ function omitHeaders(holder) {
153
+ const headers = holder.headers;
154
+ if (headers === null || typeof headers !== "object") return;
155
+ const count = Object.keys(headers).length;
156
+ if (count === 0) return;
157
+ holder.headers = {};
158
+ holder.headersOmitted = `${count} headers left out to fit the output`;
159
+ }
160
+ function capExtracts(holder) {
161
+ if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
162
+ holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
163
+ holder.codesTruncated = true;
164
+ }
165
+ if (Array.isArray(holder.links)) {
166
+ if (holder.links.length > MAX_EXTRACTS) {
167
+ holder.links = holder.links.slice(0, MAX_EXTRACTS);
168
+ holder.linksTruncated = true;
169
+ }
170
+ holder.links = holder.links.map((link) => ({
171
+ ...link,
172
+ ...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
173
+ ...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
174
+ }));
175
+ }
176
+ }
177
+ function capLists(holder) {
178
+ for (const key of EMAIL_LISTS) {
179
+ const list = holder[key];
180
+ if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
181
+ holder[key] = list.slice(0, MAX_LIST_ITEMS);
182
+ holder[`${key}Total`] = list.length;
183
+ }
184
+ }
185
+ }
186
+ function keepOnly(holder, keep) {
187
+ for (const key of Object.keys(holder)) {
188
+ if (!keep.includes(key)) delete holder[key];
189
+ }
190
+ holder.trimmed = "The email was too big for the output, so only these fields are shown.";
191
+ }
192
+ function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
193
+ const value = holder[key];
194
+ if (typeof value !== "string" || value.length <= floor) return;
195
+ const flagged = holder[flag] === true;
196
+ holder[key] = "";
197
+ holder[flag] = true;
198
+ const room = budget - jsonSize(root) + 2;
199
+ let low = 0;
200
+ let high = value.length;
201
+ while (low < high) {
202
+ const mid = Math.ceil((low + high) / 2);
203
+ if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
204
+ else high = mid - 1;
205
+ }
206
+ const kept = sliceText(value, Math.max(low, floor));
207
+ holder[key] = kept;
208
+ if (kept.length === value.length && !flagged) delete holder[flag];
209
+ }
210
+ function keepFirst(holder, key) {
211
+ const list = holder[key];
212
+ if (!Array.isArray(list) || list.length <= 1) return;
213
+ holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
214
+ holder[key] = list.slice(0, 1);
215
+ }
216
+ var ESSENTIAL_EMAIL_FIELDS = [
217
+ "subject",
218
+ "from",
219
+ "fromTotal",
220
+ "tag",
221
+ "date",
222
+ "text",
223
+ "textTruncated",
224
+ "htmlSize"
225
+ ];
226
+ function compactRequest(request) {
227
+ if (request.kind !== "email") return request;
228
+ const compact = { ...request };
229
+ delete compact.body;
230
+ delete compact.bodyRaw;
231
+ compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
232
+ if (!request.email) return compact;
233
+ const { html, ...rest } = request.email;
234
+ const { text, cut } = cutEmailText(rest.text);
235
+ const restoreText = () => {
236
+ email.text = text;
237
+ if (cut) email.textTruncated = true;
238
+ else delete email.textTruncated;
239
+ };
240
+ const email = {
241
+ ...rest,
242
+ text,
243
+ ...cut ? { textTruncated: true } : {},
244
+ htmlSize: html?.length ?? 0
245
+ };
246
+ compact.email = email;
247
+ const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
248
+ const budget = MAX_OUTPUT - 128;
249
+ shrinkToFit(root, budget, [
250
+ () => omitHeaders(compact),
251
+ () => capLists(email),
252
+ () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
253
+ // Down to the essentials there may be room for more of the text again.
254
+ () => {
255
+ keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
256
+ keepFirst(email, "from");
257
+ restoreText();
258
+ cutStringToFit(root, email, "text", "textTruncated", budget);
259
+ },
260
+ () => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
261
+ ]);
262
+ return compact;
263
+ }
264
+
265
+ // src/resources.ts
123
266
  var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
124
267
  var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
125
268
  var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
@@ -217,7 +360,7 @@ function registerResources(server, client) {
217
360
  ]);
218
361
  return jsonResource(uri.toString(), {
219
362
  endpoint,
220
- requests
363
+ requests: requests.map(compactRequest)
221
364
  });
222
365
  }
223
366
  );
@@ -235,7 +378,7 @@ function registerResources(server, client) {
235
378
  async (uri, variables) => {
236
379
  const id = variableToString(variables.id, "id");
237
380
  const request = await client.requests.get(id);
238
- return jsonResource(uri.toString(), request);
381
+ return jsonResource(uri.toString(), compactRequest(request));
239
382
  }
240
383
  );
241
384
  }
@@ -243,7 +386,7 @@ function registerResources(server, client) {
243
386
  // src/tools.ts
244
387
  var import_zod2 = require("zod");
245
388
  var import_sdk = require("@webhooks-cc/sdk");
246
- var MAX_BODY_SIZE = 32768;
389
+ var MAX_BODY_SIZE = MAX_OUTPUT;
247
390
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
248
391
  var TIME_SEPARATOR = " \u2014 ";
249
392
  var httpUrlSchema = import_zod2.z.string().url().refine(
@@ -258,6 +401,8 @@ var httpUrlSchema = import_zod2.z.string().url().refine(
258
401
  { message: "Only http and https URLs are supported" }
259
402
  );
260
403
  var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
404
+ var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
405
+ var EMAIL_SCAN_LIMIT = 100;
261
406
  var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
262
407
  var ruleConditionSchema = import_zod2.z.object({
263
408
  field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
@@ -337,6 +482,24 @@ function serializeJson(value, limit = MAX_BODY_SIZE) {
337
482
  return full.slice(0, limit) + `
338
483
  ... [truncated, ${full.length} chars total]`;
339
484
  }
485
+ function fitArrayField(value, key) {
486
+ const items = value[key];
487
+ if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
488
+ const cut = (count) => ({
489
+ ...value,
490
+ [key]: items.slice(0, count),
491
+ truncated: true,
492
+ returned: count
493
+ });
494
+ let low = 0;
495
+ let high = items.length;
496
+ while (low < high) {
497
+ const mid = Math.ceil((low + high) / 2);
498
+ if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
499
+ else high = mid - 1;
500
+ }
501
+ return cut(low);
502
+ }
340
503
  function jsonContent(value) {
341
504
  return textContent(serializeJson(value));
342
505
  }
@@ -485,6 +648,99 @@ async function summarizeResponse(response) {
485
648
  body: await readBodyTruncated(response)
486
649
  };
487
650
  }
651
+ async function emailEndpointView(client, slug) {
652
+ const endpoint = await client.endpoints.get(slug);
653
+ return {
654
+ endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
655
+ includeExtracts: endpoint.showEmailExtracts !== false
656
+ };
657
+ }
658
+ function emailSlugFromAddress(address) {
659
+ const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
660
+ return local.split("+")[0].toLowerCase();
661
+ }
662
+ var MAX_SUMMARY_LINK = 8e3;
663
+ function boundedLink(link, max) {
664
+ if (link == null || link.length <= max) return { link: link ?? null };
665
+ return {
666
+ link: null,
667
+ linkOmitted: `The main link is ${link.length} characters long, too long for the output`
668
+ };
669
+ }
670
+ var MAX_SUMMARY_FIELD = 1e3;
671
+ function summarizeEmail(email, includeExtracts) {
672
+ const from = email.email.from[0];
673
+ const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
674
+ const subject = email.email.subject;
675
+ return {
676
+ id: email.id,
677
+ receivedAt: new Date(email.receivedAt).toISOString(),
678
+ address: email.path,
679
+ tag: email.email.tag,
680
+ subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
681
+ from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
682
+ ...includeExtracts ? { code: (0, import_sdk.extractCode)(email), ...boundedLink((0, import_sdk.extractLink)(email), MAX_SUMMARY_LINK) } : {},
683
+ attachments: email.email.attachments.length
684
+ };
685
+ }
686
+ var ESSENTIAL_DETAIL_FIELDS = [
687
+ "code",
688
+ "link",
689
+ "id",
690
+ "endpoint",
691
+ "receivedAt",
692
+ "address",
693
+ "tag",
694
+ "subject",
695
+ "from",
696
+ "size",
697
+ "test",
698
+ "text",
699
+ "textTruncated",
700
+ "htmlSize",
701
+ "html",
702
+ "htmlTruncated"
703
+ ];
704
+ function emailDetail(client, email, view, includeHtml) {
705
+ const { data } = client.emails.toJson(email, {
706
+ endpoint: view.endpoint,
707
+ includeExtracts: view.includeExtracts
708
+ });
709
+ const { html, text: fullText, ...rest } = data;
710
+ const { text, cut } = cutEmailText(fullText ?? null);
711
+ const restoreText = () => {
712
+ detail.text = text;
713
+ if (cut) detail.textTruncated = true;
714
+ else delete detail.textTruncated;
715
+ };
716
+ const detail = {
717
+ ...view.includeExtracts ? { code: (0, import_sdk.extractCode)(email), link: (0, import_sdk.extractLink)(email) } : {},
718
+ ...rest,
719
+ text,
720
+ ...cut ? { textTruncated: true } : {},
721
+ htmlSize: html?.length ?? 0,
722
+ ...includeHtml && html !== null ? { html } : {}
723
+ };
724
+ shrinkToFit(detail, MAX_BODY_SIZE, [
725
+ () => omitHeaders(detail),
726
+ () => capExtracts(detail),
727
+ () => capLists(detail),
728
+ () => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
729
+ () => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
730
+ // Down to the essentials (and without a main link too long to use) there
731
+ // may be room for more of the text again.
732
+ () => {
733
+ keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
734
+ if (view.includeExtracts) {
735
+ Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
736
+ }
737
+ restoreText();
738
+ cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
739
+ },
740
+ () => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
741
+ ]);
742
+ return detail;
743
+ }
488
744
  function registerTools(server, client) {
489
745
  server.tool(
490
746
  "create_endpoint",
@@ -628,7 +884,7 @@ function registerTools(server, client) {
628
884
  );
629
885
  server.tool(
630
886
  "update_endpoint",
631
- "Update an endpoint name, mock response, conditional response rules, or signing configuration.",
887
+ "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
632
888
  {
633
889
  slug: import_zod2.z.string().describe("The endpoint slug to update"),
634
890
  name: import_zod2.z.string().optional().describe("New display name"),
@@ -641,7 +897,8 @@ function registerTools(server, client) {
641
897
  signingSecret: import_zod2.z.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
642
898
  signingHeader: import_zod2.z.string().nullable().optional().describe(
643
899
  "Custom signature header name (only for generic-hmac provider), or null to clear"
644
- )
900
+ ),
901
+ showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
645
902
  },
646
903
  withErrorHandling(
647
904
  async ({
@@ -652,7 +909,8 @@ function registerTools(server, client) {
652
909
  notificationUrl,
653
910
  signingProvider,
654
911
  signingSecret,
655
- signingHeader
912
+ signingHeader,
913
+ showEmailExtracts
656
914
  }) => {
657
915
  const endpoint = await client.endpoints.update(slug, {
658
916
  name,
@@ -661,7 +919,8 @@ function registerTools(server, client) {
661
919
  notificationUrl,
662
920
  signingProvider,
663
921
  signingSecret,
664
- signingHeader
922
+ signingHeader,
923
+ showEmailExtracts
665
924
  });
666
925
  return jsonContent(endpoint);
667
926
  }
@@ -773,15 +1032,16 @@ function registerTools(server, client) {
773
1032
  );
774
1033
  server.tool(
775
1034
  "list_requests",
776
- "List recent captured requests for an endpoint.",
1035
+ "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
777
1036
  {
778
1037
  endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
779
1038
  limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
780
- since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms")
1039
+ since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms"),
1040
+ kind: kindSchema
781
1041
  },
782
- withErrorHandling(async ({ endpointSlug, limit, since }) => {
783
- const requests = await client.requests.list(endpointSlug, { limit, since });
784
- return jsonContent(requests);
1042
+ withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1043
+ const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1044
+ return jsonContent(requests.map(compactRequest));
785
1045
  })
786
1046
  );
787
1047
  server.tool(
@@ -790,6 +1050,7 @@ function registerTools(server, client) {
790
1050
  {
791
1051
  slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
792
1052
  method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
1053
+ kind: kindSchema,
793
1054
  q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
794
1055
  from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
795
1056
  to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
@@ -797,10 +1058,11 @@ function registerTools(server, client) {
797
1058
  offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("Result offset"),
798
1059
  order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
799
1060
  },
800
- withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
1061
+ withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
801
1062
  const results = await client.requests.search({
802
1063
  slug,
803
1064
  method,
1065
+ kind,
804
1066
  q,
805
1067
  from,
806
1068
  to,
@@ -808,7 +1070,7 @@ function registerTools(server, client) {
808
1070
  offset,
809
1071
  order
810
1072
  });
811
- return jsonContent(results);
1073
+ return jsonContent(results.map(compactRequest));
812
1074
  })
813
1075
  );
814
1076
  server.tool(
@@ -817,12 +1079,13 @@ function registerTools(server, client) {
817
1079
  {
818
1080
  slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
819
1081
  method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
1082
+ kind: kindSchema,
820
1083
  q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
821
1084
  from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
822
1085
  to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
823
1086
  },
824
- withErrorHandling(async ({ slug, method, q, from, to }) => {
825
- const count = await client.requests.count({ slug, method, q, from, to });
1087
+ withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
1088
+ const count = await client.requests.count({ slug, method, kind, q, from, to });
826
1089
  return jsonContent({ count });
827
1090
  })
828
1091
  );
@@ -832,7 +1095,7 @@ function registerTools(server, client) {
832
1095
  { requestId: import_zod2.z.string().describe("The request ID") },
833
1096
  withErrorHandling(async ({ requestId }) => {
834
1097
  const request = await client.requests.get(requestId);
835
- return jsonContent(request);
1098
+ return jsonContent(compactRequest(request));
836
1099
  })
837
1100
  );
838
1101
  server.tool(
@@ -845,7 +1108,7 @@ function registerTools(server, client) {
845
1108
  },
846
1109
  withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
847
1110
  const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
848
- return jsonContent(request);
1111
+ return jsonContent(compactRequest(request));
849
1112
  })
850
1113
  );
851
1114
  server.tool(
@@ -865,7 +1128,9 @@ function registerTools(server, client) {
865
1128
  pollInterval,
866
1129
  method
867
1130
  });
868
- return jsonContent(result);
1131
+ return jsonContent(
1132
+ fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
1133
+ );
869
1134
  })
870
1135
  );
871
1136
  server.tool(
@@ -1174,6 +1439,154 @@ function registerTools(server, client) {
1174
1439
  }
1175
1440
  )
1176
1441
  );
1442
+ server.tool(
1443
+ "list_emails",
1444
+ "List the emails an endpoint received at its mailhooks.cc address, newest first: subject, sender, tag, and the one-time code and main link found in each.",
1445
+ {
1446
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
1447
+ limit: import_zod2.z.number().int().min(1).max(100).default(25).describe(
1448
+ `Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
1449
+ ),
1450
+ since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms"),
1451
+ tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
1452
+ subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
1453
+ from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)")
1454
+ },
1455
+ withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
1456
+ const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
1457
+ const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
1458
+ const [emails, view] = await Promise.all([
1459
+ client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
1460
+ emailEndpointView(client, endpointSlug)
1461
+ ]);
1462
+ return jsonContent(
1463
+ emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
1464
+ );
1465
+ })
1466
+ );
1467
+ server.tool(
1468
+ "get_email",
1469
+ "Get one captured email: by requestId, or the newest at an endpoint (optionally with a tag). Returns the parsed message with its one-time code and main link.",
1470
+ {
1471
+ requestId: import_zod2.z.string().optional().describe("The email's request ID"),
1472
+ endpointSlug: import_zod2.z.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
1473
+ tag: import_zod2.z.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
1474
+ includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
1475
+ },
1476
+ withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
1477
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1478
+ throw new Error("Pass exactly one of requestId or endpointSlug");
1479
+ }
1480
+ const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
1481
+ if (!email) {
1482
+ throw new import_sdk.NotFoundError(
1483
+ `No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
1484
+ );
1485
+ }
1486
+ const view = await emailEndpointView(
1487
+ client,
1488
+ endpointSlug ?? emailSlugFromAddress(email.path)
1489
+ );
1490
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1491
+ })
1492
+ );
1493
+ server.tool(
1494
+ "wait_for_email",
1495
+ "Wait for an email to arrive at an endpoint (for example after triggering a signup), then return it with its one-time code and main link. Use a tag per run to find your own email.",
1496
+ {
1497
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
1498
+ tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
1499
+ subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
1500
+ from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)"),
1501
+ timeout: durationOrTimestampSchema.default("30s").describe(
1502
+ `How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
1503
+ ),
1504
+ since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
1505
+ includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
1506
+ },
1507
+ withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1508
+ const [email, view] = await Promise.all([
1509
+ client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1510
+ emailEndpointView(client, endpointSlug)
1511
+ ]);
1512
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1513
+ })
1514
+ );
1515
+ server.tool(
1516
+ "send_test_email",
1517
+ "Deliver a sample email (with a 6-digit code and a link) to an endpoint, optionally to a +tag address. It counts as one request and skips SMTP, so sender checks do not run on it.",
1518
+ {
1519
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1520
+ tag: import_zod2.z.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
1521
+ },
1522
+ withErrorHandling(async ({ slug, tag }) => {
1523
+ const result = await client.emails.sendTest(slug, { tag });
1524
+ return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1525
+ })
1526
+ );
1527
+ server.tool(
1528
+ "configure_forwarding",
1529
+ "Set the URL captured emails are forwarded to (as signed JSON) and turn forwarding on or off. Owner only. Saving the first URL creates the signing secret.",
1530
+ {
1531
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1532
+ url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1533
+ enabled: import_zod2.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
1534
+ },
1535
+ withErrorHandling(async ({ slug, url, enabled }) => {
1536
+ const endpoint = await client.forwarding.configure(slug, { url, enabled });
1537
+ return jsonContent({
1538
+ slug: endpoint.slug,
1539
+ forwardEnabled: endpoint.forwardEnabled ?? false,
1540
+ forwardUrl: endpoint.forwardUrl ?? null,
1541
+ hasForwardSecret: endpoint.hasForwardSecret ?? false
1542
+ });
1543
+ })
1544
+ );
1545
+ server.tool(
1546
+ "get_forwarding_secret",
1547
+ "Read (or with rotate: true, replace) the whsec_ secret forwarded emails are signed with. Owner only. Treat the value as a credential: put it in the handler's environment, not in code.",
1548
+ {
1549
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1550
+ rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1551
+ },
1552
+ withErrorHandling(async ({ slug, rotate }) => {
1553
+ const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1554
+ return jsonContent({ secret, rotated: rotate });
1555
+ })
1556
+ );
1557
+ server.tool(
1558
+ "test_forwarding",
1559
+ "Post the endpoint's newest email (or a sample) to its forwarding URL once and report what the server answered. Works with forwarding on or off; nothing is retried or logged.",
1560
+ { slug: import_zod2.z.string().describe("The endpoint slug") },
1561
+ withErrorHandling(async ({ slug }) => {
1562
+ return jsonContent(await client.forwarding.test(slug));
1563
+ })
1564
+ );
1565
+ server.tool(
1566
+ "list_deliveries",
1567
+ "List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
1568
+ {
1569
+ endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's latest deliveries"),
1570
+ requestId: import_zod2.z.string().optional().describe("List every delivery and try of this email"),
1571
+ limit: import_zod2.z.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
1572
+ },
1573
+ withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
1574
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1575
+ throw new Error("Pass exactly one of endpointSlug or requestId");
1576
+ }
1577
+ return jsonContent(
1578
+ requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
1579
+ );
1580
+ })
1581
+ );
1582
+ server.tool(
1583
+ "redeliver_email",
1584
+ "Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
1585
+ { requestId: import_zod2.z.string().describe("The email's request ID") },
1586
+ withErrorHandling(async ({ requestId }) => {
1587
+ return jsonContent(await client.forwarding.redeliver(requestId));
1588
+ })
1589
+ );
1177
1590
  server.tool(
1178
1591
  "describe",
1179
1592
  "Describe all available SDK operations, parameters, and types.",
@@ -1198,7 +1611,7 @@ function registerAgentRegistrationTools(server) {
1198
1611
  return jsonContent({
1199
1612
  ...import_sdk.WebhooksCC.describeRegistration(baseUrl),
1200
1613
  next_steps: [
1201
- "Easiest: call register_agent (anonymous) to get a key immediately, then have a human run claim_agent with the returned userCode while logged in to webhooks.cc.",
1614
+ "Easiest: call register_agent (anonymous) to get a key immediately, then ask a human to open the returned claimUrl while signed in to webhooks.cc and enter the userCode. Poll check_claim until it says claimed.",
1202
1615
  "Or set WHK_API_KEY to an existing whcc_ key and use the authenticated tools."
1203
1616
  ]
1204
1617
  });
@@ -1206,7 +1619,7 @@ function registerAgentRegistrationTools(server) {
1206
1619
  );
1207
1620
  server.tool(
1208
1621
  "register_agent",
1209
- "Self-register for a webhooks.cc API credential via the anonymous auth.md flow. Returns a whcc_ API key that works immediately in a bounded sandbox, plus a short userCode and claimUrl a human uses to bind the key to their account. No authentication required. After registering, set WHK_API_KEY to the returned credential.",
1622
+ "Self-register for a webhooks.cc API credential via the anonymous auth.md flow. Returns a whcc_ API key, plus a short userCode and claimUrl a human uses to bind the key to their account. Until a human claims it, the key only works with the sandbox API (/api/agent/sandbox/endpoints), not with these tools, and it is deleted about 15 minutes after registration. No authentication required.",
1210
1623
  {
1211
1624
  clientName: import_zod2.z.string().optional().describe("A human-readable name for this agent, recorded on the issued key")
1212
1625
  },
@@ -1222,7 +1635,7 @@ function registerAgentRegistrationTools(server) {
1222
1635
  expiresAt: reg.claimTokenExpires,
1223
1636
  instructions: `Ask a human to open ${reg.claimUrl} while logged in to webhooks.cc and either enter the code ${reg.userCode} or open ${reg.claimUrl}?token=${reg.claimToken}. Then call check_claim with the claimToken.`
1224
1637
  },
1225
- usage: "Set WHK_API_KEY to `credential` to use the authenticated tools. Before it is claimed the key can create ephemeral endpoints and read its own captured requests in the sandbox."
1638
+ usage: "Until a human claims it, the key only works with the sandbox API at /api/agent/sandbox/endpoints (create temporary endpoints, read their requests), and it is deleted about 15 minutes after registration. Once check_claim says claimed, set WHK_API_KEY to `credential` and restart the MCP server to use the authenticated tools."
1226
1639
  });
1227
1640
  })
1228
1641
  );
@@ -1288,7 +1701,7 @@ function registerAgentRegistrationTools(server) {
1288
1701
  }
1289
1702
 
1290
1703
  // src/index.ts
1291
- var VERSION = true ? "1.9.0" : "0.0.0-dev";
1704
+ var VERSION = true ? "1.10.1" : "0.0.0-dev";
1292
1705
  function createServer(options = {}) {
1293
1706
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
1294
1707
  const server = new import_mcp2.McpServer({