@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.mjs CHANGED
@@ -114,6 +114,149 @@ function registerPrompts(server) {
114
114
 
115
115
  // src/resources.ts
116
116
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
117
+
118
+ // src/compact.ts
119
+ var MAX_OUTPUT = 32768;
120
+ var MAX_EMAIL_TEXT = 8e3;
121
+ var MAX_EXTRACTS = 10;
122
+ var MAX_LINK_TEXT = 200;
123
+ var MAX_LINK_URL = 2e3;
124
+ var MAX_LIST_ITEMS = 5;
125
+ var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
126
+ var TEXT_FLOOR = 2e3;
127
+ function jsonSize(value) {
128
+ return JSON.stringify(value, null, 2).length;
129
+ }
130
+ function sliceText(text, end) {
131
+ if (end <= 0) return "";
132
+ if (end >= text.length) return text;
133
+ const last = text.charCodeAt(end - 1);
134
+ return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
135
+ }
136
+ function cutEmailText(text) {
137
+ if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
138
+ return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
139
+ }
140
+ function shrinkToFit(root, budget, steps) {
141
+ for (const step of steps) {
142
+ if (jsonSize(root) <= budget) return;
143
+ step();
144
+ }
145
+ }
146
+ function omitHeaders(holder) {
147
+ const headers = holder.headers;
148
+ if (headers === null || typeof headers !== "object") return;
149
+ const count = Object.keys(headers).length;
150
+ if (count === 0) return;
151
+ holder.headers = {};
152
+ holder.headersOmitted = `${count} headers left out to fit the output`;
153
+ }
154
+ function capExtracts(holder) {
155
+ if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
156
+ holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
157
+ holder.codesTruncated = true;
158
+ }
159
+ if (Array.isArray(holder.links)) {
160
+ if (holder.links.length > MAX_EXTRACTS) {
161
+ holder.links = holder.links.slice(0, MAX_EXTRACTS);
162
+ holder.linksTruncated = true;
163
+ }
164
+ holder.links = holder.links.map((link) => ({
165
+ ...link,
166
+ ...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
167
+ ...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
168
+ }));
169
+ }
170
+ }
171
+ function capLists(holder) {
172
+ for (const key of EMAIL_LISTS) {
173
+ const list = holder[key];
174
+ if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
175
+ holder[key] = list.slice(0, MAX_LIST_ITEMS);
176
+ holder[`${key}Total`] = list.length;
177
+ }
178
+ }
179
+ }
180
+ function keepOnly(holder, keep) {
181
+ for (const key of Object.keys(holder)) {
182
+ if (!keep.includes(key)) delete holder[key];
183
+ }
184
+ holder.trimmed = "The email was too big for the output, so only these fields are shown.";
185
+ }
186
+ function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
187
+ const value = holder[key];
188
+ if (typeof value !== "string" || value.length <= floor) return;
189
+ const flagged = holder[flag] === true;
190
+ holder[key] = "";
191
+ holder[flag] = true;
192
+ const room = budget - jsonSize(root) + 2;
193
+ let low = 0;
194
+ let high = value.length;
195
+ while (low < high) {
196
+ const mid = Math.ceil((low + high) / 2);
197
+ if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
198
+ else high = mid - 1;
199
+ }
200
+ const kept = sliceText(value, Math.max(low, floor));
201
+ holder[key] = kept;
202
+ if (kept.length === value.length && !flagged) delete holder[flag];
203
+ }
204
+ function keepFirst(holder, key) {
205
+ const list = holder[key];
206
+ if (!Array.isArray(list) || list.length <= 1) return;
207
+ holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
208
+ holder[key] = list.slice(0, 1);
209
+ }
210
+ var ESSENTIAL_EMAIL_FIELDS = [
211
+ "subject",
212
+ "from",
213
+ "fromTotal",
214
+ "tag",
215
+ "date",
216
+ "text",
217
+ "textTruncated",
218
+ "htmlSize"
219
+ ];
220
+ function compactRequest(request) {
221
+ if (request.kind !== "email") return request;
222
+ const compact = { ...request };
223
+ delete compact.body;
224
+ delete compact.bodyRaw;
225
+ compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
226
+ if (!request.email) return compact;
227
+ const { html, ...rest } = request.email;
228
+ const { text, cut } = cutEmailText(rest.text);
229
+ const restoreText = () => {
230
+ email.text = text;
231
+ if (cut) email.textTruncated = true;
232
+ else delete email.textTruncated;
233
+ };
234
+ const email = {
235
+ ...rest,
236
+ text,
237
+ ...cut ? { textTruncated: true } : {},
238
+ htmlSize: html?.length ?? 0
239
+ };
240
+ compact.email = email;
241
+ const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
242
+ const budget = MAX_OUTPUT - 128;
243
+ shrinkToFit(root, budget, [
244
+ () => omitHeaders(compact),
245
+ () => capLists(email),
246
+ () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
247
+ // Down to the essentials there may be room for more of the text again.
248
+ () => {
249
+ keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
250
+ keepFirst(email, "from");
251
+ restoreText();
252
+ cutStringToFit(root, email, "text", "textTruncated", budget);
253
+ },
254
+ () => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
255
+ ]);
256
+ return compact;
257
+ }
258
+
259
+ // src/resources.ts
117
260
  var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
118
261
  var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
119
262
  var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
@@ -211,7 +354,7 @@ function registerResources(server, client) {
211
354
  ]);
212
355
  return jsonResource(uri.toString(), {
213
356
  endpoint,
214
- requests
357
+ requests: requests.map(compactRequest)
215
358
  });
216
359
  }
217
360
  );
@@ -229,7 +372,7 @@ function registerResources(server, client) {
229
372
  async (uri, variables) => {
230
373
  const id = variableToString(variables.id, "id");
231
374
  const request = await client.requests.get(id);
232
- return jsonResource(uri.toString(), request);
375
+ return jsonResource(uri.toString(), compactRequest(request));
233
376
  }
234
377
  );
235
378
  }
@@ -257,9 +400,11 @@ import {
257
400
  VERIFY_PROVIDERS,
258
401
  verifySignature,
259
402
  WebhooksCC,
260
- WebhooksCCError
403
+ WebhooksCCError,
404
+ extractCode,
405
+ extractLink
261
406
  } from "@webhooks-cc/sdk";
262
- var MAX_BODY_SIZE = 32768;
407
+ var MAX_BODY_SIZE = MAX_OUTPUT;
263
408
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
264
409
  var TIME_SEPARATOR = " \u2014 ";
265
410
  var httpUrlSchema = z2.string().url().refine(
@@ -274,6 +419,8 @@ var httpUrlSchema = z2.string().url().refine(
274
419
  { message: "Only http and https URLs are supported" }
275
420
  );
276
421
  var methodSchema = z2.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
422
+ var kindSchema = z2.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
423
+ var EMAIL_SCAN_LIMIT = 100;
277
424
  var durationOrTimestampSchema = z2.union([z2.string(), z2.number()]);
278
425
  var ruleConditionSchema = z2.object({
279
426
  field: z2.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
@@ -353,6 +500,24 @@ function serializeJson(value, limit = MAX_BODY_SIZE) {
353
500
  return full.slice(0, limit) + `
354
501
  ... [truncated, ${full.length} chars total]`;
355
502
  }
503
+ function fitArrayField(value, key) {
504
+ const items = value[key];
505
+ if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
506
+ const cut = (count) => ({
507
+ ...value,
508
+ [key]: items.slice(0, count),
509
+ truncated: true,
510
+ returned: count
511
+ });
512
+ let low = 0;
513
+ let high = items.length;
514
+ while (low < high) {
515
+ const mid = Math.ceil((low + high) / 2);
516
+ if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
517
+ else high = mid - 1;
518
+ }
519
+ return cut(low);
520
+ }
356
521
  function jsonContent(value) {
357
522
  return textContent(serializeJson(value));
358
523
  }
@@ -501,6 +666,99 @@ async function summarizeResponse(response) {
501
666
  body: await readBodyTruncated(response)
502
667
  };
503
668
  }
669
+ async function emailEndpointView(client, slug) {
670
+ const endpoint = await client.endpoints.get(slug);
671
+ return {
672
+ endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
673
+ includeExtracts: endpoint.showEmailExtracts !== false
674
+ };
675
+ }
676
+ function emailSlugFromAddress(address) {
677
+ const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
678
+ return local.split("+")[0].toLowerCase();
679
+ }
680
+ var MAX_SUMMARY_LINK = 8e3;
681
+ function boundedLink(link, max) {
682
+ if (link == null || link.length <= max) return { link: link ?? null };
683
+ return {
684
+ link: null,
685
+ linkOmitted: `The main link is ${link.length} characters long, too long for the output`
686
+ };
687
+ }
688
+ var MAX_SUMMARY_FIELD = 1e3;
689
+ function summarizeEmail(email, includeExtracts) {
690
+ const from = email.email.from[0];
691
+ const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
692
+ const subject = email.email.subject;
693
+ return {
694
+ id: email.id,
695
+ receivedAt: new Date(email.receivedAt).toISOString(),
696
+ address: email.path,
697
+ tag: email.email.tag,
698
+ subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
699
+ from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
700
+ ...includeExtracts ? { code: extractCode(email), ...boundedLink(extractLink(email), MAX_SUMMARY_LINK) } : {},
701
+ attachments: email.email.attachments.length
702
+ };
703
+ }
704
+ var ESSENTIAL_DETAIL_FIELDS = [
705
+ "code",
706
+ "link",
707
+ "id",
708
+ "endpoint",
709
+ "receivedAt",
710
+ "address",
711
+ "tag",
712
+ "subject",
713
+ "from",
714
+ "size",
715
+ "test",
716
+ "text",
717
+ "textTruncated",
718
+ "htmlSize",
719
+ "html",
720
+ "htmlTruncated"
721
+ ];
722
+ function emailDetail(client, email, view, includeHtml) {
723
+ const { data } = client.emails.toJson(email, {
724
+ endpoint: view.endpoint,
725
+ includeExtracts: view.includeExtracts
726
+ });
727
+ const { html, text: fullText, ...rest } = data;
728
+ const { text, cut } = cutEmailText(fullText ?? null);
729
+ const restoreText = () => {
730
+ detail.text = text;
731
+ if (cut) detail.textTruncated = true;
732
+ else delete detail.textTruncated;
733
+ };
734
+ const detail = {
735
+ ...view.includeExtracts ? { code: extractCode(email), link: extractLink(email) } : {},
736
+ ...rest,
737
+ text,
738
+ ...cut ? { textTruncated: true } : {},
739
+ htmlSize: html?.length ?? 0,
740
+ ...includeHtml && html !== null ? { html } : {}
741
+ };
742
+ shrinkToFit(detail, MAX_BODY_SIZE, [
743
+ () => omitHeaders(detail),
744
+ () => capExtracts(detail),
745
+ () => capLists(detail),
746
+ () => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
747
+ () => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
748
+ // Down to the essentials (and without a main link too long to use) there
749
+ // may be room for more of the text again.
750
+ () => {
751
+ keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
752
+ if (view.includeExtracts) {
753
+ Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
754
+ }
755
+ restoreText();
756
+ cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
757
+ },
758
+ () => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
759
+ ]);
760
+ return detail;
761
+ }
504
762
  function registerTools(server, client) {
505
763
  server.tool(
506
764
  "create_endpoint",
@@ -644,7 +902,7 @@ function registerTools(server, client) {
644
902
  );
645
903
  server.tool(
646
904
  "update_endpoint",
647
- "Update an endpoint name, mock response, conditional response rules, or signing configuration.",
905
+ "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
648
906
  {
649
907
  slug: z2.string().describe("The endpoint slug to update"),
650
908
  name: z2.string().optional().describe("New display name"),
@@ -657,7 +915,8 @@ function registerTools(server, client) {
657
915
  signingSecret: z2.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
658
916
  signingHeader: z2.string().nullable().optional().describe(
659
917
  "Custom signature header name (only for generic-hmac provider), or null to clear"
660
- )
918
+ ),
919
+ showEmailExtracts: z2.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
661
920
  },
662
921
  withErrorHandling(
663
922
  async ({
@@ -668,7 +927,8 @@ function registerTools(server, client) {
668
927
  notificationUrl,
669
928
  signingProvider,
670
929
  signingSecret,
671
- signingHeader
930
+ signingHeader,
931
+ showEmailExtracts
672
932
  }) => {
673
933
  const endpoint = await client.endpoints.update(slug, {
674
934
  name,
@@ -677,7 +937,8 @@ function registerTools(server, client) {
677
937
  notificationUrl,
678
938
  signingProvider,
679
939
  signingSecret,
680
- signingHeader
940
+ signingHeader,
941
+ showEmailExtracts
681
942
  });
682
943
  return jsonContent(endpoint);
683
944
  }
@@ -789,15 +1050,16 @@ function registerTools(server, client) {
789
1050
  );
790
1051
  server.tool(
791
1052
  "list_requests",
792
- "List recent captured requests for an endpoint.",
1053
+ "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
793
1054
  {
794
1055
  endpointSlug: z2.string().describe("The endpoint slug"),
795
1056
  limit: z2.number().int().min(1).max(100).default(25).describe("Max requests to return"),
796
- since: z2.number().optional().describe("Only return requests after this timestamp in ms")
1057
+ since: z2.number().optional().describe("Only return requests after this timestamp in ms"),
1058
+ kind: kindSchema
797
1059
  },
798
- withErrorHandling(async ({ endpointSlug, limit, since }) => {
799
- const requests = await client.requests.list(endpointSlug, { limit, since });
800
- return jsonContent(requests);
1060
+ withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1061
+ const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1062
+ return jsonContent(requests.map(compactRequest));
801
1063
  })
802
1064
  );
803
1065
  server.tool(
@@ -806,6 +1068,7 @@ function registerTools(server, client) {
806
1068
  {
807
1069
  slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
808
1070
  method: z2.string().optional().describe("Filter by HTTP method"),
1071
+ kind: kindSchema,
809
1072
  q: z2.string().optional().describe("Free-text search across path, body, and headers"),
810
1073
  from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
811
1074
  to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
@@ -813,10 +1076,11 @@ function registerTools(server, client) {
813
1076
  offset: z2.number().int().min(0).max(1e4).default(0).describe("Result offset"),
814
1077
  order: z2.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
815
1078
  },
816
- withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
1079
+ withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
817
1080
  const results = await client.requests.search({
818
1081
  slug,
819
1082
  method,
1083
+ kind,
820
1084
  q,
821
1085
  from,
822
1086
  to,
@@ -824,7 +1088,7 @@ function registerTools(server, client) {
824
1088
  offset,
825
1089
  order
826
1090
  });
827
- return jsonContent(results);
1091
+ return jsonContent(results.map(compactRequest));
828
1092
  })
829
1093
  );
830
1094
  server.tool(
@@ -833,12 +1097,13 @@ function registerTools(server, client) {
833
1097
  {
834
1098
  slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
835
1099
  method: z2.string().optional().describe("Filter by HTTP method"),
1100
+ kind: kindSchema,
836
1101
  q: z2.string().optional().describe("Free-text search across path, body, and headers"),
837
1102
  from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
838
1103
  to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
839
1104
  },
840
- withErrorHandling(async ({ slug, method, q, from, to }) => {
841
- const count = await client.requests.count({ slug, method, q, from, to });
1105
+ withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
1106
+ const count = await client.requests.count({ slug, method, kind, q, from, to });
842
1107
  return jsonContent({ count });
843
1108
  })
844
1109
  );
@@ -848,7 +1113,7 @@ function registerTools(server, client) {
848
1113
  { requestId: z2.string().describe("The request ID") },
849
1114
  withErrorHandling(async ({ requestId }) => {
850
1115
  const request = await client.requests.get(requestId);
851
- return jsonContent(request);
1116
+ return jsonContent(compactRequest(request));
852
1117
  })
853
1118
  );
854
1119
  server.tool(
@@ -861,7 +1126,7 @@ function registerTools(server, client) {
861
1126
  },
862
1127
  withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
863
1128
  const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
864
- return jsonContent(request);
1129
+ return jsonContent(compactRequest(request));
865
1130
  })
866
1131
  );
867
1132
  server.tool(
@@ -881,7 +1146,9 @@ function registerTools(server, client) {
881
1146
  pollInterval,
882
1147
  method
883
1148
  });
884
- return jsonContent(result);
1149
+ return jsonContent(
1150
+ fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
1151
+ );
885
1152
  })
886
1153
  );
887
1154
  server.tool(
@@ -1190,6 +1457,154 @@ function registerTools(server, client) {
1190
1457
  }
1191
1458
  )
1192
1459
  );
1460
+ server.tool(
1461
+ "list_emails",
1462
+ "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.",
1463
+ {
1464
+ endpointSlug: z2.string().describe("The endpoint slug"),
1465
+ limit: z2.number().int().min(1).max(100).default(25).describe(
1466
+ `Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
1467
+ ),
1468
+ since: z2.number().optional().describe("Only emails received after this timestamp in ms"),
1469
+ tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
1470
+ subject: z2.string().optional().describe("Only emails whose subject contains this text"),
1471
+ from: z2.string().optional().describe("Only emails from this address (case-insensitive)")
1472
+ },
1473
+ withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
1474
+ const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
1475
+ const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
1476
+ const [emails, view] = await Promise.all([
1477
+ client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
1478
+ emailEndpointView(client, endpointSlug)
1479
+ ]);
1480
+ return jsonContent(
1481
+ emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
1482
+ );
1483
+ })
1484
+ );
1485
+ server.tool(
1486
+ "get_email",
1487
+ "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.",
1488
+ {
1489
+ requestId: z2.string().optional().describe("The email's request ID"),
1490
+ endpointSlug: z2.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
1491
+ tag: z2.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
1492
+ includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
1493
+ },
1494
+ withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
1495
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1496
+ throw new Error("Pass exactly one of requestId or endpointSlug");
1497
+ }
1498
+ const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
1499
+ if (!email) {
1500
+ throw new NotFoundError(
1501
+ `No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
1502
+ );
1503
+ }
1504
+ const view = await emailEndpointView(
1505
+ client,
1506
+ endpointSlug ?? emailSlugFromAddress(email.path)
1507
+ );
1508
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1509
+ })
1510
+ );
1511
+ server.tool(
1512
+ "wait_for_email",
1513
+ "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.",
1514
+ {
1515
+ endpointSlug: z2.string().describe("The endpoint slug"),
1516
+ tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
1517
+ subject: z2.string().optional().describe("Only emails whose subject contains this text"),
1518
+ from: z2.string().optional().describe("Only emails from this address (case-insensitive)"),
1519
+ timeout: durationOrTimestampSchema.default("30s").describe(
1520
+ `How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
1521
+ ),
1522
+ since: z2.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
1523
+ includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false)")
1524
+ },
1525
+ withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1526
+ const [email, view] = await Promise.all([
1527
+ client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1528
+ emailEndpointView(client, endpointSlug)
1529
+ ]);
1530
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1531
+ })
1532
+ );
1533
+ server.tool(
1534
+ "send_test_email",
1535
+ "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.",
1536
+ {
1537
+ slug: z2.string().describe("The endpoint slug"),
1538
+ tag: z2.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
1539
+ },
1540
+ withErrorHandling(async ({ slug, tag }) => {
1541
+ const result = await client.emails.sendTest(slug, { tag });
1542
+ return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1543
+ })
1544
+ );
1545
+ server.tool(
1546
+ "configure_forwarding",
1547
+ "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.",
1548
+ {
1549
+ slug: z2.string().describe("The endpoint slug"),
1550
+ url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1551
+ enabled: z2.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
1552
+ },
1553
+ withErrorHandling(async ({ slug, url, enabled }) => {
1554
+ const endpoint = await client.forwarding.configure(slug, { url, enabled });
1555
+ return jsonContent({
1556
+ slug: endpoint.slug,
1557
+ forwardEnabled: endpoint.forwardEnabled ?? false,
1558
+ forwardUrl: endpoint.forwardUrl ?? null,
1559
+ hasForwardSecret: endpoint.hasForwardSecret ?? false
1560
+ });
1561
+ })
1562
+ );
1563
+ server.tool(
1564
+ "get_forwarding_secret",
1565
+ "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.",
1566
+ {
1567
+ slug: z2.string().describe("The endpoint slug"),
1568
+ rotate: z2.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1569
+ },
1570
+ withErrorHandling(async ({ slug, rotate }) => {
1571
+ const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1572
+ return jsonContent({ secret, rotated: rotate });
1573
+ })
1574
+ );
1575
+ server.tool(
1576
+ "test_forwarding",
1577
+ "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.",
1578
+ { slug: z2.string().describe("The endpoint slug") },
1579
+ withErrorHandling(async ({ slug }) => {
1580
+ return jsonContent(await client.forwarding.test(slug));
1581
+ })
1582
+ );
1583
+ server.tool(
1584
+ "list_deliveries",
1585
+ "List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
1586
+ {
1587
+ endpointSlug: z2.string().optional().describe("List the endpoint's latest deliveries"),
1588
+ requestId: z2.string().optional().describe("List every delivery and try of this email"),
1589
+ limit: z2.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
1590
+ },
1591
+ withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
1592
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1593
+ throw new Error("Pass exactly one of endpointSlug or requestId");
1594
+ }
1595
+ return jsonContent(
1596
+ requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
1597
+ );
1598
+ })
1599
+ );
1600
+ server.tool(
1601
+ "redeliver_email",
1602
+ "Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
1603
+ { requestId: z2.string().describe("The email's request ID") },
1604
+ withErrorHandling(async ({ requestId }) => {
1605
+ return jsonContent(await client.forwarding.redeliver(requestId));
1606
+ })
1607
+ );
1193
1608
  server.tool(
1194
1609
  "describe",
1195
1610
  "Describe all available SDK operations, parameters, and types.",
@@ -1304,7 +1719,7 @@ function registerAgentRegistrationTools(server) {
1304
1719
  }
1305
1720
 
1306
1721
  // src/index.ts
1307
- var VERSION = true ? "1.9.0" : "0.0.0-dev";
1722
+ var VERSION = true ? "1.10.0" : "0.0.0-dev";
1308
1723
  function createServer(options = {}) {
1309
1724
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
1310
1725
  const server = new McpServer2({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webhooks-cc/mcp",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -36,12 +36,12 @@
36
36
  },
37
37
  "homepage": "https://webhooks.cc",
38
38
  "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.30.0",
39
+ "@modelcontextprotocol/sdk": "^1.31.0",
40
40
  "zod": "^4.6.5",
41
- "@webhooks-cc/sdk": "1.11.0"
41
+ "@webhooks-cc/sdk": "1.12.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^26.6.3",
44
+ "@types/node": "^26.6.4",
45
45
  "tsup": "^8.5.1",
46
46
  "typescript": "^6.0.3",
47
47
  "vitest": "^5.0.2"