@webhooks-cc/mcp 1.9.0 → 1.10.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
@@ -142,6 +142,149 @@ function registerPrompts(server) {
142
142
 
143
143
  // src/resources.ts
144
144
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
145
+
146
+ // src/compact.ts
147
+ var MAX_OUTPUT = 32768;
148
+ var MAX_EMAIL_TEXT = 8e3;
149
+ var MAX_EXTRACTS = 10;
150
+ var MAX_LINK_TEXT = 200;
151
+ var MAX_LINK_URL = 2e3;
152
+ var MAX_LIST_ITEMS = 5;
153
+ var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
154
+ var TEXT_FLOOR = 2e3;
155
+ function jsonSize(value) {
156
+ return JSON.stringify(value, null, 2).length;
157
+ }
158
+ function sliceText(text, end) {
159
+ if (end <= 0) return "";
160
+ if (end >= text.length) return text;
161
+ const last = text.charCodeAt(end - 1);
162
+ return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
163
+ }
164
+ function cutEmailText(text) {
165
+ if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
166
+ return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
167
+ }
168
+ function shrinkToFit(root, budget, steps) {
169
+ for (const step of steps) {
170
+ if (jsonSize(root) <= budget) return;
171
+ step();
172
+ }
173
+ }
174
+ function omitHeaders(holder) {
175
+ const headers = holder.headers;
176
+ if (headers === null || typeof headers !== "object") return;
177
+ const count = Object.keys(headers).length;
178
+ if (count === 0) return;
179
+ holder.headers = {};
180
+ holder.headersOmitted = `${count} headers left out to fit the output`;
181
+ }
182
+ function capExtracts(holder) {
183
+ if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
184
+ holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
185
+ holder.codesTruncated = true;
186
+ }
187
+ if (Array.isArray(holder.links)) {
188
+ if (holder.links.length > MAX_EXTRACTS) {
189
+ holder.links = holder.links.slice(0, MAX_EXTRACTS);
190
+ holder.linksTruncated = true;
191
+ }
192
+ holder.links = holder.links.map((link) => ({
193
+ ...link,
194
+ ...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
195
+ ...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
196
+ }));
197
+ }
198
+ }
199
+ function capLists(holder) {
200
+ for (const key of EMAIL_LISTS) {
201
+ const list = holder[key];
202
+ if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
203
+ holder[key] = list.slice(0, MAX_LIST_ITEMS);
204
+ holder[`${key}Total`] = list.length;
205
+ }
206
+ }
207
+ }
208
+ function keepOnly(holder, keep) {
209
+ for (const key of Object.keys(holder)) {
210
+ if (!keep.includes(key)) delete holder[key];
211
+ }
212
+ holder.trimmed = "The email was too big for the output, so only these fields are shown.";
213
+ }
214
+ function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
215
+ const value = holder[key];
216
+ if (typeof value !== "string" || value.length <= floor) return;
217
+ const flagged = holder[flag] === true;
218
+ holder[key] = "";
219
+ holder[flag] = true;
220
+ const room = budget - jsonSize(root) + 2;
221
+ let low = 0;
222
+ let high = value.length;
223
+ while (low < high) {
224
+ const mid = Math.ceil((low + high) / 2);
225
+ if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
226
+ else high = mid - 1;
227
+ }
228
+ const kept = sliceText(value, Math.max(low, floor));
229
+ holder[key] = kept;
230
+ if (kept.length === value.length && !flagged) delete holder[flag];
231
+ }
232
+ function keepFirst(holder, key) {
233
+ const list = holder[key];
234
+ if (!Array.isArray(list) || list.length <= 1) return;
235
+ holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
236
+ holder[key] = list.slice(0, 1);
237
+ }
238
+ var ESSENTIAL_EMAIL_FIELDS = [
239
+ "subject",
240
+ "from",
241
+ "fromTotal",
242
+ "tag",
243
+ "date",
244
+ "text",
245
+ "textTruncated",
246
+ "htmlSize"
247
+ ];
248
+ function compactRequest(request) {
249
+ if (request.kind !== "email") return request;
250
+ const compact = { ...request };
251
+ delete compact.body;
252
+ delete compact.bodyRaw;
253
+ compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
254
+ if (!request.email) return compact;
255
+ const { html, ...rest } = request.email;
256
+ const { text, cut } = cutEmailText(rest.text);
257
+ const restoreText = () => {
258
+ email.text = text;
259
+ if (cut) email.textTruncated = true;
260
+ else delete email.textTruncated;
261
+ };
262
+ const email = {
263
+ ...rest,
264
+ text,
265
+ ...cut ? { textTruncated: true } : {},
266
+ htmlSize: html?.length ?? 0
267
+ };
268
+ compact.email = email;
269
+ const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
270
+ const budget = MAX_OUTPUT - 128;
271
+ shrinkToFit(root, budget, [
272
+ () => omitHeaders(compact),
273
+ () => capLists(email),
274
+ () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
275
+ // Down to the essentials there may be room for more of the text again.
276
+ () => {
277
+ keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
278
+ keepFirst(email, "from");
279
+ restoreText();
280
+ cutStringToFit(root, email, "text", "textTruncated", budget);
281
+ },
282
+ () => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
283
+ ]);
284
+ return compact;
285
+ }
286
+
287
+ // src/resources.ts
145
288
  var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
146
289
  var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
147
290
  var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
@@ -239,7 +382,7 @@ function registerResources(server, client) {
239
382
  ]);
240
383
  return jsonResource(uri.toString(), {
241
384
  endpoint,
242
- requests
385
+ requests: requests.map(compactRequest)
243
386
  });
244
387
  }
245
388
  );
@@ -257,7 +400,7 @@ function registerResources(server, client) {
257
400
  async (uri, variables) => {
258
401
  const id = variableToString(variables.id, "id");
259
402
  const request = await client.requests.get(id);
260
- return jsonResource(uri.toString(), request);
403
+ return jsonResource(uri.toString(), compactRequest(request));
261
404
  }
262
405
  );
263
406
  }
@@ -265,7 +408,7 @@ function registerResources(server, client) {
265
408
  // src/tools.ts
266
409
  var import_zod2 = require("zod");
267
410
  var import_sdk = require("@webhooks-cc/sdk");
268
- var MAX_BODY_SIZE = 32768;
411
+ var MAX_BODY_SIZE = MAX_OUTPUT;
269
412
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
270
413
  var TIME_SEPARATOR = " \u2014 ";
271
414
  var httpUrlSchema = import_zod2.z.string().url().refine(
@@ -280,6 +423,8 @@ var httpUrlSchema = import_zod2.z.string().url().refine(
280
423
  { message: "Only http and https URLs are supported" }
281
424
  );
282
425
  var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
426
+ var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
427
+ var EMAIL_SCAN_LIMIT = 100;
283
428
  var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
284
429
  var ruleConditionSchema = import_zod2.z.object({
285
430
  field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
@@ -359,6 +504,24 @@ function serializeJson(value, limit = MAX_BODY_SIZE) {
359
504
  return full.slice(0, limit) + `
360
505
  ... [truncated, ${full.length} chars total]`;
361
506
  }
507
+ function fitArrayField(value, key) {
508
+ const items = value[key];
509
+ if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
510
+ const cut = (count) => ({
511
+ ...value,
512
+ [key]: items.slice(0, count),
513
+ truncated: true,
514
+ returned: count
515
+ });
516
+ let low = 0;
517
+ let high = items.length;
518
+ while (low < high) {
519
+ const mid = Math.ceil((low + high) / 2);
520
+ if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
521
+ else high = mid - 1;
522
+ }
523
+ return cut(low);
524
+ }
362
525
  function jsonContent(value) {
363
526
  return textContent(serializeJson(value));
364
527
  }
@@ -507,6 +670,99 @@ async function summarizeResponse(response) {
507
670
  body: await readBodyTruncated(response)
508
671
  };
509
672
  }
673
+ async function emailEndpointView(client, slug) {
674
+ const endpoint = await client.endpoints.get(slug);
675
+ return {
676
+ endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
677
+ includeExtracts: endpoint.showEmailExtracts !== false
678
+ };
679
+ }
680
+ function emailSlugFromAddress(address) {
681
+ const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
682
+ return local.split("+")[0].toLowerCase();
683
+ }
684
+ var MAX_SUMMARY_LINK = 8e3;
685
+ function boundedLink(link, max) {
686
+ if (link == null || link.length <= max) return { link: link ?? null };
687
+ return {
688
+ link: null,
689
+ linkOmitted: `The main link is ${link.length} characters long, too long for the output`
690
+ };
691
+ }
692
+ var MAX_SUMMARY_FIELD = 1e3;
693
+ function summarizeEmail(email, includeExtracts) {
694
+ const from = email.email.from[0];
695
+ const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
696
+ const subject = email.email.subject;
697
+ return {
698
+ id: email.id,
699
+ receivedAt: new Date(email.receivedAt).toISOString(),
700
+ address: email.path,
701
+ tag: email.email.tag,
702
+ subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
703
+ from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
704
+ ...includeExtracts ? { code: (0, import_sdk.extractCode)(email), ...boundedLink((0, import_sdk.extractLink)(email), MAX_SUMMARY_LINK) } : {},
705
+ attachments: email.email.attachments.length
706
+ };
707
+ }
708
+ var ESSENTIAL_DETAIL_FIELDS = [
709
+ "code",
710
+ "link",
711
+ "id",
712
+ "endpoint",
713
+ "receivedAt",
714
+ "address",
715
+ "tag",
716
+ "subject",
717
+ "from",
718
+ "size",
719
+ "test",
720
+ "text",
721
+ "textTruncated",
722
+ "htmlSize",
723
+ "html",
724
+ "htmlTruncated"
725
+ ];
726
+ function emailDetail(client, email, view, includeHtml) {
727
+ const { data } = client.emails.toJson(email, {
728
+ endpoint: view.endpoint,
729
+ includeExtracts: view.includeExtracts
730
+ });
731
+ const { html, text: fullText, ...rest } = data;
732
+ const { text, cut } = cutEmailText(fullText ?? null);
733
+ const restoreText = () => {
734
+ detail.text = text;
735
+ if (cut) detail.textTruncated = true;
736
+ else delete detail.textTruncated;
737
+ };
738
+ const detail = {
739
+ ...view.includeExtracts ? { code: (0, import_sdk.extractCode)(email), link: (0, import_sdk.extractLink)(email) } : {},
740
+ ...rest,
741
+ text,
742
+ ...cut ? { textTruncated: true } : {},
743
+ htmlSize: html?.length ?? 0,
744
+ ...includeHtml && html !== null ? { html } : {}
745
+ };
746
+ shrinkToFit(detail, MAX_BODY_SIZE, [
747
+ () => omitHeaders(detail),
748
+ () => capExtracts(detail),
749
+ () => capLists(detail),
750
+ () => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
751
+ () => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
752
+ // Down to the essentials (and without a main link too long to use) there
753
+ // may be room for more of the text again.
754
+ () => {
755
+ keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
756
+ if (view.includeExtracts) {
757
+ Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
758
+ }
759
+ restoreText();
760
+ cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
761
+ },
762
+ () => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
763
+ ]);
764
+ return detail;
765
+ }
510
766
  function registerTools(server, client) {
511
767
  server.tool(
512
768
  "create_endpoint",
@@ -650,7 +906,7 @@ function registerTools(server, client) {
650
906
  );
651
907
  server.tool(
652
908
  "update_endpoint",
653
- "Update an endpoint name, mock response, conditional response rules, or signing configuration.",
909
+ "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
654
910
  {
655
911
  slug: import_zod2.z.string().describe("The endpoint slug to update"),
656
912
  name: import_zod2.z.string().optional().describe("New display name"),
@@ -663,7 +919,8 @@ function registerTools(server, client) {
663
919
  signingSecret: import_zod2.z.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
664
920
  signingHeader: import_zod2.z.string().nullable().optional().describe(
665
921
  "Custom signature header name (only for generic-hmac provider), or null to clear"
666
- )
922
+ ),
923
+ showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
667
924
  },
668
925
  withErrorHandling(
669
926
  async ({
@@ -674,7 +931,8 @@ function registerTools(server, client) {
674
931
  notificationUrl,
675
932
  signingProvider,
676
933
  signingSecret,
677
- signingHeader
934
+ signingHeader,
935
+ showEmailExtracts
678
936
  }) => {
679
937
  const endpoint = await client.endpoints.update(slug, {
680
938
  name,
@@ -683,7 +941,8 @@ function registerTools(server, client) {
683
941
  notificationUrl,
684
942
  signingProvider,
685
943
  signingSecret,
686
- signingHeader
944
+ signingHeader,
945
+ showEmailExtracts
687
946
  });
688
947
  return jsonContent(endpoint);
689
948
  }
@@ -795,15 +1054,16 @@ function registerTools(server, client) {
795
1054
  );
796
1055
  server.tool(
797
1056
  "list_requests",
798
- "List recent captured requests for an endpoint.",
1057
+ "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
799
1058
  {
800
1059
  endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
801
1060
  limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
802
- since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms")
1061
+ since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms"),
1062
+ kind: kindSchema
803
1063
  },
804
- withErrorHandling(async ({ endpointSlug, limit, since }) => {
805
- const requests = await client.requests.list(endpointSlug, { limit, since });
806
- return jsonContent(requests);
1064
+ withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1065
+ const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1066
+ return jsonContent(requests.map(compactRequest));
807
1067
  })
808
1068
  );
809
1069
  server.tool(
@@ -812,6 +1072,7 @@ function registerTools(server, client) {
812
1072
  {
813
1073
  slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
814
1074
  method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
1075
+ kind: kindSchema,
815
1076
  q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
816
1077
  from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
817
1078
  to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
@@ -819,10 +1080,11 @@ function registerTools(server, client) {
819
1080
  offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("Result offset"),
820
1081
  order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
821
1082
  },
822
- withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
1083
+ withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
823
1084
  const results = await client.requests.search({
824
1085
  slug,
825
1086
  method,
1087
+ kind,
826
1088
  q,
827
1089
  from,
828
1090
  to,
@@ -830,7 +1092,7 @@ function registerTools(server, client) {
830
1092
  offset,
831
1093
  order
832
1094
  });
833
- return jsonContent(results);
1095
+ return jsonContent(results.map(compactRequest));
834
1096
  })
835
1097
  );
836
1098
  server.tool(
@@ -839,12 +1101,13 @@ function registerTools(server, client) {
839
1101
  {
840
1102
  slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
841
1103
  method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
1104
+ kind: kindSchema,
842
1105
  q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
843
1106
  from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
844
1107
  to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
845
1108
  },
846
- withErrorHandling(async ({ slug, method, q, from, to }) => {
847
- const count = await client.requests.count({ slug, method, q, from, to });
1109
+ withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
1110
+ const count = await client.requests.count({ slug, method, kind, q, from, to });
848
1111
  return jsonContent({ count });
849
1112
  })
850
1113
  );
@@ -854,7 +1117,7 @@ function registerTools(server, client) {
854
1117
  { requestId: import_zod2.z.string().describe("The request ID") },
855
1118
  withErrorHandling(async ({ requestId }) => {
856
1119
  const request = await client.requests.get(requestId);
857
- return jsonContent(request);
1120
+ return jsonContent(compactRequest(request));
858
1121
  })
859
1122
  );
860
1123
  server.tool(
@@ -867,7 +1130,7 @@ function registerTools(server, client) {
867
1130
  },
868
1131
  withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
869
1132
  const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
870
- return jsonContent(request);
1133
+ return jsonContent(compactRequest(request));
871
1134
  })
872
1135
  );
873
1136
  server.tool(
@@ -887,7 +1150,9 @@ function registerTools(server, client) {
887
1150
  pollInterval,
888
1151
  method
889
1152
  });
890
- return jsonContent(result);
1153
+ return jsonContent(
1154
+ fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
1155
+ );
891
1156
  })
892
1157
  );
893
1158
  server.tool(
@@ -1196,6 +1461,154 @@ function registerTools(server, client) {
1196
1461
  }
1197
1462
  )
1198
1463
  );
1464
+ server.tool(
1465
+ "list_emails",
1466
+ "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.",
1467
+ {
1468
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
1469
+ limit: import_zod2.z.number().int().min(1).max(100).default(25).describe(
1470
+ `Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
1471
+ ),
1472
+ since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms"),
1473
+ tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
1474
+ subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
1475
+ from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)")
1476
+ },
1477
+ withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
1478
+ const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
1479
+ const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
1480
+ const [emails, view] = await Promise.all([
1481
+ client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
1482
+ emailEndpointView(client, endpointSlug)
1483
+ ]);
1484
+ return jsonContent(
1485
+ emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
1486
+ );
1487
+ })
1488
+ );
1489
+ server.tool(
1490
+ "get_email",
1491
+ "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.",
1492
+ {
1493
+ requestId: import_zod2.z.string().optional().describe("The email's request ID"),
1494
+ endpointSlug: import_zod2.z.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
1495
+ tag: import_zod2.z.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
1496
+ includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
1497
+ },
1498
+ withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
1499
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1500
+ throw new Error("Pass exactly one of requestId or endpointSlug");
1501
+ }
1502
+ const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
1503
+ if (!email) {
1504
+ throw new import_sdk.NotFoundError(
1505
+ `No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
1506
+ );
1507
+ }
1508
+ const view = await emailEndpointView(
1509
+ client,
1510
+ endpointSlug ?? emailSlugFromAddress(email.path)
1511
+ );
1512
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1513
+ })
1514
+ );
1515
+ server.tool(
1516
+ "wait_for_email",
1517
+ "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.",
1518
+ {
1519
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
1520
+ tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
1521
+ subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
1522
+ from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)"),
1523
+ timeout: durationOrTimestampSchema.default("30s").describe(
1524
+ `How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
1525
+ ),
1526
+ since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
1527
+ includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
1528
+ },
1529
+ withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1530
+ const [email, view] = await Promise.all([
1531
+ client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1532
+ emailEndpointView(client, endpointSlug)
1533
+ ]);
1534
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1535
+ })
1536
+ );
1537
+ server.tool(
1538
+ "send_test_email",
1539
+ "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.",
1540
+ {
1541
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1542
+ tag: import_zod2.z.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
1543
+ },
1544
+ withErrorHandling(async ({ slug, tag }) => {
1545
+ const result = await client.emails.sendTest(slug, { tag });
1546
+ return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1547
+ })
1548
+ );
1549
+ server.tool(
1550
+ "configure_forwarding",
1551
+ "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.",
1552
+ {
1553
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1554
+ url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1555
+ enabled: import_zod2.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
1556
+ },
1557
+ withErrorHandling(async ({ slug, url, enabled }) => {
1558
+ const endpoint = await client.forwarding.configure(slug, { url, enabled });
1559
+ return jsonContent({
1560
+ slug: endpoint.slug,
1561
+ forwardEnabled: endpoint.forwardEnabled ?? false,
1562
+ forwardUrl: endpoint.forwardUrl ?? null,
1563
+ hasForwardSecret: endpoint.hasForwardSecret ?? false
1564
+ });
1565
+ })
1566
+ );
1567
+ server.tool(
1568
+ "get_forwarding_secret",
1569
+ "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.",
1570
+ {
1571
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1572
+ rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1573
+ },
1574
+ withErrorHandling(async ({ slug, rotate }) => {
1575
+ const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1576
+ return jsonContent({ secret, rotated: rotate });
1577
+ })
1578
+ );
1579
+ server.tool(
1580
+ "test_forwarding",
1581
+ "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.",
1582
+ { slug: import_zod2.z.string().describe("The endpoint slug") },
1583
+ withErrorHandling(async ({ slug }) => {
1584
+ return jsonContent(await client.forwarding.test(slug));
1585
+ })
1586
+ );
1587
+ server.tool(
1588
+ "list_deliveries",
1589
+ "List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
1590
+ {
1591
+ endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's latest deliveries"),
1592
+ requestId: import_zod2.z.string().optional().describe("List every delivery and try of this email"),
1593
+ limit: import_zod2.z.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
1594
+ },
1595
+ withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
1596
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1597
+ throw new Error("Pass exactly one of endpointSlug or requestId");
1598
+ }
1599
+ return jsonContent(
1600
+ requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
1601
+ );
1602
+ })
1603
+ );
1604
+ server.tool(
1605
+ "redeliver_email",
1606
+ "Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
1607
+ { requestId: import_zod2.z.string().describe("The email's request ID") },
1608
+ withErrorHandling(async ({ requestId }) => {
1609
+ return jsonContent(await client.forwarding.redeliver(requestId));
1610
+ })
1611
+ );
1199
1612
  server.tool(
1200
1613
  "describe",
1201
1614
  "Describe all available SDK operations, parameters, and types.",
@@ -1310,7 +1723,7 @@ function registerAgentRegistrationTools(server) {
1310
1723
  }
1311
1724
 
1312
1725
  // src/index.ts
1313
- var VERSION = true ? "1.9.0" : "0.0.0-dev";
1726
+ var VERSION = true ? "1.10.0" : "0.0.0-dev";
1314
1727
  function createServer(options = {}) {
1315
1728
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
1316
1729
  const server = new import_mcp2.McpServer({