@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/tools.js ADDED
@@ -0,0 +1,1827 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/tools.ts
21
+ var tools_exports = {};
22
+ __export(tools_exports, {
23
+ REMOTE_MAX_WAIT_MS: () => REMOTE_MAX_WAIT_MS,
24
+ REMOTE_MIN_POLL_MS: () => REMOTE_MIN_POLL_MS,
25
+ createEndpointShape: () => createEndpointShape,
26
+ deleteEndpointShape: () => deleteEndpointShape,
27
+ getEndpointShape: () => getEndpointShape,
28
+ getRequestShape: () => getRequestShape,
29
+ jsonContent: () => jsonContent,
30
+ listEndpointsShape: () => listEndpointsShape,
31
+ listRequestsShape: () => listRequestsShape,
32
+ payloadSchemaResult: () => payloadSchemaResult,
33
+ payloadSchemaShape: () => payloadSchemaShape,
34
+ registerTools: () => registerTools,
35
+ textContent: () => textContent,
36
+ waitForRequestShape: () => waitForRequestShape,
37
+ withErrorHandling: () => withErrorHandling
38
+ });
39
+ module.exports = __toCommonJS(tools_exports);
40
+ var import_zod = require("zod");
41
+ var import_sdk = require("@webhooks-cc/sdk");
42
+
43
+ // src/compact.ts
44
+ var MAX_OUTPUT = 32768;
45
+ var MAX_EMAIL_TEXT = 8e3;
46
+ var MAX_EXTRACTS = 10;
47
+ var MAX_LINK_TEXT = 200;
48
+ var MAX_LINK_URL = 2e3;
49
+ var MAX_LIST_ITEMS = 5;
50
+ var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
51
+ var TEXT_FLOOR = 2e3;
52
+ function jsonSize(value) {
53
+ return JSON.stringify(value, null, 2).length;
54
+ }
55
+ function sliceText(text, end) {
56
+ if (end <= 0) return "";
57
+ if (end >= text.length) return text;
58
+ const last = text.charCodeAt(end - 1);
59
+ return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
60
+ }
61
+ function cutEmailText(text) {
62
+ if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
63
+ return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
64
+ }
65
+ function shrinkToFit(root, budget, steps) {
66
+ for (const step of steps) {
67
+ if (jsonSize(root) <= budget) return;
68
+ step();
69
+ }
70
+ }
71
+ function omitHeaders(holder) {
72
+ const headers = holder.headers;
73
+ if (headers === null || typeof headers !== "object") return;
74
+ const count = Object.keys(headers).length;
75
+ if (count === 0) return;
76
+ holder.headers = {};
77
+ holder.headersOmitted = `${count} headers left out to fit the output`;
78
+ }
79
+ function trimSpam(holder) {
80
+ const spam = holder.spam;
81
+ if (spam && typeof spam === "object" && !Array.isArray(spam)) {
82
+ const { score, required, action, error } = spam;
83
+ holder.spam = { score, required, action, error };
84
+ }
85
+ }
86
+ function capExtracts(holder) {
87
+ if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
88
+ holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
89
+ holder.codesTruncated = true;
90
+ }
91
+ if (Array.isArray(holder.links)) {
92
+ if (holder.links.length > MAX_EXTRACTS) {
93
+ holder.links = holder.links.slice(0, MAX_EXTRACTS);
94
+ holder.linksTruncated = true;
95
+ }
96
+ holder.links = holder.links.map((link) => ({
97
+ ...link,
98
+ ...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
99
+ ...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
100
+ }));
101
+ }
102
+ }
103
+ function capLists(holder) {
104
+ for (const key of EMAIL_LISTS) {
105
+ const list = holder[key];
106
+ if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
107
+ holder[key] = list.slice(0, MAX_LIST_ITEMS);
108
+ holder[`${key}Total`] = list.length;
109
+ }
110
+ }
111
+ }
112
+ function keepOnly(holder, keep) {
113
+ for (const key of Object.keys(holder)) {
114
+ if (!keep.includes(key)) delete holder[key];
115
+ }
116
+ holder.trimmed = "The email was too big for the output, so only these fields are shown.";
117
+ }
118
+ function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
119
+ const value = holder[key];
120
+ if (typeof value !== "string" || value.length <= floor) return;
121
+ const flagged = holder[flag] === true;
122
+ holder[key] = "";
123
+ holder[flag] = true;
124
+ const room = budget - jsonSize(root) + 2;
125
+ let low = 0;
126
+ let high = value.length;
127
+ while (low < high) {
128
+ const mid = Math.ceil((low + high) / 2);
129
+ if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
130
+ else high = mid - 1;
131
+ }
132
+ const kept = sliceText(value, Math.max(low, floor));
133
+ holder[key] = kept;
134
+ if (kept.length === value.length && !flagged) delete holder[flag];
135
+ }
136
+ function keepFirst(holder, key) {
137
+ const list = holder[key];
138
+ if (!Array.isArray(list) || list.length <= 1) return;
139
+ holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
140
+ holder[key] = list.slice(0, 1);
141
+ }
142
+ var ESSENTIAL_EMAIL_FIELDS = [
143
+ "subject",
144
+ "from",
145
+ "fromTotal",
146
+ "tag",
147
+ "date",
148
+ "text",
149
+ "textTruncated",
150
+ "htmlSize",
151
+ "spam"
152
+ ];
153
+ function compactRequest(request) {
154
+ if (request.kind !== "email") return request;
155
+ const compact = { ...request };
156
+ delete compact.body;
157
+ delete compact.bodyRaw;
158
+ compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
159
+ if (!request.email) return compact;
160
+ const { html, ...rest } = request.email;
161
+ const { text, cut } = cutEmailText(rest.text);
162
+ const restoreText = () => {
163
+ email.text = text;
164
+ if (cut) email.textTruncated = true;
165
+ else delete email.textTruncated;
166
+ };
167
+ const email = {
168
+ ...rest,
169
+ text,
170
+ ...cut ? { textTruncated: true } : {},
171
+ htmlSize: html?.length ?? 0
172
+ };
173
+ compact.email = email;
174
+ const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
175
+ const budget = MAX_OUTPUT - 128;
176
+ shrinkToFit(root, budget, [
177
+ () => omitHeaders(compact),
178
+ () => trimSpam(email),
179
+ () => capLists(email),
180
+ () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
181
+ // Down to the essentials there may be room for more of the text again.
182
+ () => {
183
+ keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
184
+ keepFirst(email, "from");
185
+ restoreText();
186
+ cutStringToFit(root, email, "text", "textTruncated", budget);
187
+ },
188
+ () => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
189
+ ]);
190
+ return compact;
191
+ }
192
+
193
+ // src/tools.ts
194
+ var MAX_BODY_SIZE = MAX_OUTPUT;
195
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
196
+ var TIME_SEPARATOR = " \u2014 ";
197
+ var httpUrlSchema = import_zod.z.string().url().refine(
198
+ (value) => {
199
+ try {
200
+ const protocol = new URL(value).protocol;
201
+ return protocol === "http:" || protocol === "https:";
202
+ } catch {
203
+ return false;
204
+ }
205
+ },
206
+ { message: "Only http and https URLs are supported" }
207
+ );
208
+ var methodSchema = import_zod.z.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
209
+ var kindSchema = import_zod.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
210
+ var EMAIL_SCAN_LIMIT = 100;
211
+ var durationOrTimestampSchema = import_zod.z.union([import_zod.z.string(), import_zod.z.number()]);
212
+ var CONDITION_FIELD_NAMES = Object.keys(import_sdk.CONDITION_FIELDS);
213
+ var CONDITION_OP_NAMES = import_sdk.CONDITION_OPS;
214
+ function conditionSchema(use) {
215
+ const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => import_sdk.CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
216
+ return import_zod.z.object({
217
+ field: import_zod.z.enum(fields).describe(
218
+ 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)"
219
+ ),
220
+ op: import_zod.z.enum(CONDITION_OP_NAMES).describe(
221
+ "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)"
222
+ ),
223
+ value: import_zod.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
224
+ name: import_zod.z.string().max(import_sdk.MAX_CONDITION_NAME_LEN).optional().describe("Header or query parameter name; the email part; spf, dkim or dmarc"),
225
+ path: import_zod.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"]')
226
+ }).superRefine((condition, ctx) => {
227
+ for (const issue of (0, import_sdk.conditionIssues)(condition, { use })) {
228
+ ctx.addIssue({ code: "custom", message: issue });
229
+ }
230
+ });
231
+ }
232
+ var ruleConditionSchema = conditionSchema("response");
233
+ var destinationFields = {
234
+ name: import_zod.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
235
+ enabled: import_zod.z.boolean().optional().describe("Send to it (default true)"),
236
+ http: import_zod.z.boolean().optional().describe("Takes HTTP requests (default true)"),
237
+ email: import_zod.z.boolean().optional().describe("Takes emails (default true)"),
238
+ filter: import_zod.z.object({
239
+ logic: import_zod.z.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
240
+ conditions: import_zod.z.array(conditionSchema("filter")).max(10)
241
+ }).nullable().optional().describe(
242
+ "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)"
243
+ ),
244
+ format: import_zod.z.enum([
245
+ "auto",
246
+ "as_received",
247
+ "json",
248
+ "template",
249
+ "chat",
250
+ "postmark",
251
+ "sendgrid",
252
+ "mailgun",
253
+ "cloudmailin"
254
+ ]).optional().describe(
255
+ "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"
256
+ ),
257
+ mailgunSigningKey: import_zod.z.string().max(200).nullable().optional().describe(
258
+ "For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
259
+ ),
260
+ bodyTemplate: import_zod.z.unknown().optional().describe(
261
+ `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)`
262
+ ),
263
+ messageTemplate: import_zod.z.string().max(4e3).nullable().optional().describe(
264
+ "For format chat: the message text with {{ placeholders }}; null sends the standard message"
265
+ ),
266
+ headers: import_zod.z.array(
267
+ import_zod.z.object({
268
+ name: import_zod.z.string().describe("Header name, e.g. Authorization"),
269
+ value: import_zod.z.string().nullable().describe("Value, or null to keep the stored one")
270
+ })
271
+ ).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
272
+ appendPath: import_zod.z.boolean().optional().describe("Append the captured path to the URL (as_received)"),
273
+ retrySeconds: import_zod.z.union([import_zod.z.literal(0), import_zod.z.literal(3600), import_zod.z.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
274
+ keepOrder: import_zod.z.boolean().optional().describe("One delivery at a time, in capture order"),
275
+ sentField: import_zod.z.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
276
+ throttleSeconds: import_zod.z.number().int().min(0).max(3600).optional().describe(
277
+ "At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
278
+ ),
279
+ delaySeconds: import_zod.z.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
280
+ };
281
+ var responseRuleSchema = import_zod.z.object({
282
+ name: import_zod.z.string().max(import_sdk.MAX_RULE_NAME_LEN).optional().describe(`Human-readable rule name (max ${import_sdk.MAX_RULE_NAME_LEN} chars)`),
283
+ enabled: import_zod.z.boolean().optional().default(true).describe("Whether this rule is active"),
284
+ logic: import_zod.z.enum(["and", "or"]).optional().default("and").describe('How to combine conditions: "and" (all match) or "or" (any match)'),
285
+ conditions: import_zod.z.array(ruleConditionSchema).min(1).max(10).describe("Conditions to evaluate (1-10)"),
286
+ response: import_zod.z.lazy(() => mockResponseSchema).describe("Response when conditions match")
287
+ });
288
+ var responseRulesSchema = import_zod.z.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
289
+ var mockResponseSchema = import_zod.z.object({
290
+ status: import_zod.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})`),
291
+ body: import_zod.z.string().default("").describe(
292
+ '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'
293
+ ),
294
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
295
+ delay: import_zod.z.number().int().min(import_sdk.MOCK_RESPONSE_DELAY_MIN).max(import_sdk.MOCK_RESPONSE_DELAY_MAX).optional().describe(
296
+ `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
297
+ )
298
+ });
299
+ var createEndpointShape = {
300
+ name: import_zod.z.string().optional().describe("Display name for the endpoint"),
301
+ ephemeral: import_zod.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
302
+ expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"'),
303
+ mockResponse: mockResponseSchema.optional().describe("Optional mock response to return when the endpoint receives a request"),
304
+ responseRules: responseRulesSchema.optional().describe(
305
+ "Conditional response rules. Each rule has conditions and a response. First matching rule wins."
306
+ ),
307
+ notificationUrl: import_zod.z.string().url().optional().describe(
308
+ "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
309
+ ),
310
+ signingProvider: import_zod.z.string().optional().describe(
311
+ "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
312
+ ),
313
+ signingSecret: import_zod.z.string().optional().describe(
314
+ "Signing secret (encrypted server-side, never returned). Required when signingProvider is set."
315
+ ),
316
+ signingHeader: import_zod.z.string().optional().describe("Custom signature header name. Only used with generic-hmac provider.")
317
+ };
318
+ var listEndpointsShape = {
319
+ team: import_zod.z.string().min(1).optional().describe(
320
+ "Team id or name (case-insensitive). Keeps endpoints shared with you from that team and endpoints you own that are shared with it."
321
+ )
322
+ };
323
+ var getEndpointShape = { slug: import_zod.z.string().describe("The endpoint slug") };
324
+ var deleteEndpointShape = { slug: import_zod.z.string().describe("The endpoint slug to delete") };
325
+ var listRequestsShape = {
326
+ endpointSlug: import_zod.z.string().describe("The endpoint slug"),
327
+ limit: import_zod.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
328
+ since: import_zod.z.number().optional().describe("Only return requests after this timestamp in ms"),
329
+ kind: kindSchema
330
+ };
331
+ var getRequestShape = { requestId: import_zod.z.string().describe("The request ID") };
332
+ var waitForRequestShape = {
333
+ endpointSlug: import_zod.z.string().describe("The endpoint slug to monitor"),
334
+ timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
335
+ pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
336
+ };
337
+ var payloadSchemaShape = {
338
+ endpointSlug: import_zod.z.string().describe("The endpoint slug"),
339
+ format: import_zod.z.enum(import_sdk.PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
340
+ 'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
341
+ ),
342
+ name: import_zod.z.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
343
+ event: import_zod.z.string().optional().describe(
344
+ 'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
345
+ ),
346
+ method: import_zod.z.string().optional().describe("Only requests with this HTTP method"),
347
+ path: import_zod.z.string().optional().describe("Only requests with this path"),
348
+ requestIds: import_zod.z.array(import_zod.z.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
349
+ limit: import_zod.z.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
350
+ };
351
+ function eventName(event) {
352
+ const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
353
+ const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
354
+ if (!name) return "WebhookPayload";
355
+ return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
356
+ }
357
+ function payloadSchemaResult(requests, args) {
358
+ const method = args.method?.toUpperCase();
359
+ const matching = requests.filter(
360
+ (request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args.path || request.path === args.path) && (!args.event || (0, import_sdk.detectWebhookInfo)(request)?.event === args.event)
361
+ );
362
+ if (matching.length === 0) {
363
+ throw new Error(
364
+ `None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
365
+ );
366
+ }
367
+ const events = [
368
+ ...new Set(
369
+ matching.map((request) => (0, import_sdk.detectWebhookInfo)(request)?.event).filter((e) => !!e)
370
+ )
371
+ ];
372
+ const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
373
+ const result = (0, import_sdk.generateRequestSchema)(matching, { format: args.format, name });
374
+ if (result.samples === 0) {
375
+ throw new Error(
376
+ `None of the ${matching.length} matching requests has a JSON or form body to learn from.`
377
+ );
378
+ }
379
+ const output = {
380
+ format: result.format,
381
+ name: result.name,
382
+ samples: result.samples,
383
+ skipped: result.skipped,
384
+ ...events.length > 0 ? { events } : {},
385
+ ...events.length > 1 ? {
386
+ warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
387
+ } : {},
388
+ code: result.code
389
+ };
390
+ const size = JSON.stringify(output, null, 2).length;
391
+ if (size > MAX_OUTPUT) {
392
+ throw new Error(
393
+ `The schema is ${result.code.length} characters, more than this tool can return. Narrow it with event or requestIds, or call generateRequestSchema() from @webhooks-cc/sdk in code.`
394
+ );
395
+ }
396
+ return jsonContent(output);
397
+ }
398
+ function textContent(text) {
399
+ return { content: [{ type: "text", text }] };
400
+ }
401
+ function serializeJson(value, limit = MAX_BODY_SIZE) {
402
+ const full = JSON.stringify(value, null, 2);
403
+ if (full.length <= limit) {
404
+ return full;
405
+ }
406
+ if (Array.isArray(value)) {
407
+ let low = 0;
408
+ let high = value.length;
409
+ let best = JSON.stringify(
410
+ { items: [], truncated: true, total: value.length, returned: 0 },
411
+ null,
412
+ 2
413
+ );
414
+ while (low <= high) {
415
+ const mid = Math.floor((low + high) / 2);
416
+ const candidate = JSON.stringify(
417
+ {
418
+ items: value.slice(0, mid),
419
+ truncated: true,
420
+ total: value.length,
421
+ returned: mid
422
+ },
423
+ null,
424
+ 2
425
+ );
426
+ if (candidate.length <= limit) {
427
+ best = candidate;
428
+ low = mid + 1;
429
+ } else {
430
+ high = mid - 1;
431
+ }
432
+ }
433
+ return best;
434
+ }
435
+ return full.slice(0, limit) + `
436
+ ... [truncated, ${full.length} chars total]`;
437
+ }
438
+ function fitArrayField(value, key) {
439
+ const items = value[key];
440
+ if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
441
+ const cut = (count) => ({
442
+ ...value,
443
+ [key]: items.slice(0, count),
444
+ truncated: true,
445
+ returned: count
446
+ });
447
+ let low = 0;
448
+ let high = items.length;
449
+ while (low < high) {
450
+ const mid = Math.ceil((low + high) / 2);
451
+ if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
452
+ else high = mid - 1;
453
+ }
454
+ return cut(low);
455
+ }
456
+ function jsonContent(value) {
457
+ return textContent(serializeJson(value));
458
+ }
459
+ async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
460
+ const text = await response.text();
461
+ if (text.length <= limit) return text;
462
+ return text.slice(0, limit) + `
463
+ ... [truncated, ${text.length} chars total]`;
464
+ }
465
+ function sleep(ms) {
466
+ return new Promise((resolve) => setTimeout(resolve, ms));
467
+ }
468
+ function splitHint(message) {
469
+ const separatorIndex = message.indexOf(TIME_SEPARATOR);
470
+ if (separatorIndex === -1) {
471
+ return { message, hint: null };
472
+ }
473
+ return {
474
+ message: message.slice(0, separatorIndex),
475
+ hint: message.slice(separatorIndex + TIME_SEPARATOR.length) || null
476
+ };
477
+ }
478
+ function serializeError(error) {
479
+ const rawMessage = error instanceof Error ? error.message : String(error);
480
+ const { message, hint } = splitHint(rawMessage);
481
+ const payload = {
482
+ error: true,
483
+ code: "validation_error",
484
+ message,
485
+ hint,
486
+ retryAfter: null
487
+ };
488
+ if (error instanceof import_sdk.UnauthorizedError) {
489
+ payload.code = "unauthorized";
490
+ } else if (error instanceof import_sdk.NotFoundError) {
491
+ payload.code = "not_found";
492
+ } else if (error instanceof import_sdk.RateLimitError) {
493
+ payload.code = "rate_limited";
494
+ payload.retryAfter = error.retryAfter ?? null;
495
+ } else if (error instanceof import_sdk.TimeoutError) {
496
+ payload.code = "timeout";
497
+ } else if (error instanceof import_sdk.WebhooksCCError) {
498
+ payload.code = error.statusCode >= 500 ? "server_error" : "validation_error";
499
+ }
500
+ return JSON.stringify(payload, null, 2);
501
+ }
502
+ function withErrorHandling(handler) {
503
+ return async (args) => {
504
+ try {
505
+ return await handler(args);
506
+ } catch (error) {
507
+ return { ...textContent(serializeError(error)), isError: true };
508
+ }
509
+ };
510
+ }
511
+ function filterRequestsByMethod(requests, method) {
512
+ if (!method) {
513
+ return requests;
514
+ }
515
+ const target = method.toUpperCase();
516
+ return requests.filter((request) => request.method.toUpperCase() === target);
517
+ }
518
+ async function waitForMultipleRequests(client, endpointSlug, options) {
519
+ const timeoutMs = typeof options.timeout === "number" ? options.timeout : options.timeout ? Number.isNaN(Number(options.timeout)) ? parseDurationLike(options.timeout) : Number(options.timeout) : 3e4;
520
+ const pollIntervalMs = typeof options.pollInterval === "number" ? options.pollInterval : options.pollInterval ? Number.isNaN(Number(options.pollInterval)) ? parseDurationLike(options.pollInterval) : Number(options.pollInterval) : 500;
521
+ const startedAt = Date.now();
522
+ let since = startedAt;
523
+ const seenIds = /* @__PURE__ */ new Set();
524
+ const requests = [];
525
+ while (Date.now() - startedAt < timeoutMs) {
526
+ const checkTime = Date.now();
527
+ const page = await client.requests.list(endpointSlug, {
528
+ since,
529
+ limit: Math.max(100, options.count * 5)
530
+ });
531
+ since = checkTime;
532
+ const filtered = filterRequestsByMethod(page, options.method).slice().sort((left, right) => left.receivedAt - right.receivedAt);
533
+ for (const request of filtered) {
534
+ if (seenIds.has(request.id)) {
535
+ continue;
536
+ }
537
+ seenIds.add(request.id);
538
+ requests.push(request);
539
+ if (requests.length >= options.count) {
540
+ return {
541
+ requests,
542
+ complete: true,
543
+ timedOut: false,
544
+ expectedCount: options.count
545
+ };
546
+ }
547
+ }
548
+ await sleep(
549
+ Math.min(Math.max(10, pollIntervalMs), Math.max(0, timeoutMs - (Date.now() - startedAt)))
550
+ );
551
+ }
552
+ return {
553
+ requests,
554
+ complete: requests.length >= options.count,
555
+ timedOut: true,
556
+ expectedCount: options.count
557
+ };
558
+ }
559
+ function parseDurationLike(value) {
560
+ const trimmed = value.trim();
561
+ if (trimmed.length === 0) {
562
+ throw new Error("Duration value cannot be empty");
563
+ }
564
+ const numeric = Number(trimmed);
565
+ if (!Number.isNaN(numeric)) {
566
+ return numeric;
567
+ }
568
+ const match = trimmed.match(/^(\d+)\s*(ms|s|m|h|d)$/i);
569
+ if (!match) {
570
+ throw new Error(`Invalid duration: "${value}"`);
571
+ }
572
+ const amount = Number(match[1]);
573
+ const unit = match[2].toLowerCase();
574
+ const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
575
+ return amount * multiplier;
576
+ }
577
+ function ensureVerifyArgs(args) {
578
+ if (args.provider === "discord") {
579
+ const publicKey = args.publicKey?.trim();
580
+ if (!publicKey) {
581
+ throw new Error('verify_signature for provider "discord" requires publicKey');
582
+ }
583
+ return {
584
+ provider: "discord",
585
+ publicKey
586
+ };
587
+ }
588
+ const secret = args.secret?.trim();
589
+ if (!secret) {
590
+ throw new Error(`verify_signature for provider "${args.provider}" requires secret`);
591
+ }
592
+ return {
593
+ provider: args.provider,
594
+ secret,
595
+ ...args.url ? { url: args.url } : {},
596
+ ...args.method ? { method: args.method } : {}
597
+ };
598
+ }
599
+ async function summarizeResponse(response) {
600
+ return {
601
+ status: response.status,
602
+ statusText: response.statusText,
603
+ body: await readBodyTruncated(response)
604
+ };
605
+ }
606
+ async function emailEndpointView(client, slug) {
607
+ const endpoint = await client.endpoints.get(slug);
608
+ return {
609
+ endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
610
+ includeExtracts: endpoint.showEmailExtracts !== false
611
+ };
612
+ }
613
+ function emailSlugFromAddress(address) {
614
+ const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
615
+ return local.split("+")[0].toLowerCase();
616
+ }
617
+ var MAX_SUMMARY_LINK = 8e3;
618
+ function boundedLink(link, max) {
619
+ if (link == null || link.length <= max) return { link: link ?? null };
620
+ return {
621
+ link: null,
622
+ linkOmitted: `The main link is ${link.length} characters long, too long for the output`
623
+ };
624
+ }
625
+ var MAX_SUMMARY_FIELD = 1e3;
626
+ function summarizeEmail(email, includeExtracts) {
627
+ const from = email.email.from[0];
628
+ const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
629
+ const subject = email.email.subject;
630
+ return {
631
+ id: email.id,
632
+ receivedAt: new Date(email.receivedAt).toISOString(),
633
+ address: email.path,
634
+ tag: email.email.tag,
635
+ subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
636
+ from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
637
+ ...includeExtracts ? { code: (0, import_sdk.extractCode)(email), ...boundedLink((0, import_sdk.extractLink)(email), MAX_SUMMARY_LINK) } : {},
638
+ attachments: email.email.attachments.length
639
+ };
640
+ }
641
+ var ESSENTIAL_DETAIL_FIELDS = [
642
+ "code",
643
+ "link",
644
+ "id",
645
+ "endpoint",
646
+ "receivedAt",
647
+ "address",
648
+ "tag",
649
+ "subject",
650
+ "from",
651
+ "fields",
652
+ "replyText",
653
+ "size",
654
+ "test",
655
+ "spam",
656
+ "text",
657
+ "textTruncated",
658
+ "htmlSize",
659
+ "html",
660
+ "htmlTruncated"
661
+ ];
662
+ function shrinkRendered(rendered, max) {
663
+ const body = typeof rendered.body === "string" ? rendered.body : "";
664
+ if (body.length <= max - 1024) return rendered;
665
+ return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
666
+ }
667
+ function emailDetail(client, email, view, includeHtml) {
668
+ const { data } = client.emails.toJson(email, {
669
+ endpoint: view.endpoint,
670
+ includeExtracts: view.includeExtracts
671
+ });
672
+ const { html, text: fullText, ...rest } = data;
673
+ const { text, cut } = cutEmailText(fullText ?? null);
674
+ const restoreText = () => {
675
+ detail.text = text;
676
+ if (cut) detail.textTruncated = true;
677
+ else delete detail.textTruncated;
678
+ };
679
+ const detail = {
680
+ ...view.includeExtracts ? { code: (0, import_sdk.extractCode)(email), link: (0, import_sdk.extractLink)(email) } : {},
681
+ ...rest,
682
+ text,
683
+ ...cut ? { textTruncated: true } : {},
684
+ htmlSize: html?.length ?? 0,
685
+ ...includeHtml && html !== null ? { html } : {}
686
+ };
687
+ shrinkToFit(detail, MAX_BODY_SIZE, [
688
+ () => omitHeaders(detail),
689
+ () => trimSpam(detail),
690
+ () => capExtracts(detail),
691
+ () => capLists(detail),
692
+ () => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
693
+ () => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
694
+ // Down to the essentials (and without a main link too long to use) there
695
+ // may be room for more of the text again.
696
+ () => {
697
+ keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
698
+ if (view.includeExtracts) {
699
+ Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
700
+ }
701
+ restoreText();
702
+ cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
703
+ },
704
+ () => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
705
+ ]);
706
+ return detail;
707
+ }
708
+ var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
709
+ var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
710
+ var REMOTE_MAX_WAIT_MS = 5e4;
711
+ var REMOTE_MIN_POLL_MS = 1e3;
712
+ function durationMs(value) {
713
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
714
+ if (typeof value !== "string") return null;
715
+ try {
716
+ const ms = (0, import_sdk.parseDuration)(value);
717
+ return Number.isFinite(ms) && ms >= 0 ? ms : null;
718
+ } catch {
719
+ return null;
720
+ }
721
+ }
722
+ function toolTarget(target, remote, handles) {
723
+ return {
724
+ tool(name, description, shape, handler) {
725
+ if (remote && LOCAL_ONLY_TOOLS.has(name)) return { remove() {
726
+ } };
727
+ const wrapped = remote ? async (args) => {
728
+ const input = args;
729
+ if (TARGET_URL_TOOLS.has(name) && input.targetUrl !== void 0) {
730
+ return {
731
+ ...textContent(
732
+ "targetUrl needs the local MCP server (npx -y @webhooks-cc/mcp): the hosted server cannot reach your machine. Leave it out here."
733
+ ),
734
+ isError: true
735
+ };
736
+ }
737
+ const bounded = { ...input };
738
+ if ("pollInterval" in shape && input.pollInterval === void 0) {
739
+ bounded.pollInterval = REMOTE_MIN_POLL_MS;
740
+ }
741
+ for (const [key, bound] of [
742
+ ["timeout", (ms) => Math.min(ms, REMOTE_MAX_WAIT_MS)],
743
+ [
744
+ "pollInterval",
745
+ (ms) => Math.min(Math.max(ms, REMOTE_MIN_POLL_MS), REMOTE_MAX_WAIT_MS)
746
+ ]
747
+ ]) {
748
+ if (bounded[key] === void 0) continue;
749
+ const ms = durationMs(bounded[key]);
750
+ if (ms === null) {
751
+ return {
752
+ ...textContent(`${key} must be a duration such as "30s" or milliseconds.`),
753
+ isError: true
754
+ };
755
+ }
756
+ bounded[key] = bound(ms);
757
+ }
758
+ return handler(bounded);
759
+ } : handler;
760
+ const handle = target.tool(name, description, shape, wrapped);
761
+ handles.push(handle);
762
+ return handle;
763
+ }
764
+ };
765
+ }
766
+ function registerTools(target, client, options = {}) {
767
+ const handles = [];
768
+ const server = toolTarget(target, options.remote === true, handles);
769
+ server.tool(
770
+ "create_endpoint",
771
+ "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
772
+ createEndpointShape,
773
+ withErrorHandling(
774
+ async ({
775
+ name,
776
+ ephemeral,
777
+ expiresIn,
778
+ mockResponse,
779
+ responseRules,
780
+ notificationUrl,
781
+ signingProvider,
782
+ signingSecret,
783
+ signingHeader
784
+ }) => {
785
+ if (signingProvider) {
786
+ if (signingProvider !== "discord" && !signingSecret) {
787
+ throw new Error(`signingSecret is required for provider "${signingProvider}"`);
788
+ }
789
+ if (signingProvider === "generic-hmac" && !signingHeader) {
790
+ throw new Error("signingHeader is required for generic-hmac provider");
791
+ }
792
+ }
793
+ if (signingSecret && !signingProvider) {
794
+ throw new Error("signingProvider is required when signingSecret is set");
795
+ }
796
+ const endpoint = await client.endpoints.create({
797
+ name,
798
+ ephemeral,
799
+ expiresIn,
800
+ mockResponse,
801
+ responseRules,
802
+ notificationUrl
803
+ });
804
+ if (signingProvider && signingSecret) {
805
+ try {
806
+ await client.endpoints.update(endpoint.slug, {
807
+ signingProvider,
808
+ signingSecret,
809
+ signingHeader
810
+ });
811
+ } catch (err) {
812
+ return jsonContent({
813
+ ...endpoint,
814
+ warning: `Endpoint created but signing configuration failed: ${err instanceof Error ? err.message : String(err)}. Update endpoint "${endpoint.slug}" to add signing.`
815
+ });
816
+ }
817
+ }
818
+ return jsonContent(endpoint);
819
+ }
820
+ )
821
+ );
822
+ server.tool(
823
+ "list_endpoints",
824
+ "List webhook endpoints: those you own (with sharedWith) and those shared with you through teams (with fromTeam). Optionally keep only one team's endpoints.",
825
+ listEndpointsShape,
826
+ withErrorHandling(async ({ team }) => {
827
+ const endpoints = await client.endpoints.list({ team });
828
+ return jsonContent(endpoints);
829
+ })
830
+ );
831
+ server.tool(
832
+ "list_teams",
833
+ "List the teams you own or belong to, with role, seats, pooled request usage (requestsUsed/requestLimit), period end, and whether the team is suspended for lack of a subscription.",
834
+ {},
835
+ withErrorHandling(async () => {
836
+ const teams = await client.teams.list();
837
+ return jsonContent(
838
+ teams.map((team) => ({
839
+ ...team,
840
+ periodEnd: team.periodEnd ? new Date(team.periodEnd).toISOString() : null
841
+ }))
842
+ );
843
+ })
844
+ );
845
+ server.tool(
846
+ "list_team_members",
847
+ "List a team's members and pending invites. Any member of the team may call this.",
848
+ { teamId: import_zod.z.string().min(1).describe("Team id (from list_teams)") },
849
+ withErrorHandling(async ({ teamId }) => {
850
+ const result = await client.teams.members(teamId);
851
+ return jsonContent(result);
852
+ })
853
+ );
854
+ server.tool(
855
+ "share_endpoint",
856
+ "Share an endpoint you own with a team so its members can inspect, stream, and edit it. The team needs an active subscription; the endpoint's requests then bill the team's pooled quota.",
857
+ {
858
+ slug: import_zod.z.string().describe("Slug of an endpoint you own"),
859
+ teamId: import_zod.z.string().min(1).describe("Team id (from list_teams)")
860
+ },
861
+ withErrorHandling(async ({ slug, teamId }) => {
862
+ await client.teams.share(teamId, slug);
863
+ return jsonContent({ shared: true, slug, teamId });
864
+ })
865
+ );
866
+ server.tool(
867
+ "unshare_endpoint",
868
+ "Stop sharing an endpoint you own with a team. Works even when the team is suspended.",
869
+ {
870
+ slug: import_zod.z.string().describe("Slug of an endpoint you own"),
871
+ teamId: import_zod.z.string().min(1).describe("Team id (from list_teams)")
872
+ },
873
+ withErrorHandling(async ({ slug, teamId }) => {
874
+ await client.teams.unshare(teamId, slug);
875
+ return jsonContent({ shared: false, slug, teamId });
876
+ })
877
+ );
878
+ server.tool(
879
+ "get_endpoint",
880
+ "Get details for a specific webhook endpoint by slug.",
881
+ getEndpointShape,
882
+ withErrorHandling(async ({ slug }) => {
883
+ const endpoint = await client.endpoints.get(slug);
884
+ return jsonContent(endpoint);
885
+ })
886
+ );
887
+ server.tool(
888
+ "update_endpoint",
889
+ "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.",
890
+ {
891
+ slug: import_zod.z.string().describe("The endpoint slug to update"),
892
+ name: import_zod.z.string().optional().describe("New display name"),
893
+ mockResponse: mockResponseSchema.nullable().optional().describe("Default mock response (used when no rule matches), or null to clear"),
894
+ responseRules: responseRulesSchema.nullable().optional().describe("Conditional response rules (first match wins), or null to clear"),
895
+ notificationUrl: import_zod.z.string().url().nullable().optional().describe("Notification webhook URL, or null to clear it"),
896
+ signingProvider: import_zod.z.string().nullable().optional().describe(
897
+ "Signing provider for automatic verification (e.g. stripe, github), or null to disable"
898
+ ),
899
+ signingSecret: import_zod.z.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
900
+ signingHeader: import_zod.z.string().nullable().optional().describe(
901
+ "Custom signature header name (only for generic-hmac provider), or null to clear"
902
+ ),
903
+ showEmailExtracts: import_zod.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
904
+ emailFields: import_zod.z.array(
905
+ import_zod.z.object({
906
+ name: import_zod.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
907
+ from: import_zod.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
908
+ header: import_zod.z.string().optional().describe('The header name, for from "header"'),
909
+ pattern: import_zod.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
910
+ })
911
+ ).max(20).nullable().optional().describe(
912
+ "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."
913
+ )
914
+ },
915
+ withErrorHandling(
916
+ async ({
917
+ slug,
918
+ name,
919
+ mockResponse,
920
+ responseRules,
921
+ notificationUrl,
922
+ signingProvider,
923
+ signingSecret,
924
+ signingHeader,
925
+ showEmailExtracts,
926
+ emailFields
927
+ }) => {
928
+ const endpoint = await client.endpoints.update(slug, {
929
+ name,
930
+ mockResponse,
931
+ responseRules,
932
+ notificationUrl,
933
+ signingProvider,
934
+ signingSecret,
935
+ signingHeader,
936
+ showEmailExtracts,
937
+ emailFields
938
+ });
939
+ return jsonContent(endpoint);
940
+ }
941
+ )
942
+ );
943
+ server.tool(
944
+ "preview_response",
945
+ "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.",
946
+ {
947
+ slug: import_zod.z.string().describe("The endpoint slug"),
948
+ requestId: import_zod.z.string().optional().describe(
949
+ "A captured request on the endpoint (default: the newest HTTP request, or a sample)"
950
+ ),
951
+ mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
952
+ responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
953
+ },
954
+ withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
955
+ const preview = await client.endpoints.previewResponse(slug, {
956
+ ...requestId ? { requestId } : {},
957
+ ...mockResponse !== void 0 ? { mockResponse } : {},
958
+ ...responseRules !== void 0 ? { responseRules } : {}
959
+ });
960
+ return jsonContent(preview);
961
+ })
962
+ );
963
+ server.tool(
964
+ "delete_endpoint",
965
+ "Delete a webhook endpoint and all its captured requests.",
966
+ deleteEndpointShape,
967
+ withErrorHandling(async ({ slug }) => {
968
+ await client.endpoints.delete(slug);
969
+ return textContent(`Endpoint "${slug}" deleted.`);
970
+ })
971
+ );
972
+ server.tool(
973
+ "create_endpoints",
974
+ "Create multiple webhook endpoints in one call.",
975
+ {
976
+ count: import_zod.z.number().int().min(1).max(20).describe("Number of endpoints to create"),
977
+ namePrefix: import_zod.z.string().optional().describe("Optional prefix for endpoint names"),
978
+ ephemeral: import_zod.z.boolean().optional().describe("Create temporary endpoints that auto-expire"),
979
+ expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"')
980
+ },
981
+ withErrorHandling(async ({ count, namePrefix, ephemeral, expiresIn }) => {
982
+ const endpoints = await Promise.all(
983
+ Array.from(
984
+ { length: count },
985
+ (_, index) => client.endpoints.create({
986
+ name: namePrefix ? `${namePrefix}-${index + 1}` : void 0,
987
+ ephemeral,
988
+ expiresIn
989
+ })
990
+ )
991
+ );
992
+ return jsonContent({ endpoints });
993
+ })
994
+ );
995
+ server.tool(
996
+ "delete_endpoints",
997
+ "Delete multiple webhook endpoints in one call.",
998
+ {
999
+ slugs: import_zod.z.array(import_zod.z.string()).min(1).max(100).describe("Endpoint slugs to delete")
1000
+ },
1001
+ withErrorHandling(async ({ slugs }) => {
1002
+ const settled = await Promise.allSettled(
1003
+ slugs.map(async (slug) => {
1004
+ await client.endpoints.delete(slug);
1005
+ return slug;
1006
+ })
1007
+ );
1008
+ return jsonContent({
1009
+ deleted: settled.filter(
1010
+ (result) => result.status === "fulfilled"
1011
+ ).map((result) => result.value),
1012
+ failed: settled.flatMap(
1013
+ (result, index) => result.status === "rejected" ? [
1014
+ {
1015
+ slug: slugs[index],
1016
+ message: result.reason instanceof Error ? result.reason.message : String(result.reason)
1017
+ }
1018
+ ] : []
1019
+ )
1020
+ });
1021
+ })
1022
+ );
1023
+ server.tool(
1024
+ "send_webhook",
1025
+ "Send a test webhook to a hosted endpoint. Supports provider templates and signing.",
1026
+ {
1027
+ slug: import_zod.z.string().describe("The endpoint slug to send to"),
1028
+ method: methodSchema,
1029
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional().describe("HTTP headers to include"),
1030
+ body: import_zod.z.unknown().optional().describe("Request body"),
1031
+ provider: import_zod.z.enum(import_sdk.TEMPLATE_PROVIDERS).optional().describe("Optional provider template to send with signed headers"),
1032
+ template: import_zod.z.string().optional().describe("Provider-specific template preset"),
1033
+ event: import_zod.z.string().optional().describe("Provider event or topic name"),
1034
+ secret: import_zod.z.string().optional().describe(
1035
+ "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1036
+ )
1037
+ },
1038
+ withErrorHandling(
1039
+ async ({ slug, method, headers, body, provider, template, event, secret }) => {
1040
+ let response;
1041
+ if (provider) {
1042
+ const templateSecret = secret?.trim();
1043
+ if (!templateSecret && import_sdk.TEMPLATE_METADATA[provider]?.secretRequired !== false) {
1044
+ throw new Error(`send_webhook with provider "${provider}" requires a non-empty secret`);
1045
+ }
1046
+ response = await client.endpoints.sendTemplate(slug, {
1047
+ provider,
1048
+ template,
1049
+ event,
1050
+ secret: templateSecret,
1051
+ method,
1052
+ headers,
1053
+ body
1054
+ });
1055
+ } else {
1056
+ response = await client.endpoints.send(slug, { method, headers, body });
1057
+ }
1058
+ const responseBody = await readBodyTruncated(response);
1059
+ return jsonContent({
1060
+ status: response.status,
1061
+ statusText: response.statusText,
1062
+ body: responseBody
1063
+ });
1064
+ }
1065
+ )
1066
+ );
1067
+ server.tool(
1068
+ "list_requests",
1069
+ "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
1070
+ listRequestsShape,
1071
+ withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1072
+ const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1073
+ return jsonContent(requests.map(compactRequest));
1074
+ })
1075
+ );
1076
+ server.tool(
1077
+ "search_requests",
1078
+ "Search captured webhook requests across endpoints using retained full-text search.",
1079
+ {
1080
+ slug: import_zod.z.string().optional().describe("Filter to a specific endpoint slug"),
1081
+ method: import_zod.z.string().optional().describe("Filter by HTTP method"),
1082
+ kind: kindSchema,
1083
+ q: import_zod.z.string().optional().describe("Free-text search across path, body, and headers"),
1084
+ from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
1085
+ to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
1086
+ limit: import_zod.z.number().int().min(1).max(200).default(50).describe("Max results to return"),
1087
+ offset: import_zod.z.number().int().min(0).max(1e4).default(0).describe("Result offset"),
1088
+ order: import_zod.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
1089
+ },
1090
+ withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
1091
+ const results = await client.requests.search({
1092
+ slug,
1093
+ method,
1094
+ kind,
1095
+ q,
1096
+ from,
1097
+ to,
1098
+ limit,
1099
+ offset,
1100
+ order
1101
+ });
1102
+ return jsonContent(results.map(compactRequest));
1103
+ })
1104
+ );
1105
+ server.tool(
1106
+ "count_requests",
1107
+ "Count captured webhook requests that match the given filters.",
1108
+ {
1109
+ slug: import_zod.z.string().optional().describe("Filter to a specific endpoint slug"),
1110
+ method: import_zod.z.string().optional().describe("Filter by HTTP method"),
1111
+ kind: kindSchema,
1112
+ q: import_zod.z.string().optional().describe("Free-text search across path, body, and headers"),
1113
+ from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
1114
+ to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
1115
+ },
1116
+ withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
1117
+ const count = await client.requests.count({ slug, method, kind, q, from, to });
1118
+ return jsonContent({ count });
1119
+ })
1120
+ );
1121
+ server.tool(
1122
+ "get_request",
1123
+ "Get full details for a specific captured request by ID.",
1124
+ getRequestShape,
1125
+ withErrorHandling(async ({ requestId }) => {
1126
+ const request = await client.requests.get(requestId);
1127
+ return jsonContent(compactRequest(request));
1128
+ })
1129
+ );
1130
+ server.tool(
1131
+ "wait_for_request",
1132
+ "Wait for a request to arrive at an endpoint.",
1133
+ waitForRequestShape,
1134
+ withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
1135
+ const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
1136
+ return jsonContent(compactRequest(request));
1137
+ })
1138
+ );
1139
+ server.tool(
1140
+ "wait_for_requests",
1141
+ "Wait for multiple requests to arrive at an endpoint.",
1142
+ {
1143
+ endpointSlug: import_zod.z.string().describe("The endpoint slug to monitor"),
1144
+ count: import_zod.z.number().int().min(1).max(20).describe("Number of requests to collect"),
1145
+ timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
1146
+ pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"'),
1147
+ method: import_zod.z.string().optional().describe("Only collect requests with this HTTP method")
1148
+ },
1149
+ withErrorHandling(async ({ endpointSlug, count, timeout, pollInterval, method }) => {
1150
+ const result = await waitForMultipleRequests(client, endpointSlug, {
1151
+ count,
1152
+ timeout,
1153
+ pollInterval,
1154
+ method
1155
+ });
1156
+ return jsonContent(
1157
+ fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
1158
+ );
1159
+ })
1160
+ );
1161
+ server.tool(
1162
+ "replay_request",
1163
+ "Replay a previously captured request to a target URL.",
1164
+ {
1165
+ requestId: import_zod.z.string().describe("The captured request ID"),
1166
+ targetUrl: httpUrlSchema.describe("The URL to replay the request to")
1167
+ },
1168
+ withErrorHandling(async ({ requestId, targetUrl }) => {
1169
+ const response = await client.requests.replay(requestId, targetUrl);
1170
+ const responseBody = await readBodyTruncated(response);
1171
+ return jsonContent({
1172
+ status: response.status,
1173
+ statusText: response.statusText,
1174
+ body: responseBody
1175
+ });
1176
+ })
1177
+ );
1178
+ server.tool(
1179
+ "render_email",
1180
+ "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.",
1181
+ {
1182
+ requestId: import_zod.z.string().describe("The captured email's request ID"),
1183
+ format: import_zod.z.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
1184
+ mailgunSigningKey: import_zod.z.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
1185
+ targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
1186
+ },
1187
+ withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
1188
+ const rendered = await client.emails.render(requestId, {
1189
+ format,
1190
+ ...mailgunSigningKey ? { mailgunSigningKey } : {}
1191
+ });
1192
+ if (!targetUrl) {
1193
+ return jsonContent(
1194
+ shrinkRendered(rendered, MAX_BODY_SIZE)
1195
+ );
1196
+ }
1197
+ const response = await fetch(targetUrl, {
1198
+ method: "POST",
1199
+ headers: { "content-type": rendered.headers["content-type"] },
1200
+ body: rendered.body,
1201
+ redirect: "manual"
1202
+ });
1203
+ return jsonContent({
1204
+ format,
1205
+ status: response.status,
1206
+ statusText: response.statusText,
1207
+ body: await readBodyTruncated(response)
1208
+ });
1209
+ })
1210
+ );
1211
+ server.tool(
1212
+ "compare_requests",
1213
+ "Compare two captured requests and show the structured differences.",
1214
+ {
1215
+ leftRequestId: import_zod.z.string().describe("The first request ID"),
1216
+ rightRequestId: import_zod.z.string().describe("The second request ID"),
1217
+ ignoreHeaders: import_zod.z.array(import_zod.z.string()).optional().describe("Headers to ignore during comparison")
1218
+ },
1219
+ withErrorHandling(async ({ leftRequestId, rightRequestId, ignoreHeaders }) => {
1220
+ const [leftRequest, rightRequest] = await Promise.all([
1221
+ client.requests.get(leftRequestId),
1222
+ client.requests.get(rightRequestId)
1223
+ ]);
1224
+ const diff = (0, import_sdk.diffRequests)(leftRequest, rightRequest, { ignoreHeaders });
1225
+ return jsonContent(diff);
1226
+ })
1227
+ );
1228
+ server.tool(
1229
+ "generate_payload_schema",
1230
+ "Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from captured request bodies. Several requests of one event teach which fields are optional or nullable, so filter with event (or pass requestIds) and write the result into the codebase instead of hand-typing the payload.",
1231
+ payloadSchemaShape,
1232
+ withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
1233
+ const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
1234
+ return payloadSchemaResult(requests, args);
1235
+ })
1236
+ );
1237
+ server.tool(
1238
+ "extract_from_request",
1239
+ "Extract specific JSON fields from a captured request body.",
1240
+ {
1241
+ requestId: import_zod.z.string().describe("The request ID"),
1242
+ jsonPaths: import_zod.z.array(import_zod.z.string()).min(1).max(50).describe("Dot-notation JSON paths to extract")
1243
+ },
1244
+ withErrorHandling(async ({ requestId, jsonPaths }) => {
1245
+ const request = await client.requests.get(requestId);
1246
+ const extracted = Object.fromEntries(
1247
+ jsonPaths.map((path) => [path, (0, import_sdk.extractJsonField)(request, path) ?? null])
1248
+ );
1249
+ return jsonContent(extracted);
1250
+ })
1251
+ );
1252
+ server.tool(
1253
+ "verify_signature",
1254
+ "Verify the webhook signature on a captured request. When called without provider/secret, returns the server-side pre-computed result (if signing is configured on the endpoint). When called with provider+secret, performs client-side verification.",
1255
+ {
1256
+ requestId: import_zod.z.string().describe("The captured request ID"),
1257
+ provider: import_zod.z.enum(import_sdk.VERIFY_PROVIDERS).optional().describe("Provider whose signature scheme should be verified. Omit to use stored result."),
1258
+ secret: import_zod.z.string().optional().describe(
1259
+ "Provider credential for client-side verification (for example a shared signing secret, Adyen hex HMAC key, or PayPal webhook ID). Required for non-Discord providers."
1260
+ ),
1261
+ publicKey: import_zod.z.string().optional().describe("Discord application public key. Required for provider=discord."),
1262
+ url: httpUrlSchema.optional().describe("Original signed URL. Required for Twilio, Square, and HubSpot verification."),
1263
+ method: import_zod.z.string().optional().describe(
1264
+ "Original HTTP method used when the signature was generated. Used by HubSpot v3 verification; the SDK assumes POST when omitted."
1265
+ )
1266
+ },
1267
+ withErrorHandling(async ({ requestId, provider, secret, publicKey, url, method }) => {
1268
+ const request = await client.requests.get(requestId);
1269
+ if (!provider && !secret && !publicKey) {
1270
+ const verified = request.signatureVerified;
1271
+ if (verified === null || verified === void 0) {
1272
+ return jsonContent({
1273
+ valid: null,
1274
+ details: "No signature verification configured on this endpoint. Pass provider and secret to verify client-side."
1275
+ });
1276
+ }
1277
+ let errorData = null;
1278
+ if (request.signatureError) {
1279
+ try {
1280
+ errorData = JSON.parse(request.signatureError);
1281
+ } catch {
1282
+ errorData = null;
1283
+ }
1284
+ }
1285
+ const isSkipped = !verified && errorData?.code && (errorData.code === "missing_header" || errorData.code === "unsupported");
1286
+ return jsonContent({
1287
+ valid: isSkipped ? null : verified,
1288
+ provider: request.signingProvider ?? null,
1289
+ details: verified ? "Signature verified by server." : isSkipped ? `Verification skipped: ${errorData?.message ?? errorData?.code}` : `Signature invalid: ${request.signatureError ?? "unknown error"}`,
1290
+ error: request.signatureError ?? null,
1291
+ skipped: isSkipped ?? false
1292
+ });
1293
+ }
1294
+ const verificationOptions = ensureVerifyArgs({
1295
+ provider,
1296
+ secret,
1297
+ publicKey,
1298
+ url,
1299
+ method
1300
+ });
1301
+ const result = await (0, import_sdk.verifySignature)(request, verificationOptions);
1302
+ return jsonContent({
1303
+ valid: result.valid,
1304
+ details: result.valid ? "Signature is valid." : "Signature did not match."
1305
+ });
1306
+ })
1307
+ );
1308
+ server.tool(
1309
+ "clear_requests",
1310
+ "Delete captured requests for an endpoint without deleting the endpoint itself.",
1311
+ {
1312
+ slug: import_zod.z.string().describe("The endpoint slug to clear"),
1313
+ before: durationOrTimestampSchema.optional().describe('Only clear requests older than this timestamp or duration like "1h"')
1314
+ },
1315
+ withErrorHandling(async ({ slug, before }) => {
1316
+ await client.requests.clear(slug, { before });
1317
+ return jsonContent({ slug, cleared: true, before: before ?? null });
1318
+ })
1319
+ );
1320
+ server.tool(
1321
+ "send_to",
1322
+ "Send a webhook directly to any URL with optional provider signing.",
1323
+ {
1324
+ url: httpUrlSchema.describe("Target URL"),
1325
+ method: methodSchema,
1326
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional().describe("HTTP headers to include"),
1327
+ body: import_zod.z.unknown().optional().describe("Request body"),
1328
+ provider: import_zod.z.enum(import_sdk.TEMPLATE_PROVIDERS).optional().describe("Optional provider template for signing"),
1329
+ template: import_zod.z.string().optional().describe("Provider-specific template preset"),
1330
+ event: import_zod.z.string().optional().describe("Provider event or topic name"),
1331
+ secret: import_zod.z.string().optional().describe(
1332
+ "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1333
+ )
1334
+ },
1335
+ withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
1336
+ const response = await client.sendTo(url, {
1337
+ method,
1338
+ headers,
1339
+ body,
1340
+ provider,
1341
+ template,
1342
+ event,
1343
+ secret
1344
+ });
1345
+ const responseBody = await readBodyTruncated(response);
1346
+ return jsonContent({
1347
+ status: response.status,
1348
+ statusText: response.statusText,
1349
+ body: responseBody
1350
+ });
1351
+ })
1352
+ );
1353
+ server.tool(
1354
+ "preview_webhook",
1355
+ "Preview a webhook request without sending it. Returns the exact URL, method, headers, and body.",
1356
+ {
1357
+ url: httpUrlSchema.describe("Target URL"),
1358
+ method: methodSchema,
1359
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional().describe("HTTP headers to include"),
1360
+ body: import_zod.z.unknown().optional().describe("Request body"),
1361
+ provider: import_zod.z.enum(import_sdk.TEMPLATE_PROVIDERS).optional().describe("Optional provider template for signing"),
1362
+ template: import_zod.z.string().optional().describe("Provider-specific template preset"),
1363
+ event: import_zod.z.string().optional().describe("Provider event or topic name"),
1364
+ secret: import_zod.z.string().optional().describe(
1365
+ "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1366
+ )
1367
+ },
1368
+ withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
1369
+ const preview = await client.buildRequest(url, {
1370
+ method,
1371
+ headers,
1372
+ body,
1373
+ provider,
1374
+ template,
1375
+ event,
1376
+ secret
1377
+ });
1378
+ return jsonContent(preview);
1379
+ })
1380
+ );
1381
+ server.tool(
1382
+ "list_provider_templates",
1383
+ "List supported webhook providers, templates, and signing metadata.",
1384
+ {
1385
+ provider: import_zod.z.enum(import_sdk.TEMPLATE_PROVIDERS).optional().describe("Filter to a single provider")
1386
+ },
1387
+ withErrorHandling(async ({ provider }) => {
1388
+ if (provider) {
1389
+ return jsonContent([client.templates.get(provider)]);
1390
+ }
1391
+ return jsonContent(
1392
+ client.templates.listProviders().map((name) => client.templates.get(name))
1393
+ );
1394
+ })
1395
+ );
1396
+ server.tool(
1397
+ "get_usage",
1398
+ "Check current request usage, remaining quota, plan, and period end, plus the pooled quota of every subscribed team you belong to.",
1399
+ {},
1400
+ withErrorHandling(async () => {
1401
+ const [usageResult, teamsResult] = await Promise.allSettled([
1402
+ client.usage(),
1403
+ client.teams.list()
1404
+ ]);
1405
+ if (usageResult.status === "rejected") throw usageResult.reason;
1406
+ const usage = usageResult.value;
1407
+ const teams = teamsResult.status === "fulfilled" ? teamsResult.value : [];
1408
+ const teamsError = teamsResult.status === "rejected" ? teamsResult.reason instanceof Error ? teamsResult.reason.message : String(teamsResult.reason) : void 0;
1409
+ return jsonContent({
1410
+ ...usage,
1411
+ periodEnd: usage.periodEnd ? new Date(usage.periodEnd).toISOString() : null,
1412
+ ...teamsError ? { teamsError } : {},
1413
+ teams: teams.filter((team) => !team.suspended).map((team) => ({
1414
+ id: team.id,
1415
+ name: team.name,
1416
+ role: team.role,
1417
+ seats: team.seats,
1418
+ used: team.requestsUsed,
1419
+ limit: team.requestLimit,
1420
+ remaining: Math.max(0, team.requestLimit - team.requestsUsed),
1421
+ periodEnd: team.periodEnd ? new Date(team.periodEnd).toISOString() : null
1422
+ }))
1423
+ });
1424
+ })
1425
+ );
1426
+ server.tool(
1427
+ "test_webhook_flow",
1428
+ "Run a full webhook test flow: create endpoint, optionally mock, send, wait, verify, replay, and clean up.",
1429
+ {
1430
+ provider: import_zod.z.enum(import_sdk.TEMPLATE_PROVIDERS).optional().describe("Optional provider template to use when sending the webhook"),
1431
+ event: import_zod.z.string().optional().describe("Optional provider event or topic name"),
1432
+ secret: import_zod.z.string().optional().describe(
1433
+ "Signing secret. Required when provider is set or signature verification is enabled."
1434
+ ),
1435
+ mockStatus: import_zod.z.number().int().min(100).max(599).optional().describe("Optional mock response status to configure before sending"),
1436
+ targetUrl: httpUrlSchema.optional().describe("Optional URL to replay the captured request to after capture"),
1437
+ verifySignature: import_zod.z.boolean().default(false).describe("Verify the captured request signature after capture"),
1438
+ cleanup: import_zod.z.boolean().default(true).describe("Delete the created endpoint after the flow completes")
1439
+ },
1440
+ withErrorHandling(
1441
+ async ({
1442
+ provider,
1443
+ event,
1444
+ secret,
1445
+ mockStatus,
1446
+ targetUrl,
1447
+ verifySignature: shouldVerify,
1448
+ cleanup
1449
+ }) => {
1450
+ const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s", pollInterval: "1s" });
1451
+ if (mockStatus !== void 0) {
1452
+ flow.setMock({
1453
+ status: mockStatus,
1454
+ body: "",
1455
+ headers: {}
1456
+ });
1457
+ }
1458
+ if (provider) {
1459
+ const templateSecret = secret?.trim();
1460
+ if (!templateSecret && import_sdk.TEMPLATE_METADATA[provider]?.secretRequired !== false) {
1461
+ throw new Error(
1462
+ `test_webhook_flow with provider "${provider}" requires a non-empty secret`
1463
+ );
1464
+ }
1465
+ flow.sendTemplate({
1466
+ provider,
1467
+ event,
1468
+ secret: templateSecret
1469
+ });
1470
+ if (shouldVerify) {
1471
+ if (provider === "discord") {
1472
+ throw new Error(
1473
+ "test_webhook_flow cannot verify Discord signatures (Ed25519 requires a public key, not a secret)"
1474
+ );
1475
+ }
1476
+ if (!templateSecret) {
1477
+ throw new Error(
1478
+ `test_webhook_flow cannot verify "${provider}" signatures \u2014 this provider's templates are unsigned`
1479
+ );
1480
+ }
1481
+ flow.verifySignature({
1482
+ provider,
1483
+ secret: templateSecret
1484
+ });
1485
+ }
1486
+ } else {
1487
+ if (shouldVerify) {
1488
+ throw new Error("test_webhook_flow cannot verify signatures without a provider");
1489
+ }
1490
+ flow.send();
1491
+ }
1492
+ if (targetUrl) {
1493
+ flow.replayTo(targetUrl);
1494
+ }
1495
+ if (cleanup) {
1496
+ flow.cleanup();
1497
+ }
1498
+ const result = await flow.run();
1499
+ return jsonContent({
1500
+ endpoint: result.endpoint,
1501
+ request: result.request ?? null,
1502
+ verification: result.verification ?? null,
1503
+ replayResponse: result.replayResponse ? await summarizeResponse(result.replayResponse) : null,
1504
+ cleanedUp: result.cleanedUp
1505
+ });
1506
+ }
1507
+ )
1508
+ );
1509
+ server.tool(
1510
+ "list_emails",
1511
+ "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.",
1512
+ {
1513
+ endpointSlug: import_zod.z.string().describe("The endpoint slug"),
1514
+ limit: import_zod.z.number().int().min(1).max(100).default(25).describe(
1515
+ `Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
1516
+ ),
1517
+ since: import_zod.z.number().optional().describe("Only emails received after this timestamp in ms"),
1518
+ tag: import_zod.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
1519
+ subject: import_zod.z.string().optional().describe("Only emails whose subject contains this text"),
1520
+ from: import_zod.z.string().optional().describe("Only emails from this address (case-insensitive)")
1521
+ },
1522
+ withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
1523
+ const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
1524
+ const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
1525
+ const [emails, view] = await Promise.all([
1526
+ client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
1527
+ emailEndpointView(client, endpointSlug)
1528
+ ]);
1529
+ return jsonContent(
1530
+ emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
1531
+ );
1532
+ })
1533
+ );
1534
+ server.tool(
1535
+ "get_email",
1536
+ "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.",
1537
+ {
1538
+ requestId: import_zod.z.string().optional().describe("The email's request ID"),
1539
+ endpointSlug: import_zod.z.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
1540
+ tag: import_zod.z.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
1541
+ includeHtml: import_zod.z.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
1542
+ },
1543
+ withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
1544
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1545
+ throw new Error("Pass exactly one of requestId or endpointSlug");
1546
+ }
1547
+ const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
1548
+ if (!email) {
1549
+ throw new import_sdk.NotFoundError(
1550
+ `No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
1551
+ );
1552
+ }
1553
+ const view = await emailEndpointView(
1554
+ client,
1555
+ endpointSlug ?? emailSlugFromAddress(email.path)
1556
+ );
1557
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1558
+ })
1559
+ );
1560
+ server.tool(
1561
+ "wait_for_email",
1562
+ "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.",
1563
+ {
1564
+ endpointSlug: import_zod.z.string().describe("The endpoint slug"),
1565
+ tag: import_zod.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
1566
+ subject: import_zod.z.string().optional().describe("Only emails whose subject contains this text"),
1567
+ from: import_zod.z.string().optional().describe("Only emails from this address (case-insensitive)"),
1568
+ timeout: durationOrTimestampSchema.default("30s").describe(
1569
+ `How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
1570
+ ),
1571
+ since: import_zod.z.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
1572
+ includeHtml: import_zod.z.boolean().default(false).describe("Include the HTML part (default: false)")
1573
+ },
1574
+ withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1575
+ const view = await emailEndpointView(client, endpointSlug);
1576
+ const email = await client.emails.waitFor(endpointSlug, {
1577
+ tag,
1578
+ subject,
1579
+ from,
1580
+ timeout,
1581
+ since
1582
+ });
1583
+ return jsonContent(emailDetail(client, email, view, includeHtml));
1584
+ })
1585
+ );
1586
+ server.tool(
1587
+ "send_test_email",
1588
+ "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.",
1589
+ {
1590
+ slug: import_zod.z.string().describe("The endpoint slug"),
1591
+ tag: import_zod.z.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
1592
+ },
1593
+ withErrorHandling(async ({ slug, tag }) => {
1594
+ const result = await client.emails.sendTest(slug, { tag });
1595
+ return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1596
+ })
1597
+ );
1598
+ server.tool(
1599
+ "list_destinations",
1600
+ "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.",
1601
+ {
1602
+ slug: import_zod.z.string().describe("The endpoint slug"),
1603
+ summary: import_zod.z.boolean().default(true).describe("Add each destination's last 24 hours")
1604
+ },
1605
+ withErrorHandling(async ({ slug, summary }) => {
1606
+ return jsonContent(await client.destinations.list(slug, { summary }));
1607
+ })
1608
+ );
1609
+ server.tool(
1610
+ "add_destination",
1611
+ "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.",
1612
+ {
1613
+ slug: import_zod.z.string().describe("The endpoint slug"),
1614
+ url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
1615
+ ...destinationFields
1616
+ },
1617
+ withErrorHandling(async ({ slug, ...input }) => {
1618
+ return jsonContent(await client.destinations.create(slug, input));
1619
+ })
1620
+ );
1621
+ server.tool(
1622
+ "update_destination",
1623
+ "Change a destination: any of its fields; only the ones you pass change. Owner only.",
1624
+ {
1625
+ slug: import_zod.z.string().describe("The endpoint slug"),
1626
+ destinationId: import_zod.z.string().describe("The destination's id (from list_destinations)"),
1627
+ url: httpUrlSchema.optional().describe("A new URL"),
1628
+ ...destinationFields
1629
+ },
1630
+ withErrorHandling(async ({ slug, destinationId, ...input }) => {
1631
+ return jsonContent(await client.destinations.update(slug, destinationId, input));
1632
+ })
1633
+ );
1634
+ server.tool(
1635
+ "remove_destination",
1636
+ "Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
1637
+ {
1638
+ slug: import_zod.z.string().describe("The endpoint slug"),
1639
+ destinationId: import_zod.z.string().describe("The destination's id")
1640
+ },
1641
+ withErrorHandling(async ({ slug, destinationId }) => {
1642
+ await client.destinations.delete(slug, destinationId);
1643
+ return textContent(`Destination ${destinationId} removed.`);
1644
+ })
1645
+ );
1646
+ server.tool(
1647
+ "preview_destination",
1648
+ "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.",
1649
+ {
1650
+ slug: import_zod.z.string().describe("The endpoint slug"),
1651
+ destinationId: import_zod.z.string().optional().describe("A saved destination"),
1652
+ destination: import_zod.z.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
1653
+ requestId: import_zod.z.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
1654
+ },
1655
+ withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
1656
+ if (!destinationId && !destination?.url) {
1657
+ throw new Error("Pass destinationId, or a destination with a url");
1658
+ }
1659
+ return jsonContent(
1660
+ await client.destinations.preview(slug, {
1661
+ ...destinationId ? { destinationId } : {},
1662
+ ...destination ? { destination } : {},
1663
+ ...requestId ? { requestId } : {}
1664
+ })
1665
+ );
1666
+ })
1667
+ );
1668
+ server.tool(
1669
+ "configure_forwarding",
1670
+ "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.",
1671
+ {
1672
+ slug: import_zod.z.string().describe("The endpoint slug"),
1673
+ url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1674
+ enabled: import_zod.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)"),
1675
+ http: import_zod.z.boolean().optional().describe("Forward HTTP requests"),
1676
+ email: import_zod.z.boolean().optional().describe("Forward emails"),
1677
+ format: import_zod.z.enum(["auto", "as_received", "json", "chat"]).optional().describe(
1678
+ "auto (pick from the URL), as_received (relay HTTP requests unchanged; emails as signed JSON), json (signed JSON envelope for both kinds) or chat (Slack or Discord message)"
1679
+ ),
1680
+ headers: import_zod.z.array(
1681
+ import_zod.z.object({
1682
+ name: import_zod.z.string().describe("Header name, e.g. Authorization"),
1683
+ value: import_zod.z.string().nullable().describe("Header value, or null to keep the value stored under this name")
1684
+ })
1685
+ ).max(10).optional().describe(
1686
+ "Your own headers for every delivery (not chat). Replaces the whole list; [] removes them"
1687
+ ),
1688
+ appendPath: import_zod.z.boolean().optional().describe("Add the path after the slug to the URL for relayed requests (default true)"),
1689
+ retrySeconds: import_zod.z.union([import_zod.z.literal(0), import_zod.z.literal(3600), import_zod.z.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
1690
+ keepOrder: import_zod.z.boolean().optional().describe("One delivery at a time, in capture order (default false)"),
1691
+ sentField: import_zod.z.string().max(128).nullable().optional().describe(
1692
+ "JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
1693
+ )
1694
+ },
1695
+ withErrorHandling(async ({ slug, ...options2 }) => {
1696
+ const endpoint = await client.forwarding.configure(slug, options2);
1697
+ return jsonContent({
1698
+ slug: endpoint.slug,
1699
+ forwardEnabled: endpoint.forwardEnabled ?? false,
1700
+ forwardUrl: endpoint.forwardUrl ?? null,
1701
+ hasForwardSecret: endpoint.hasForwardSecret ?? false,
1702
+ forwardHttp: endpoint.forwardHttp ?? false,
1703
+ forwardEmail: endpoint.forwardEmail ?? false,
1704
+ forwardFormat: endpoint.forwardFormat ?? "auto",
1705
+ forwardHeaders: endpoint.forwardHeaders ?? [],
1706
+ forwardAppendPath: endpoint.forwardAppendPath ?? true,
1707
+ forwardRetrySeconds: endpoint.forwardRetrySeconds ?? 86400,
1708
+ forwardKeepOrder: endpoint.forwardKeepOrder ?? false,
1709
+ forwardSentField: endpoint.forwardSentField ?? null
1710
+ });
1711
+ })
1712
+ );
1713
+ server.tool(
1714
+ "get_forwarding_secret",
1715
+ "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.",
1716
+ {
1717
+ slug: import_zod.z.string().describe("The endpoint slug"),
1718
+ destinationId: import_zod.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1719
+ rotate: import_zod.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1720
+ },
1721
+ withErrorHandling(async ({ slug, destinationId, rotate }) => {
1722
+ 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);
1723
+ return jsonContent({ secret, rotated: rotate });
1724
+ })
1725
+ );
1726
+ server.tool(
1727
+ "test_forwarding",
1728
+ "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.",
1729
+ {
1730
+ slug: import_zod.z.string().describe("The endpoint slug"),
1731
+ destinationId: import_zod.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1732
+ kind: import_zod.z.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
1733
+ requestId: import_zod.z.string().optional().describe("With destinationId: a captured request to send")
1734
+ },
1735
+ withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
1736
+ if (destinationId) {
1737
+ return jsonContent(
1738
+ await client.destinations.test(slug, destinationId, {
1739
+ ...kind ? { kind } : {},
1740
+ ...requestId ? { requestId } : {}
1741
+ })
1742
+ );
1743
+ }
1744
+ return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1745
+ })
1746
+ );
1747
+ server.tool(
1748
+ "list_deliveries",
1749
+ "List forwarding deliveries: an endpoint's delivery log with its counts (endpointSlug), or every delivery and try of one captured request or email (requestId). Rows carry senderAt when the request had the sender's own timestamp, so receivedAt - senderAt is the time to reach webhooks.cc.",
1750
+ {
1751
+ endpointSlug: import_zod.z.string().optional().describe("List the endpoint's delivery log"),
1752
+ requestId: import_zod.z.string().optional().describe("List every delivery and try of this request"),
1753
+ limit: import_zod.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1754
+ status: import_zod.z.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
1755
+ "With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
1756
+ ),
1757
+ destinationId: import_zod.z.string().optional().describe("With endpointSlug: only this destination"),
1758
+ before: import_zod.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1759
+ },
1760
+ withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
1761
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1762
+ throw new Error("Pass exactly one of endpointSlug or requestId");
1763
+ }
1764
+ if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1765
+ const [summary, deliveries] = await Promise.all([
1766
+ client.forwarding.summary(endpointSlug),
1767
+ client.forwarding.deliveries(endpointSlug, {
1768
+ limit,
1769
+ status,
1770
+ before,
1771
+ ...destinationId ? { destinationId } : {}
1772
+ })
1773
+ ]);
1774
+ return jsonContent({
1775
+ summary,
1776
+ deliveries,
1777
+ nextCursor: deliveries.length === limit ? deliveries[deliveries.length - 1]?.cursor ?? null : null
1778
+ });
1779
+ })
1780
+ );
1781
+ server.tool(
1782
+ "redeliver",
1783
+ "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.",
1784
+ {
1785
+ requestId: import_zod.z.string().optional().describe("The request or email to send again"),
1786
+ endpointSlug: import_zod.z.string().optional().describe("Send every failed delivery of this endpoint again"),
1787
+ destinationId: import_zod.z.string().optional().describe("Only this destination")
1788
+ },
1789
+ withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
1790
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1791
+ throw new Error("Pass exactly one of requestId or endpointSlug");
1792
+ }
1793
+ const options2 = destinationId ? { destinationId } : {};
1794
+ return jsonContent(
1795
+ requestId ? await client.forwarding.redeliver(requestId, options2) : await client.forwarding.redeliverFailed(endpointSlug, options2)
1796
+ );
1797
+ })
1798
+ );
1799
+ server.tool(
1800
+ "describe",
1801
+ "Describe all available SDK operations, parameters, and types.",
1802
+ {},
1803
+ withErrorHandling(async () => {
1804
+ const description = client.describe();
1805
+ return jsonContent(description);
1806
+ })
1807
+ );
1808
+ return handles;
1809
+ }
1810
+ // Annotate the CommonJS export names for ESM import in node:
1811
+ 0 && (module.exports = {
1812
+ REMOTE_MAX_WAIT_MS,
1813
+ REMOTE_MIN_POLL_MS,
1814
+ createEndpointShape,
1815
+ deleteEndpointShape,
1816
+ getEndpointShape,
1817
+ getRequestShape,
1818
+ jsonContent,
1819
+ listEndpointsShape,
1820
+ listRequestsShape,
1821
+ payloadSchemaResult,
1822
+ payloadSchemaShape,
1823
+ registerTools,
1824
+ textContent,
1825
+ waitForRequestShape,
1826
+ withErrorHandling
1827
+ });