@webhooks-cc/mcp 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,23 @@
1
+ import {
2
+ REMOTE_MAX_WAIT_MS,
3
+ compactRequest,
4
+ createEndpointShape,
5
+ deleteEndpointShape,
6
+ getEndpointShape,
7
+ getRequestShape,
8
+ jsonContent,
9
+ listEndpointsShape,
10
+ listRequestsShape,
11
+ payloadSchemaResult,
12
+ payloadSchemaShape,
13
+ registerTools,
14
+ waitForRequestShape,
15
+ withErrorHandling
16
+ } from "./chunk-RHQD2GRD.mjs";
17
+
1
18
  // src/index.ts
2
19
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { WebhooksCC as WebhooksCC3 } from "@webhooks-cc/sdk";
20
+ import { WebhooksCC as WebhooksCC2 } from "@webhooks-cc/sdk";
4
21
 
5
22
  // src/prompts.ts
6
23
  import { z } from "zod";
@@ -115,158 +132,6 @@ function registerPrompts(server) {
115
132
 
116
133
  // src/resources.ts
117
134
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
118
-
119
- // src/compact.ts
120
- var MAX_OUTPUT = 32768;
121
- var MAX_EMAIL_TEXT = 8e3;
122
- var MAX_EXTRACTS = 10;
123
- var MAX_LINK_TEXT = 200;
124
- var MAX_LINK_URL = 2e3;
125
- var MAX_LIST_ITEMS = 5;
126
- var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
127
- var TEXT_FLOOR = 2e3;
128
- function jsonSize(value) {
129
- return JSON.stringify(value, null, 2).length;
130
- }
131
- function sliceText(text, end) {
132
- if (end <= 0) return "";
133
- if (end >= text.length) return text;
134
- const last = text.charCodeAt(end - 1);
135
- return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
136
- }
137
- function cutEmailText(text) {
138
- if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
139
- return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
140
- }
141
- function shrinkToFit(root, budget, steps) {
142
- for (const step of steps) {
143
- if (jsonSize(root) <= budget) return;
144
- step();
145
- }
146
- }
147
- function omitHeaders(holder) {
148
- const headers = holder.headers;
149
- if (headers === null || typeof headers !== "object") return;
150
- const count = Object.keys(headers).length;
151
- if (count === 0) return;
152
- holder.headers = {};
153
- holder.headersOmitted = `${count} headers left out to fit the output`;
154
- }
155
- function trimSpam(holder) {
156
- const spam = holder.spam;
157
- if (spam && typeof spam === "object" && !Array.isArray(spam)) {
158
- const { score, required, action, error } = spam;
159
- holder.spam = { score, required, action, error };
160
- }
161
- }
162
- function capExtracts(holder) {
163
- if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
164
- holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
165
- holder.codesTruncated = true;
166
- }
167
- if (Array.isArray(holder.links)) {
168
- if (holder.links.length > MAX_EXTRACTS) {
169
- holder.links = holder.links.slice(0, MAX_EXTRACTS);
170
- holder.linksTruncated = true;
171
- }
172
- holder.links = holder.links.map((link) => ({
173
- ...link,
174
- ...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
175
- ...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
176
- }));
177
- }
178
- }
179
- function capLists(holder) {
180
- for (const key of EMAIL_LISTS) {
181
- const list = holder[key];
182
- if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
183
- holder[key] = list.slice(0, MAX_LIST_ITEMS);
184
- holder[`${key}Total`] = list.length;
185
- }
186
- }
187
- }
188
- function keepOnly(holder, keep) {
189
- for (const key of Object.keys(holder)) {
190
- if (!keep.includes(key)) delete holder[key];
191
- }
192
- holder.trimmed = "The email was too big for the output, so only these fields are shown.";
193
- }
194
- function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
195
- const value = holder[key];
196
- if (typeof value !== "string" || value.length <= floor) return;
197
- const flagged = holder[flag] === true;
198
- holder[key] = "";
199
- holder[flag] = true;
200
- const room = budget - jsonSize(root) + 2;
201
- let low = 0;
202
- let high = value.length;
203
- while (low < high) {
204
- const mid = Math.ceil((low + high) / 2);
205
- if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
206
- else high = mid - 1;
207
- }
208
- const kept = sliceText(value, Math.max(low, floor));
209
- holder[key] = kept;
210
- if (kept.length === value.length && !flagged) delete holder[flag];
211
- }
212
- function keepFirst(holder, key) {
213
- const list = holder[key];
214
- if (!Array.isArray(list) || list.length <= 1) return;
215
- holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
216
- holder[key] = list.slice(0, 1);
217
- }
218
- var ESSENTIAL_EMAIL_FIELDS = [
219
- "subject",
220
- "from",
221
- "fromTotal",
222
- "tag",
223
- "date",
224
- "text",
225
- "textTruncated",
226
- "htmlSize",
227
- "spam"
228
- ];
229
- function compactRequest(request) {
230
- if (request.kind !== "email") return request;
231
- const compact = { ...request };
232
- delete compact.body;
233
- delete compact.bodyRaw;
234
- compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
235
- if (!request.email) return compact;
236
- const { html, ...rest } = request.email;
237
- const { text, cut } = cutEmailText(rest.text);
238
- const restoreText = () => {
239
- email.text = text;
240
- if (cut) email.textTruncated = true;
241
- else delete email.textTruncated;
242
- };
243
- const email = {
244
- ...rest,
245
- text,
246
- ...cut ? { textTruncated: true } : {},
247
- htmlSize: html?.length ?? 0
248
- };
249
- compact.email = email;
250
- const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
251
- const budget = MAX_OUTPUT - 128;
252
- shrinkToFit(root, budget, [
253
- () => omitHeaders(compact),
254
- () => trimSpam(email),
255
- () => capLists(email),
256
- () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
257
- // Down to the essentials there may be room for more of the text again.
258
- () => {
259
- keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
260
- keepFirst(email, "from");
261
- restoreText();
262
- cutStringToFit(root, email, "text", "textTruncated", budget);
263
- },
264
- () => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
265
- ]);
266
- return compact;
267
- }
268
-
269
- // src/resources.ts
270
135
  var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
271
136
  var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
272
137
  var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
@@ -388,1594 +253,13 @@ function registerResources(server, client) {
388
253
  }
389
254
 
390
255
  // src/sandbox-tools.ts
391
- import { z as z3 } from "zod";
392
- import {
393
- AgentAuthError,
394
- WebhooksCC as WebhooksCC2
395
- } from "@webhooks-cc/sdk";
396
-
397
- // src/tools.ts
398
256
  import { z as z2 } from "zod";
399
257
  import {
400
- diffRequests,
401
- extractJsonField,
402
- MOCK_RESPONSE_DELAY_MAX,
403
- MOCK_RESPONSE_DELAY_MIN,
404
- MOCK_RESPONSE_STATUS_MAX,
405
- MOCK_RESPONSE_STATUS_MIN,
406
- MAX_CONDITION_VALUE_LEN,
407
- MAX_CONDITION_NAME_LEN,
408
- MAX_CONDITION_PATH_LEN,
409
- MAX_RULE_NAME_LEN,
410
- NotFoundError,
411
- RateLimitError,
412
- TEMPLATE_METADATA,
413
- TEMPLATE_PROVIDERS,
414
- TimeoutError,
415
- UnauthorizedError,
416
- VERIFY_PROVIDERS,
417
- verifySignature,
418
- WebhooksCCError,
419
- extractCode,
420
- extractLink,
421
- detectWebhookInfo,
422
- generateRequestSchema,
423
- PAYLOAD_SCHEMA_FORMATS,
424
- CONDITION_FIELDS,
425
- CONDITION_OPS,
426
- conditionIssues
258
+ AgentAuthError,
259
+ AgentCredentialStore,
260
+ SandboxClient,
261
+ WebhooksCC
427
262
  } from "@webhooks-cc/sdk";
428
- var MAX_BODY_SIZE = MAX_OUTPUT;
429
- var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
430
- var TIME_SEPARATOR = " \u2014 ";
431
- var httpUrlSchema = z2.string().url().refine(
432
- (value) => {
433
- try {
434
- const protocol = new URL(value).protocol;
435
- return protocol === "http:" || protocol === "https:";
436
- } catch {
437
- return false;
438
- }
439
- },
440
- { message: "Only http and https URLs are supported" }
441
- );
442
- var methodSchema = z2.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
443
- var kindSchema = z2.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
444
- var EMAIL_SCAN_LIMIT = 100;
445
- var durationOrTimestampSchema = z2.union([z2.string(), z2.number()]);
446
- var CONDITION_FIELD_NAMES = Object.keys(CONDITION_FIELDS);
447
- var CONDITION_OP_NAMES = CONDITION_OPS;
448
- function conditionSchema(use) {
449
- const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
450
- return z2.object({
451
- field: z2.enum(fields).describe(
452
- 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)"
453
- ),
454
- op: z2.enum(CONDITION_OP_NAMES).describe(
455
- "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)"
456
- ),
457
- value: z2.string().max(MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
458
- name: z2.string().max(MAX_CONDITION_NAME_LEN).optional().describe("Header or query parameter name; the email part; spf, dkim or dmarc"),
459
- path: z2.string().max(MAX_CONDITION_PATH_LEN).optional().describe('Dot path for body_path, e.g. data.object.status or items.0.sku or a["odd.key"]')
460
- }).superRefine((condition, ctx) => {
461
- for (const issue of conditionIssues(condition, { use })) {
462
- ctx.addIssue({ code: "custom", message: issue });
463
- }
464
- });
465
- }
466
- var ruleConditionSchema = conditionSchema("response");
467
- var destinationFields = {
468
- name: z2.string().max(100).nullable().optional().describe("A name for the list and the log"),
469
- enabled: z2.boolean().optional().describe("Send to it (default true)"),
470
- http: z2.boolean().optional().describe("Takes HTTP requests (default true)"),
471
- email: z2.boolean().optional().describe("Takes emails (default true)"),
472
- filter: z2.object({
473
- logic: z2.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
474
- conditions: z2.array(conditionSchema("filter")).max(10)
475
- }).nullable().optional().describe(
476
- "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)"
477
- ),
478
- format: z2.enum([
479
- "auto",
480
- "as_received",
481
- "json",
482
- "template",
483
- "chat",
484
- "postmark",
485
- "sendgrid",
486
- "mailgun",
487
- "cloudmailin"
488
- ]).optional().describe(
489
- "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"
490
- ),
491
- mailgunSigningKey: z2.string().max(200).nullable().optional().describe(
492
- "For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
493
- ),
494
- bodyTemplate: z2.unknown().optional().describe(
495
- `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)`
496
- ),
497
- messageTemplate: z2.string().max(4e3).nullable().optional().describe(
498
- "For format chat: the message text with {{ placeholders }}; null sends the standard message"
499
- ),
500
- headers: z2.array(
501
- z2.object({
502
- name: z2.string().describe("Header name, e.g. Authorization"),
503
- value: z2.string().nullable().describe("Value, or null to keep the stored one")
504
- })
505
- ).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
506
- appendPath: z2.boolean().optional().describe("Append the captured path to the URL (as_received)"),
507
- retrySeconds: z2.union([z2.literal(0), z2.literal(3600), z2.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
508
- keepOrder: z2.boolean().optional().describe("One delivery at a time, in capture order"),
509
- sentField: z2.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
510
- throttleSeconds: z2.number().int().min(0).max(3600).optional().describe(
511
- "At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
512
- ),
513
- delaySeconds: z2.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
514
- };
515
- var responseRuleSchema = z2.object({
516
- name: z2.string().max(MAX_RULE_NAME_LEN).optional().describe(`Human-readable rule name (max ${MAX_RULE_NAME_LEN} chars)`),
517
- enabled: z2.boolean().optional().default(true).describe("Whether this rule is active"),
518
- logic: z2.enum(["and", "or"]).optional().default("and").describe('How to combine conditions: "and" (all match) or "or" (any match)'),
519
- conditions: z2.array(ruleConditionSchema).min(1).max(10).describe("Conditions to evaluate (1-10)"),
520
- response: z2.lazy(() => mockResponseSchema).describe("Response when conditions match")
521
- });
522
- var responseRulesSchema = z2.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
523
- var mockResponseSchema = z2.object({
524
- status: z2.number().int().min(MOCK_RESPONSE_STATUS_MIN).max(MOCK_RESPONSE_STATUS_MAX).describe(`HTTP status code (${MOCK_RESPONSE_STATUS_MIN}-${MOCK_RESPONSE_STATUS_MAX})`),
525
- body: z2.string().default("").describe(
526
- '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'
527
- ),
528
- headers: z2.record(z2.string(), z2.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
529
- delay: z2.number().int().min(MOCK_RESPONSE_DELAY_MIN).max(MOCK_RESPONSE_DELAY_MAX).optional().describe(
530
- `Response delay in milliseconds (${MOCK_RESPONSE_DELAY_MIN}-${MOCK_RESPONSE_DELAY_MAX}, default: none)`
531
- )
532
- });
533
- var createEndpointShape = {
534
- name: z2.string().optional().describe("Display name for the endpoint"),
535
- ephemeral: z2.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
536
- expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"'),
537
- mockResponse: mockResponseSchema.optional().describe("Optional mock response to return when the endpoint receives a request"),
538
- responseRules: responseRulesSchema.optional().describe(
539
- "Conditional response rules. Each rule has conditions and a response. First matching rule wins."
540
- ),
541
- notificationUrl: z2.string().url().optional().describe(
542
- "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
543
- ),
544
- signingProvider: z2.string().optional().describe(
545
- "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
546
- ),
547
- signingSecret: z2.string().optional().describe(
548
- "Signing secret (encrypted server-side, never returned). Required when signingProvider is set."
549
- ),
550
- signingHeader: z2.string().optional().describe("Custom signature header name. Only used with generic-hmac provider.")
551
- };
552
- var listEndpointsShape = {
553
- team: z2.string().min(1).optional().describe(
554
- "Team id or name (case-insensitive). Keeps endpoints shared with you from that team and endpoints you own that are shared with it."
555
- )
556
- };
557
- var getEndpointShape = { slug: z2.string().describe("The endpoint slug") };
558
- var deleteEndpointShape = { slug: z2.string().describe("The endpoint slug to delete") };
559
- var listRequestsShape = {
560
- endpointSlug: z2.string().describe("The endpoint slug"),
561
- limit: z2.number().int().min(1).max(100).default(25).describe("Max requests to return"),
562
- since: z2.number().optional().describe("Only return requests after this timestamp in ms"),
563
- kind: kindSchema
564
- };
565
- var getRequestShape = { requestId: z2.string().describe("The request ID") };
566
- var waitForRequestShape = {
567
- endpointSlug: z2.string().describe("The endpoint slug to monitor"),
568
- timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
569
- pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
570
- };
571
- var payloadSchemaShape = {
572
- endpointSlug: z2.string().describe("The endpoint slug"),
573
- format: z2.enum(PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
574
- 'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
575
- ),
576
- name: z2.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
577
- event: z2.string().optional().describe(
578
- 'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
579
- ),
580
- method: z2.string().optional().describe("Only requests with this HTTP method"),
581
- path: z2.string().optional().describe("Only requests with this path"),
582
- requestIds: z2.array(z2.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
583
- limit: z2.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
584
- };
585
- function eventName(event) {
586
- const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
587
- const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
588
- if (!name) return "WebhookPayload";
589
- return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
590
- }
591
- function payloadSchemaResult(requests, args) {
592
- const method = args.method?.toUpperCase();
593
- const matching = requests.filter(
594
- (request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args.path || request.path === args.path) && (!args.event || detectWebhookInfo(request)?.event === args.event)
595
- );
596
- if (matching.length === 0) {
597
- throw new Error(
598
- `None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
599
- );
600
- }
601
- const events = [
602
- ...new Set(
603
- matching.map((request) => detectWebhookInfo(request)?.event).filter((e) => !!e)
604
- )
605
- ];
606
- const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
607
- const result = generateRequestSchema(matching, { format: args.format, name });
608
- if (result.samples === 0) {
609
- throw new Error(
610
- `None of the ${matching.length} matching requests has a JSON or form body to learn from.`
611
- );
612
- }
613
- const output = {
614
- format: result.format,
615
- name: result.name,
616
- samples: result.samples,
617
- skipped: result.skipped,
618
- ...events.length > 0 ? { events } : {},
619
- ...events.length > 1 ? {
620
- warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
621
- } : {},
622
- code: result.code
623
- };
624
- const size = JSON.stringify(output, null, 2).length;
625
- if (size > MAX_OUTPUT) {
626
- throw new Error(
627
- `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.`
628
- );
629
- }
630
- return jsonContent(output);
631
- }
632
- function textContent(text) {
633
- return { content: [{ type: "text", text }] };
634
- }
635
- function serializeJson(value, limit = MAX_BODY_SIZE) {
636
- const full = JSON.stringify(value, null, 2);
637
- if (full.length <= limit) {
638
- return full;
639
- }
640
- if (Array.isArray(value)) {
641
- let low = 0;
642
- let high = value.length;
643
- let best = JSON.stringify(
644
- { items: [], truncated: true, total: value.length, returned: 0 },
645
- null,
646
- 2
647
- );
648
- while (low <= high) {
649
- const mid = Math.floor((low + high) / 2);
650
- const candidate = JSON.stringify(
651
- {
652
- items: value.slice(0, mid),
653
- truncated: true,
654
- total: value.length,
655
- returned: mid
656
- },
657
- null,
658
- 2
659
- );
660
- if (candidate.length <= limit) {
661
- best = candidate;
662
- low = mid + 1;
663
- } else {
664
- high = mid - 1;
665
- }
666
- }
667
- return best;
668
- }
669
- return full.slice(0, limit) + `
670
- ... [truncated, ${full.length} chars total]`;
671
- }
672
- function fitArrayField(value, key) {
673
- const items = value[key];
674
- if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
675
- const cut = (count) => ({
676
- ...value,
677
- [key]: items.slice(0, count),
678
- truncated: true,
679
- returned: count
680
- });
681
- let low = 0;
682
- let high = items.length;
683
- while (low < high) {
684
- const mid = Math.ceil((low + high) / 2);
685
- if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
686
- else high = mid - 1;
687
- }
688
- return cut(low);
689
- }
690
- function jsonContent(value) {
691
- return textContent(serializeJson(value));
692
- }
693
- async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
694
- const text = await response.text();
695
- if (text.length <= limit) return text;
696
- return text.slice(0, limit) + `
697
- ... [truncated, ${text.length} chars total]`;
698
- }
699
- function sleep(ms) {
700
- return new Promise((resolve) => setTimeout(resolve, ms));
701
- }
702
- function splitHint(message) {
703
- const separatorIndex = message.indexOf(TIME_SEPARATOR);
704
- if (separatorIndex === -1) {
705
- return { message, hint: null };
706
- }
707
- return {
708
- message: message.slice(0, separatorIndex),
709
- hint: message.slice(separatorIndex + TIME_SEPARATOR.length) || null
710
- };
711
- }
712
- function serializeError(error) {
713
- const rawMessage = error instanceof Error ? error.message : String(error);
714
- const { message, hint } = splitHint(rawMessage);
715
- const payload = {
716
- error: true,
717
- code: "validation_error",
718
- message,
719
- hint,
720
- retryAfter: null
721
- };
722
- if (error instanceof UnauthorizedError) {
723
- payload.code = "unauthorized";
724
- } else if (error instanceof NotFoundError) {
725
- payload.code = "not_found";
726
- } else if (error instanceof RateLimitError) {
727
- payload.code = "rate_limited";
728
- payload.retryAfter = error.retryAfter ?? null;
729
- } else if (error instanceof TimeoutError) {
730
- payload.code = "timeout";
731
- } else if (error instanceof WebhooksCCError) {
732
- payload.code = error.statusCode >= 500 ? "server_error" : "validation_error";
733
- }
734
- return JSON.stringify(payload, null, 2);
735
- }
736
- function withErrorHandling(handler) {
737
- return async (args) => {
738
- try {
739
- return await handler(args);
740
- } catch (error) {
741
- return { ...textContent(serializeError(error)), isError: true };
742
- }
743
- };
744
- }
745
- function filterRequestsByMethod(requests, method) {
746
- if (!method) {
747
- return requests;
748
- }
749
- const target = method.toUpperCase();
750
- return requests.filter((request) => request.method.toUpperCase() === target);
751
- }
752
- async function waitForMultipleRequests(client, endpointSlug, options) {
753
- const timeoutMs = typeof options.timeout === "number" ? options.timeout : options.timeout ? Number.isNaN(Number(options.timeout)) ? parseDurationLike(options.timeout) : Number(options.timeout) : 3e4;
754
- const pollIntervalMs = typeof options.pollInterval === "number" ? options.pollInterval : options.pollInterval ? Number.isNaN(Number(options.pollInterval)) ? parseDurationLike(options.pollInterval) : Number(options.pollInterval) : 500;
755
- const startedAt = Date.now();
756
- let since = startedAt;
757
- const seenIds = /* @__PURE__ */ new Set();
758
- const requests = [];
759
- while (Date.now() - startedAt < timeoutMs) {
760
- const checkTime = Date.now();
761
- const page = await client.requests.list(endpointSlug, {
762
- since,
763
- limit: Math.max(100, options.count * 5)
764
- });
765
- since = checkTime;
766
- const filtered = filterRequestsByMethod(page, options.method).slice().sort((left, right) => left.receivedAt - right.receivedAt);
767
- for (const request of filtered) {
768
- if (seenIds.has(request.id)) {
769
- continue;
770
- }
771
- seenIds.add(request.id);
772
- requests.push(request);
773
- if (requests.length >= options.count) {
774
- return {
775
- requests,
776
- complete: true,
777
- timedOut: false,
778
- expectedCount: options.count
779
- };
780
- }
781
- }
782
- await sleep(Math.max(10, pollIntervalMs));
783
- }
784
- return {
785
- requests,
786
- complete: requests.length >= options.count,
787
- timedOut: true,
788
- expectedCount: options.count
789
- };
790
- }
791
- function parseDurationLike(value) {
792
- const trimmed = value.trim();
793
- if (trimmed.length === 0) {
794
- throw new Error("Duration value cannot be empty");
795
- }
796
- const numeric = Number(trimmed);
797
- if (!Number.isNaN(numeric)) {
798
- return numeric;
799
- }
800
- const match = trimmed.match(/^(\d+)\s*(ms|s|m|h|d)$/i);
801
- if (!match) {
802
- throw new Error(`Invalid duration: "${value}"`);
803
- }
804
- const amount = Number(match[1]);
805
- const unit = match[2].toLowerCase();
806
- const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
807
- return amount * multiplier;
808
- }
809
- function ensureVerifyArgs(args) {
810
- if (args.provider === "discord") {
811
- const publicKey = args.publicKey?.trim();
812
- if (!publicKey) {
813
- throw new Error('verify_signature for provider "discord" requires publicKey');
814
- }
815
- return {
816
- provider: "discord",
817
- publicKey
818
- };
819
- }
820
- const secret = args.secret?.trim();
821
- if (!secret) {
822
- throw new Error(`verify_signature for provider "${args.provider}" requires secret`);
823
- }
824
- return {
825
- provider: args.provider,
826
- secret,
827
- ...args.url ? { url: args.url } : {},
828
- ...args.method ? { method: args.method } : {}
829
- };
830
- }
831
- async function summarizeResponse(response) {
832
- return {
833
- status: response.status,
834
- statusText: response.statusText,
835
- body: await readBodyTruncated(response)
836
- };
837
- }
838
- async function emailEndpointView(client, slug) {
839
- const endpoint = await client.endpoints.get(slug);
840
- return {
841
- endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
842
- includeExtracts: endpoint.showEmailExtracts !== false
843
- };
844
- }
845
- function emailSlugFromAddress(address) {
846
- const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
847
- return local.split("+")[0].toLowerCase();
848
- }
849
- var MAX_SUMMARY_LINK = 8e3;
850
- function boundedLink(link, max) {
851
- if (link == null || link.length <= max) return { link: link ?? null };
852
- return {
853
- link: null,
854
- linkOmitted: `The main link is ${link.length} characters long, too long for the output`
855
- };
856
- }
857
- var MAX_SUMMARY_FIELD = 1e3;
858
- function summarizeEmail(email, includeExtracts) {
859
- const from = email.email.from[0];
860
- const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
861
- const subject = email.email.subject;
862
- return {
863
- id: email.id,
864
- receivedAt: new Date(email.receivedAt).toISOString(),
865
- address: email.path,
866
- tag: email.email.tag,
867
- subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
868
- from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
869
- ...includeExtracts ? { code: extractCode(email), ...boundedLink(extractLink(email), MAX_SUMMARY_LINK) } : {},
870
- attachments: email.email.attachments.length
871
- };
872
- }
873
- var ESSENTIAL_DETAIL_FIELDS = [
874
- "code",
875
- "link",
876
- "id",
877
- "endpoint",
878
- "receivedAt",
879
- "address",
880
- "tag",
881
- "subject",
882
- "from",
883
- "fields",
884
- "replyText",
885
- "size",
886
- "test",
887
- "spam",
888
- "text",
889
- "textTruncated",
890
- "htmlSize",
891
- "html",
892
- "htmlTruncated"
893
- ];
894
- function shrinkRendered(rendered, max) {
895
- const body = typeof rendered.body === "string" ? rendered.body : "";
896
- if (body.length <= max - 1024) return rendered;
897
- return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
898
- }
899
- function emailDetail(client, email, view, includeHtml) {
900
- const { data } = client.emails.toJson(email, {
901
- endpoint: view.endpoint,
902
- includeExtracts: view.includeExtracts
903
- });
904
- const { html, text: fullText, ...rest } = data;
905
- const { text, cut } = cutEmailText(fullText ?? null);
906
- const restoreText = () => {
907
- detail.text = text;
908
- if (cut) detail.textTruncated = true;
909
- else delete detail.textTruncated;
910
- };
911
- const detail = {
912
- ...view.includeExtracts ? { code: extractCode(email), link: extractLink(email) } : {},
913
- ...rest,
914
- text,
915
- ...cut ? { textTruncated: true } : {},
916
- htmlSize: html?.length ?? 0,
917
- ...includeHtml && html !== null ? { html } : {}
918
- };
919
- shrinkToFit(detail, MAX_BODY_SIZE, [
920
- () => omitHeaders(detail),
921
- () => trimSpam(detail),
922
- () => capExtracts(detail),
923
- () => capLists(detail),
924
- () => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
925
- () => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
926
- // Down to the essentials (and without a main link too long to use) there
927
- // may be room for more of the text again.
928
- () => {
929
- keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
930
- if (view.includeExtracts) {
931
- Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
932
- }
933
- restoreText();
934
- cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
935
- },
936
- () => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
937
- ]);
938
- return detail;
939
- }
940
- function registerTools(server, client) {
941
- server.tool(
942
- "create_endpoint",
943
- "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
944
- createEndpointShape,
945
- withErrorHandling(
946
- async ({
947
- name,
948
- ephemeral,
949
- expiresIn,
950
- mockResponse,
951
- responseRules,
952
- notificationUrl,
953
- signingProvider,
954
- signingSecret,
955
- signingHeader
956
- }) => {
957
- if (signingProvider) {
958
- if (signingProvider !== "discord" && !signingSecret) {
959
- throw new Error(`signingSecret is required for provider "${signingProvider}"`);
960
- }
961
- if (signingProvider === "generic-hmac" && !signingHeader) {
962
- throw new Error("signingHeader is required for generic-hmac provider");
963
- }
964
- }
965
- if (signingSecret && !signingProvider) {
966
- throw new Error("signingProvider is required when signingSecret is set");
967
- }
968
- const endpoint = await client.endpoints.create({
969
- name,
970
- ephemeral,
971
- expiresIn,
972
- mockResponse,
973
- responseRules,
974
- notificationUrl
975
- });
976
- if (signingProvider && signingSecret) {
977
- try {
978
- await client.endpoints.update(endpoint.slug, {
979
- signingProvider,
980
- signingSecret,
981
- signingHeader
982
- });
983
- } catch (err) {
984
- return jsonContent({
985
- ...endpoint,
986
- warning: `Endpoint created but signing configuration failed: ${err instanceof Error ? err.message : String(err)}. Update endpoint "${endpoint.slug}" to add signing.`
987
- });
988
- }
989
- }
990
- return jsonContent(endpoint);
991
- }
992
- )
993
- );
994
- server.tool(
995
- "list_endpoints",
996
- "List webhook endpoints: those you own (with sharedWith) and those shared with you through teams (with fromTeam). Optionally keep only one team's endpoints.",
997
- listEndpointsShape,
998
- withErrorHandling(async ({ team }) => {
999
- const endpoints = await client.endpoints.list({ team });
1000
- return jsonContent(endpoints);
1001
- })
1002
- );
1003
- server.tool(
1004
- "list_teams",
1005
- "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.",
1006
- {},
1007
- withErrorHandling(async () => {
1008
- const teams = await client.teams.list();
1009
- return jsonContent(
1010
- teams.map((team) => ({
1011
- ...team,
1012
- periodEnd: team.periodEnd ? new Date(team.periodEnd).toISOString() : null
1013
- }))
1014
- );
1015
- })
1016
- );
1017
- server.tool(
1018
- "list_team_members",
1019
- "List a team's members and pending invites. Any member of the team may call this.",
1020
- { teamId: z2.string().min(1).describe("Team id (from list_teams)") },
1021
- withErrorHandling(async ({ teamId }) => {
1022
- const result = await client.teams.members(teamId);
1023
- return jsonContent(result);
1024
- })
1025
- );
1026
- server.tool(
1027
- "share_endpoint",
1028
- "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.",
1029
- {
1030
- slug: z2.string().describe("Slug of an endpoint you own"),
1031
- teamId: z2.string().min(1).describe("Team id (from list_teams)")
1032
- },
1033
- withErrorHandling(async ({ slug, teamId }) => {
1034
- await client.teams.share(teamId, slug);
1035
- return jsonContent({ shared: true, slug, teamId });
1036
- })
1037
- );
1038
- server.tool(
1039
- "unshare_endpoint",
1040
- "Stop sharing an endpoint you own with a team. Works even when the team is suspended.",
1041
- {
1042
- slug: z2.string().describe("Slug of an endpoint you own"),
1043
- teamId: z2.string().min(1).describe("Team id (from list_teams)")
1044
- },
1045
- withErrorHandling(async ({ slug, teamId }) => {
1046
- await client.teams.unshare(teamId, slug);
1047
- return jsonContent({ shared: false, slug, teamId });
1048
- })
1049
- );
1050
- server.tool(
1051
- "get_endpoint",
1052
- "Get details for a specific webhook endpoint by slug.",
1053
- getEndpointShape,
1054
- withErrorHandling(async ({ slug }) => {
1055
- const endpoint = await client.endpoints.get(slug);
1056
- return jsonContent(endpoint);
1057
- })
1058
- );
1059
- server.tool(
1060
- "update_endpoint",
1061
- "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.",
1062
- {
1063
- slug: z2.string().describe("The endpoint slug to update"),
1064
- name: z2.string().optional().describe("New display name"),
1065
- mockResponse: mockResponseSchema.nullable().optional().describe("Default mock response (used when no rule matches), or null to clear"),
1066
- responseRules: responseRulesSchema.nullable().optional().describe("Conditional response rules (first match wins), or null to clear"),
1067
- notificationUrl: z2.string().url().nullable().optional().describe("Notification webhook URL, or null to clear it"),
1068
- signingProvider: z2.string().nullable().optional().describe(
1069
- "Signing provider for automatic verification (e.g. stripe, github), or null to disable"
1070
- ),
1071
- signingSecret: z2.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
1072
- signingHeader: z2.string().nullable().optional().describe(
1073
- "Custom signature header name (only for generic-hmac provider), or null to clear"
1074
- ),
1075
- showEmailExtracts: z2.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
1076
- emailFields: z2.array(
1077
- z2.object({
1078
- name: z2.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
1079
- from: z2.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
1080
- header: z2.string().optional().describe('The header name, for from "header"'),
1081
- pattern: z2.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
1082
- })
1083
- ).max(20).nullable().optional().describe(
1084
- "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."
1085
- )
1086
- },
1087
- withErrorHandling(
1088
- async ({
1089
- slug,
1090
- name,
1091
- mockResponse,
1092
- responseRules,
1093
- notificationUrl,
1094
- signingProvider,
1095
- signingSecret,
1096
- signingHeader,
1097
- showEmailExtracts,
1098
- emailFields
1099
- }) => {
1100
- const endpoint = await client.endpoints.update(slug, {
1101
- name,
1102
- mockResponse,
1103
- responseRules,
1104
- notificationUrl,
1105
- signingProvider,
1106
- signingSecret,
1107
- signingHeader,
1108
- showEmailExtracts,
1109
- emailFields
1110
- });
1111
- return jsonContent(endpoint);
1112
- }
1113
- )
1114
- );
1115
- server.tool(
1116
- "preview_response",
1117
- "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.",
1118
- {
1119
- slug: z2.string().describe("The endpoint slug"),
1120
- requestId: z2.string().optional().describe(
1121
- "A captured request on the endpoint (default: the newest HTTP request, or a sample)"
1122
- ),
1123
- mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
1124
- responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
1125
- },
1126
- withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
1127
- const preview = await client.endpoints.previewResponse(slug, {
1128
- ...requestId ? { requestId } : {},
1129
- ...mockResponse !== void 0 ? { mockResponse } : {},
1130
- ...responseRules !== void 0 ? { responseRules } : {}
1131
- });
1132
- return jsonContent(preview);
1133
- })
1134
- );
1135
- server.tool(
1136
- "delete_endpoint",
1137
- "Delete a webhook endpoint and all its captured requests.",
1138
- deleteEndpointShape,
1139
- withErrorHandling(async ({ slug }) => {
1140
- await client.endpoints.delete(slug);
1141
- return textContent(`Endpoint "${slug}" deleted.`);
1142
- })
1143
- );
1144
- server.tool(
1145
- "create_endpoints",
1146
- "Create multiple webhook endpoints in one call.",
1147
- {
1148
- count: z2.number().int().min(1).max(20).describe("Number of endpoints to create"),
1149
- namePrefix: z2.string().optional().describe("Optional prefix for endpoint names"),
1150
- ephemeral: z2.boolean().optional().describe("Create temporary endpoints that auto-expire"),
1151
- expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"')
1152
- },
1153
- withErrorHandling(async ({ count, namePrefix, ephemeral, expiresIn }) => {
1154
- const endpoints = await Promise.all(
1155
- Array.from(
1156
- { length: count },
1157
- (_, index) => client.endpoints.create({
1158
- name: namePrefix ? `${namePrefix}-${index + 1}` : void 0,
1159
- ephemeral,
1160
- expiresIn
1161
- })
1162
- )
1163
- );
1164
- return jsonContent({ endpoints });
1165
- })
1166
- );
1167
- server.tool(
1168
- "delete_endpoints",
1169
- "Delete multiple webhook endpoints in one call.",
1170
- {
1171
- slugs: z2.array(z2.string()).min(1).max(100).describe("Endpoint slugs to delete")
1172
- },
1173
- withErrorHandling(async ({ slugs }) => {
1174
- const settled = await Promise.allSettled(
1175
- slugs.map(async (slug) => {
1176
- await client.endpoints.delete(slug);
1177
- return slug;
1178
- })
1179
- );
1180
- return jsonContent({
1181
- deleted: settled.filter(
1182
- (result) => result.status === "fulfilled"
1183
- ).map((result) => result.value),
1184
- failed: settled.flatMap(
1185
- (result, index) => result.status === "rejected" ? [
1186
- {
1187
- slug: slugs[index],
1188
- message: result.reason instanceof Error ? result.reason.message : String(result.reason)
1189
- }
1190
- ] : []
1191
- )
1192
- });
1193
- })
1194
- );
1195
- server.tool(
1196
- "send_webhook",
1197
- "Send a test webhook to a hosted endpoint. Supports provider templates and signing.",
1198
- {
1199
- slug: z2.string().describe("The endpoint slug to send to"),
1200
- method: methodSchema,
1201
- headers: z2.record(z2.string(), z2.string()).optional().describe("HTTP headers to include"),
1202
- body: z2.unknown().optional().describe("Request body"),
1203
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template to send with signed headers"),
1204
- template: z2.string().optional().describe("Provider-specific template preset"),
1205
- event: z2.string().optional().describe("Provider event or topic name"),
1206
- secret: z2.string().optional().describe(
1207
- "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1208
- )
1209
- },
1210
- withErrorHandling(
1211
- async ({ slug, method, headers, body, provider, template, event, secret }) => {
1212
- let response;
1213
- if (provider) {
1214
- const templateSecret = secret?.trim();
1215
- if (!templateSecret && TEMPLATE_METADATA[provider]?.secretRequired !== false) {
1216
- throw new Error(`send_webhook with provider "${provider}" requires a non-empty secret`);
1217
- }
1218
- response = await client.endpoints.sendTemplate(slug, {
1219
- provider,
1220
- template,
1221
- event,
1222
- secret: templateSecret,
1223
- method,
1224
- headers,
1225
- body
1226
- });
1227
- } else {
1228
- response = await client.endpoints.send(slug, { method, headers, body });
1229
- }
1230
- const responseBody = await readBodyTruncated(response);
1231
- return jsonContent({
1232
- status: response.status,
1233
- statusText: response.statusText,
1234
- body: responseBody
1235
- });
1236
- }
1237
- )
1238
- );
1239
- server.tool(
1240
- "list_requests",
1241
- "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
1242
- listRequestsShape,
1243
- withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1244
- const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1245
- return jsonContent(requests.map(compactRequest));
1246
- })
1247
- );
1248
- server.tool(
1249
- "search_requests",
1250
- "Search captured webhook requests across endpoints using retained full-text search.",
1251
- {
1252
- slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
1253
- method: z2.string().optional().describe("Filter by HTTP method"),
1254
- kind: kindSchema,
1255
- q: z2.string().optional().describe("Free-text search across path, body, and headers"),
1256
- from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
1257
- to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
1258
- limit: z2.number().int().min(1).max(200).default(50).describe("Max results to return"),
1259
- offset: z2.number().int().min(0).max(1e4).default(0).describe("Result offset"),
1260
- order: z2.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
1261
- },
1262
- withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
1263
- const results = await client.requests.search({
1264
- slug,
1265
- method,
1266
- kind,
1267
- q,
1268
- from,
1269
- to,
1270
- limit,
1271
- offset,
1272
- order
1273
- });
1274
- return jsonContent(results.map(compactRequest));
1275
- })
1276
- );
1277
- server.tool(
1278
- "count_requests",
1279
- "Count captured webhook requests that match the given filters.",
1280
- {
1281
- slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
1282
- method: z2.string().optional().describe("Filter by HTTP method"),
1283
- kind: kindSchema,
1284
- q: z2.string().optional().describe("Free-text search across path, body, and headers"),
1285
- from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
1286
- to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
1287
- },
1288
- withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
1289
- const count = await client.requests.count({ slug, method, kind, q, from, to });
1290
- return jsonContent({ count });
1291
- })
1292
- );
1293
- server.tool(
1294
- "get_request",
1295
- "Get full details for a specific captured request by ID.",
1296
- getRequestShape,
1297
- withErrorHandling(async ({ requestId }) => {
1298
- const request = await client.requests.get(requestId);
1299
- return jsonContent(compactRequest(request));
1300
- })
1301
- );
1302
- server.tool(
1303
- "wait_for_request",
1304
- "Wait for a request to arrive at an endpoint.",
1305
- waitForRequestShape,
1306
- withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
1307
- const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
1308
- return jsonContent(compactRequest(request));
1309
- })
1310
- );
1311
- server.tool(
1312
- "wait_for_requests",
1313
- "Wait for multiple requests to arrive at an endpoint.",
1314
- {
1315
- endpointSlug: z2.string().describe("The endpoint slug to monitor"),
1316
- count: z2.number().int().min(1).max(20).describe("Number of requests to collect"),
1317
- timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
1318
- pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"'),
1319
- method: z2.string().optional().describe("Only collect requests with this HTTP method")
1320
- },
1321
- withErrorHandling(async ({ endpointSlug, count, timeout, pollInterval, method }) => {
1322
- const result = await waitForMultipleRequests(client, endpointSlug, {
1323
- count,
1324
- timeout,
1325
- pollInterval,
1326
- method
1327
- });
1328
- return jsonContent(
1329
- fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
1330
- );
1331
- })
1332
- );
1333
- server.tool(
1334
- "replay_request",
1335
- "Replay a previously captured request to a target URL.",
1336
- {
1337
- requestId: z2.string().describe("The captured request ID"),
1338
- targetUrl: httpUrlSchema.describe("The URL to replay the request to")
1339
- },
1340
- withErrorHandling(async ({ requestId, targetUrl }) => {
1341
- const response = await client.requests.replay(requestId, targetUrl);
1342
- const responseBody = await readBodyTruncated(response);
1343
- return jsonContent({
1344
- status: response.status,
1345
- statusText: response.statusText,
1346
- body: responseBody
1347
- });
1348
- })
1349
- );
1350
- server.tool(
1351
- "render_email",
1352
- "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.",
1353
- {
1354
- requestId: z2.string().describe("The captured email's request ID"),
1355
- format: z2.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
1356
- mailgunSigningKey: z2.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
1357
- targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
1358
- },
1359
- withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
1360
- const rendered = await client.emails.render(requestId, {
1361
- format,
1362
- ...mailgunSigningKey ? { mailgunSigningKey } : {}
1363
- });
1364
- if (!targetUrl) {
1365
- return jsonContent(
1366
- shrinkRendered(rendered, MAX_BODY_SIZE)
1367
- );
1368
- }
1369
- const response = await fetch(targetUrl, {
1370
- method: "POST",
1371
- headers: { "content-type": rendered.headers["content-type"] },
1372
- body: rendered.body,
1373
- redirect: "manual"
1374
- });
1375
- return jsonContent({
1376
- format,
1377
- status: response.status,
1378
- statusText: response.statusText,
1379
- body: await readBodyTruncated(response)
1380
- });
1381
- })
1382
- );
1383
- server.tool(
1384
- "compare_requests",
1385
- "Compare two captured requests and show the structured differences.",
1386
- {
1387
- leftRequestId: z2.string().describe("The first request ID"),
1388
- rightRequestId: z2.string().describe("The second request ID"),
1389
- ignoreHeaders: z2.array(z2.string()).optional().describe("Headers to ignore during comparison")
1390
- },
1391
- withErrorHandling(async ({ leftRequestId, rightRequestId, ignoreHeaders }) => {
1392
- const [leftRequest, rightRequest] = await Promise.all([
1393
- client.requests.get(leftRequestId),
1394
- client.requests.get(rightRequestId)
1395
- ]);
1396
- const diff = diffRequests(leftRequest, rightRequest, { ignoreHeaders });
1397
- return jsonContent(diff);
1398
- })
1399
- );
1400
- server.tool(
1401
- "generate_payload_schema",
1402
- "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.",
1403
- payloadSchemaShape,
1404
- withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
1405
- const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
1406
- return payloadSchemaResult(requests, args);
1407
- })
1408
- );
1409
- server.tool(
1410
- "extract_from_request",
1411
- "Extract specific JSON fields from a captured request body.",
1412
- {
1413
- requestId: z2.string().describe("The request ID"),
1414
- jsonPaths: z2.array(z2.string()).min(1).max(50).describe("Dot-notation JSON paths to extract")
1415
- },
1416
- withErrorHandling(async ({ requestId, jsonPaths }) => {
1417
- const request = await client.requests.get(requestId);
1418
- const extracted = Object.fromEntries(
1419
- jsonPaths.map((path) => [path, extractJsonField(request, path) ?? null])
1420
- );
1421
- return jsonContent(extracted);
1422
- })
1423
- );
1424
- server.tool(
1425
- "verify_signature",
1426
- "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.",
1427
- {
1428
- requestId: z2.string().describe("The captured request ID"),
1429
- provider: z2.enum(VERIFY_PROVIDERS).optional().describe("Provider whose signature scheme should be verified. Omit to use stored result."),
1430
- secret: z2.string().optional().describe(
1431
- "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."
1432
- ),
1433
- publicKey: z2.string().optional().describe("Discord application public key. Required for provider=discord."),
1434
- url: httpUrlSchema.optional().describe("Original signed URL. Required for Twilio, Square, and HubSpot verification."),
1435
- method: z2.string().optional().describe(
1436
- "Original HTTP method used when the signature was generated. Used by HubSpot v3 verification; the SDK assumes POST when omitted."
1437
- )
1438
- },
1439
- withErrorHandling(async ({ requestId, provider, secret, publicKey, url, method }) => {
1440
- const request = await client.requests.get(requestId);
1441
- if (!provider && !secret && !publicKey) {
1442
- const verified = request.signatureVerified;
1443
- if (verified === null || verified === void 0) {
1444
- return jsonContent({
1445
- valid: null,
1446
- details: "No signature verification configured on this endpoint. Pass provider and secret to verify client-side."
1447
- });
1448
- }
1449
- let errorData = null;
1450
- if (request.signatureError) {
1451
- try {
1452
- errorData = JSON.parse(request.signatureError);
1453
- } catch {
1454
- errorData = null;
1455
- }
1456
- }
1457
- const isSkipped = !verified && errorData?.code && (errorData.code === "missing_header" || errorData.code === "unsupported");
1458
- return jsonContent({
1459
- valid: isSkipped ? null : verified,
1460
- provider: request.signingProvider ?? null,
1461
- details: verified ? "Signature verified by server." : isSkipped ? `Verification skipped: ${errorData?.message ?? errorData?.code}` : `Signature invalid: ${request.signatureError ?? "unknown error"}`,
1462
- error: request.signatureError ?? null,
1463
- skipped: isSkipped ?? false
1464
- });
1465
- }
1466
- const verificationOptions = ensureVerifyArgs({
1467
- provider,
1468
- secret,
1469
- publicKey,
1470
- url,
1471
- method
1472
- });
1473
- const result = await verifySignature(request, verificationOptions);
1474
- return jsonContent({
1475
- valid: result.valid,
1476
- details: result.valid ? "Signature is valid." : "Signature did not match."
1477
- });
1478
- })
1479
- );
1480
- server.tool(
1481
- "clear_requests",
1482
- "Delete captured requests for an endpoint without deleting the endpoint itself.",
1483
- {
1484
- slug: z2.string().describe("The endpoint slug to clear"),
1485
- before: durationOrTimestampSchema.optional().describe('Only clear requests older than this timestamp or duration like "1h"')
1486
- },
1487
- withErrorHandling(async ({ slug, before }) => {
1488
- await client.requests.clear(slug, { before });
1489
- return jsonContent({ slug, cleared: true, before: before ?? null });
1490
- })
1491
- );
1492
- server.tool(
1493
- "send_to",
1494
- "Send a webhook directly to any URL with optional provider signing.",
1495
- {
1496
- url: httpUrlSchema.describe("Target URL"),
1497
- method: methodSchema,
1498
- headers: z2.record(z2.string(), z2.string()).optional().describe("HTTP headers to include"),
1499
- body: z2.unknown().optional().describe("Request body"),
1500
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template for signing"),
1501
- template: z2.string().optional().describe("Provider-specific template preset"),
1502
- event: z2.string().optional().describe("Provider event or topic name"),
1503
- secret: z2.string().optional().describe(
1504
- "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1505
- )
1506
- },
1507
- withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
1508
- const response = await client.sendTo(url, {
1509
- method,
1510
- headers,
1511
- body,
1512
- provider,
1513
- template,
1514
- event,
1515
- secret
1516
- });
1517
- const responseBody = await readBodyTruncated(response);
1518
- return jsonContent({
1519
- status: response.status,
1520
- statusText: response.statusText,
1521
- body: responseBody
1522
- });
1523
- })
1524
- );
1525
- server.tool(
1526
- "preview_webhook",
1527
- "Preview a webhook request without sending it. Returns the exact URL, method, headers, and body.",
1528
- {
1529
- url: httpUrlSchema.describe("Target URL"),
1530
- method: methodSchema,
1531
- headers: z2.record(z2.string(), z2.string()).optional().describe("HTTP headers to include"),
1532
- body: z2.unknown().optional().describe("Request body"),
1533
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template for signing"),
1534
- template: z2.string().optional().describe("Provider-specific template preset"),
1535
- event: z2.string().optional().describe("Provider event or topic name"),
1536
- secret: z2.string().optional().describe(
1537
- "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1538
- )
1539
- },
1540
- withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
1541
- const preview = await client.buildRequest(url, {
1542
- method,
1543
- headers,
1544
- body,
1545
- provider,
1546
- template,
1547
- event,
1548
- secret
1549
- });
1550
- return jsonContent(preview);
1551
- })
1552
- );
1553
- server.tool(
1554
- "list_provider_templates",
1555
- "List supported webhook providers, templates, and signing metadata.",
1556
- {
1557
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Filter to a single provider")
1558
- },
1559
- withErrorHandling(async ({ provider }) => {
1560
- if (provider) {
1561
- return jsonContent([client.templates.get(provider)]);
1562
- }
1563
- return jsonContent(
1564
- client.templates.listProviders().map((name) => client.templates.get(name))
1565
- );
1566
- })
1567
- );
1568
- server.tool(
1569
- "get_usage",
1570
- "Check current request usage, remaining quota, plan, and period end, plus the pooled quota of every subscribed team you belong to.",
1571
- {},
1572
- withErrorHandling(async () => {
1573
- const [usageResult, teamsResult] = await Promise.allSettled([
1574
- client.usage(),
1575
- client.teams.list()
1576
- ]);
1577
- if (usageResult.status === "rejected") throw usageResult.reason;
1578
- const usage = usageResult.value;
1579
- const teams = teamsResult.status === "fulfilled" ? teamsResult.value : [];
1580
- const teamsError = teamsResult.status === "rejected" ? teamsResult.reason instanceof Error ? teamsResult.reason.message : String(teamsResult.reason) : void 0;
1581
- return jsonContent({
1582
- ...usage,
1583
- periodEnd: usage.periodEnd ? new Date(usage.periodEnd).toISOString() : null,
1584
- ...teamsError ? { teamsError } : {},
1585
- teams: teams.filter((team) => !team.suspended).map((team) => ({
1586
- id: team.id,
1587
- name: team.name,
1588
- role: team.role,
1589
- seats: team.seats,
1590
- used: team.requestsUsed,
1591
- limit: team.requestLimit,
1592
- remaining: Math.max(0, team.requestLimit - team.requestsUsed),
1593
- periodEnd: team.periodEnd ? new Date(team.periodEnd).toISOString() : null
1594
- }))
1595
- });
1596
- })
1597
- );
1598
- server.tool(
1599
- "test_webhook_flow",
1600
- "Run a full webhook test flow: create endpoint, optionally mock, send, wait, verify, replay, and clean up.",
1601
- {
1602
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template to use when sending the webhook"),
1603
- event: z2.string().optional().describe("Optional provider event or topic name"),
1604
- secret: z2.string().optional().describe(
1605
- "Signing secret. Required when provider is set or signature verification is enabled."
1606
- ),
1607
- mockStatus: z2.number().int().min(100).max(599).optional().describe("Optional mock response status to configure before sending"),
1608
- targetUrl: httpUrlSchema.optional().describe("Optional URL to replay the captured request to after capture"),
1609
- verifySignature: z2.boolean().default(false).describe("Verify the captured request signature after capture"),
1610
- cleanup: z2.boolean().default(true).describe("Delete the created endpoint after the flow completes")
1611
- },
1612
- withErrorHandling(
1613
- async ({
1614
- provider,
1615
- event,
1616
- secret,
1617
- mockStatus,
1618
- targetUrl,
1619
- verifySignature: shouldVerify,
1620
- cleanup
1621
- }) => {
1622
- const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s" });
1623
- if (mockStatus !== void 0) {
1624
- flow.setMock({
1625
- status: mockStatus,
1626
- body: "",
1627
- headers: {}
1628
- });
1629
- }
1630
- if (provider) {
1631
- const templateSecret = secret?.trim();
1632
- if (!templateSecret && TEMPLATE_METADATA[provider]?.secretRequired !== false) {
1633
- throw new Error(
1634
- `test_webhook_flow with provider "${provider}" requires a non-empty secret`
1635
- );
1636
- }
1637
- flow.sendTemplate({
1638
- provider,
1639
- event,
1640
- secret: templateSecret
1641
- });
1642
- if (shouldVerify) {
1643
- if (provider === "discord") {
1644
- throw new Error(
1645
- "test_webhook_flow cannot verify Discord signatures (Ed25519 requires a public key, not a secret)"
1646
- );
1647
- }
1648
- if (!templateSecret) {
1649
- throw new Error(
1650
- `test_webhook_flow cannot verify "${provider}" signatures \u2014 this provider's templates are unsigned`
1651
- );
1652
- }
1653
- flow.verifySignature({
1654
- provider,
1655
- secret: templateSecret
1656
- });
1657
- }
1658
- } else {
1659
- if (shouldVerify) {
1660
- throw new Error("test_webhook_flow cannot verify signatures without a provider");
1661
- }
1662
- flow.send();
1663
- }
1664
- if (targetUrl) {
1665
- flow.replayTo(targetUrl);
1666
- }
1667
- if (cleanup) {
1668
- flow.cleanup();
1669
- }
1670
- const result = await flow.run();
1671
- return jsonContent({
1672
- endpoint: result.endpoint,
1673
- request: result.request ?? null,
1674
- verification: result.verification ?? null,
1675
- replayResponse: result.replayResponse ? await summarizeResponse(result.replayResponse) : null,
1676
- cleanedUp: result.cleanedUp
1677
- });
1678
- }
1679
- )
1680
- );
1681
- server.tool(
1682
- "list_emails",
1683
- "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.",
1684
- {
1685
- endpointSlug: z2.string().describe("The endpoint slug"),
1686
- limit: z2.number().int().min(1).max(100).default(25).describe(
1687
- `Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
1688
- ),
1689
- since: z2.number().optional().describe("Only emails received after this timestamp in ms"),
1690
- tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
1691
- subject: z2.string().optional().describe("Only emails whose subject contains this text"),
1692
- from: z2.string().optional().describe("Only emails from this address (case-insensitive)")
1693
- },
1694
- withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
1695
- const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
1696
- const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
1697
- const [emails, view] = await Promise.all([
1698
- client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
1699
- emailEndpointView(client, endpointSlug)
1700
- ]);
1701
- return jsonContent(
1702
- emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
1703
- );
1704
- })
1705
- );
1706
- server.tool(
1707
- "get_email",
1708
- "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.",
1709
- {
1710
- requestId: z2.string().optional().describe("The email's request ID"),
1711
- endpointSlug: z2.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
1712
- tag: z2.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
1713
- includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
1714
- },
1715
- withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
1716
- if (requestId === void 0 === (endpointSlug === void 0)) {
1717
- throw new Error("Pass exactly one of requestId or endpointSlug");
1718
- }
1719
- const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
1720
- if (!email) {
1721
- throw new NotFoundError(
1722
- `No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
1723
- );
1724
- }
1725
- const view = await emailEndpointView(
1726
- client,
1727
- endpointSlug ?? emailSlugFromAddress(email.path)
1728
- );
1729
- return jsonContent(emailDetail(client, email, view, includeHtml));
1730
- })
1731
- );
1732
- server.tool(
1733
- "wait_for_email",
1734
- "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.",
1735
- {
1736
- endpointSlug: z2.string().describe("The endpoint slug"),
1737
- tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
1738
- subject: z2.string().optional().describe("Only emails whose subject contains this text"),
1739
- from: z2.string().optional().describe("Only emails from this address (case-insensitive)"),
1740
- timeout: durationOrTimestampSchema.default("30s").describe(
1741
- `How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
1742
- ),
1743
- since: z2.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
1744
- includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false)")
1745
- },
1746
- withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1747
- const [email, view] = await Promise.all([
1748
- client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1749
- emailEndpointView(client, endpointSlug)
1750
- ]);
1751
- return jsonContent(emailDetail(client, email, view, includeHtml));
1752
- })
1753
- );
1754
- server.tool(
1755
- "send_test_email",
1756
- "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.",
1757
- {
1758
- slug: z2.string().describe("The endpoint slug"),
1759
- tag: z2.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
1760
- },
1761
- withErrorHandling(async ({ slug, tag }) => {
1762
- const result = await client.emails.sendTest(slug, { tag });
1763
- return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1764
- })
1765
- );
1766
- server.tool(
1767
- "list_destinations",
1768
- "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.",
1769
- {
1770
- slug: z2.string().describe("The endpoint slug"),
1771
- summary: z2.boolean().default(true).describe("Add each destination's last 24 hours")
1772
- },
1773
- withErrorHandling(async ({ slug, summary }) => {
1774
- return jsonContent(await client.destinations.list(slug, { summary }));
1775
- })
1776
- );
1777
- server.tool(
1778
- "add_destination",
1779
- "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.",
1780
- {
1781
- slug: z2.string().describe("The endpoint slug"),
1782
- url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
1783
- ...destinationFields
1784
- },
1785
- withErrorHandling(async ({ slug, ...input }) => {
1786
- return jsonContent(await client.destinations.create(slug, input));
1787
- })
1788
- );
1789
- server.tool(
1790
- "update_destination",
1791
- "Change a destination: any of its fields; only the ones you pass change. Owner only.",
1792
- {
1793
- slug: z2.string().describe("The endpoint slug"),
1794
- destinationId: z2.string().describe("The destination's id (from list_destinations)"),
1795
- url: httpUrlSchema.optional().describe("A new URL"),
1796
- ...destinationFields
1797
- },
1798
- withErrorHandling(async ({ slug, destinationId, ...input }) => {
1799
- return jsonContent(await client.destinations.update(slug, destinationId, input));
1800
- })
1801
- );
1802
- server.tool(
1803
- "remove_destination",
1804
- "Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
1805
- {
1806
- slug: z2.string().describe("The endpoint slug"),
1807
- destinationId: z2.string().describe("The destination's id")
1808
- },
1809
- withErrorHandling(async ({ slug, destinationId }) => {
1810
- await client.destinations.delete(slug, destinationId);
1811
- return textContent(`Destination ${destinationId} removed.`);
1812
- })
1813
- );
1814
- server.tool(
1815
- "preview_destination",
1816
- "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.",
1817
- {
1818
- slug: z2.string().describe("The endpoint slug"),
1819
- destinationId: z2.string().optional().describe("A saved destination"),
1820
- destination: z2.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
1821
- requestId: z2.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
1822
- },
1823
- withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
1824
- if (!destinationId && !destination?.url) {
1825
- throw new Error("Pass destinationId, or a destination with a url");
1826
- }
1827
- return jsonContent(
1828
- await client.destinations.preview(slug, {
1829
- ...destinationId ? { destinationId } : {},
1830
- ...destination ? { destination } : {},
1831
- ...requestId ? { requestId } : {}
1832
- })
1833
- );
1834
- })
1835
- );
1836
- server.tool(
1837
- "configure_forwarding",
1838
- "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.",
1839
- {
1840
- slug: z2.string().describe("The endpoint slug"),
1841
- url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1842
- enabled: z2.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)"),
1843
- http: z2.boolean().optional().describe("Forward HTTP requests"),
1844
- email: z2.boolean().optional().describe("Forward emails"),
1845
- format: z2.enum(["auto", "as_received", "json", "chat"]).optional().describe(
1846
- "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)"
1847
- ),
1848
- headers: z2.array(
1849
- z2.object({
1850
- name: z2.string().describe("Header name, e.g. Authorization"),
1851
- value: z2.string().nullable().describe("Header value, or null to keep the value stored under this name")
1852
- })
1853
- ).max(10).optional().describe(
1854
- "Your own headers for every delivery (not chat). Replaces the whole list; [] removes them"
1855
- ),
1856
- appendPath: z2.boolean().optional().describe("Add the path after the slug to the URL for relayed requests (default true)"),
1857
- retrySeconds: z2.union([z2.literal(0), z2.literal(3600), z2.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
1858
- keepOrder: z2.boolean().optional().describe("One delivery at a time, in capture order (default false)"),
1859
- sentField: z2.string().max(128).nullable().optional().describe(
1860
- "JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
1861
- )
1862
- },
1863
- withErrorHandling(async ({ slug, ...options }) => {
1864
- const endpoint = await client.forwarding.configure(slug, options);
1865
- return jsonContent({
1866
- slug: endpoint.slug,
1867
- forwardEnabled: endpoint.forwardEnabled ?? false,
1868
- forwardUrl: endpoint.forwardUrl ?? null,
1869
- hasForwardSecret: endpoint.hasForwardSecret ?? false,
1870
- forwardHttp: endpoint.forwardHttp ?? false,
1871
- forwardEmail: endpoint.forwardEmail ?? false,
1872
- forwardFormat: endpoint.forwardFormat ?? "auto",
1873
- forwardHeaders: endpoint.forwardHeaders ?? [],
1874
- forwardAppendPath: endpoint.forwardAppendPath ?? true,
1875
- forwardRetrySeconds: endpoint.forwardRetrySeconds ?? 86400,
1876
- forwardKeepOrder: endpoint.forwardKeepOrder ?? false,
1877
- forwardSentField: endpoint.forwardSentField ?? null
1878
- });
1879
- })
1880
- );
1881
- server.tool(
1882
- "get_forwarding_secret",
1883
- "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.",
1884
- {
1885
- slug: z2.string().describe("The endpoint slug"),
1886
- destinationId: z2.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1887
- rotate: z2.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1888
- },
1889
- withErrorHandling(async ({ slug, destinationId, rotate }) => {
1890
- 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);
1891
- return jsonContent({ secret, rotated: rotate });
1892
- })
1893
- );
1894
- server.tool(
1895
- "test_forwarding",
1896
- "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.",
1897
- {
1898
- slug: z2.string().describe("The endpoint slug"),
1899
- destinationId: z2.string().optional().describe("A destination (default: the endpoint's primary destination)"),
1900
- kind: z2.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
1901
- requestId: z2.string().optional().describe("With destinationId: a captured request to send")
1902
- },
1903
- withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
1904
- if (destinationId) {
1905
- return jsonContent(
1906
- await client.destinations.test(slug, destinationId, {
1907
- ...kind ? { kind } : {},
1908
- ...requestId ? { requestId } : {}
1909
- })
1910
- );
1911
- }
1912
- return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1913
- })
1914
- );
1915
- server.tool(
1916
- "list_deliveries",
1917
- "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.",
1918
- {
1919
- endpointSlug: z2.string().optional().describe("List the endpoint's delivery log"),
1920
- requestId: z2.string().optional().describe("List every delivery and try of this request"),
1921
- limit: z2.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1922
- status: z2.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
1923
- "With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
1924
- ),
1925
- destinationId: z2.string().optional().describe("With endpointSlug: only this destination"),
1926
- before: z2.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1927
- },
1928
- withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
1929
- if (requestId === void 0 === (endpointSlug === void 0)) {
1930
- throw new Error("Pass exactly one of endpointSlug or requestId");
1931
- }
1932
- if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1933
- const [summary, deliveries] = await Promise.all([
1934
- client.forwarding.summary(endpointSlug),
1935
- client.forwarding.deliveries(endpointSlug, {
1936
- limit,
1937
- status,
1938
- before,
1939
- ...destinationId ? { destinationId } : {}
1940
- })
1941
- ]);
1942
- return jsonContent({
1943
- summary,
1944
- deliveries,
1945
- nextCursor: deliveries.length === limit ? deliveries[deliveries.length - 1]?.cursor ?? null : null
1946
- });
1947
- })
1948
- );
1949
- server.tool(
1950
- "redeliver",
1951
- "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.",
1952
- {
1953
- requestId: z2.string().optional().describe("The request or email to send again"),
1954
- endpointSlug: z2.string().optional().describe("Send every failed delivery of this endpoint again"),
1955
- destinationId: z2.string().optional().describe("Only this destination")
1956
- },
1957
- withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
1958
- if (requestId === void 0 === (endpointSlug === void 0)) {
1959
- throw new Error("Pass exactly one of requestId or endpointSlug");
1960
- }
1961
- const options = destinationId ? { destinationId } : {};
1962
- return jsonContent(
1963
- requestId ? await client.forwarding.redeliver(requestId, options) : await client.forwarding.redeliverFailed(endpointSlug, options)
1964
- );
1965
- })
1966
- );
1967
- server.tool(
1968
- "describe",
1969
- "Describe all available SDK operations, parameters, and types.",
1970
- {},
1971
- withErrorHandling(async () => {
1972
- const description = client.describe();
1973
- return jsonContent(description);
1974
- })
1975
- );
1976
- }
1977
-
1978
- // src/sandbox-tools.ts
1979
263
  var SANDBOX = {
1980
264
  maxEndpoints: 3,
1981
265
  requestsPerEndpoint: 25,
@@ -1984,7 +268,6 @@ var SANDBOX = {
1984
268
  };
1985
269
  var DEFAULT_WAIT_SECONDS = 45;
1986
270
  var MAX_WAIT_SECONDS = 600;
1987
- var TOKEN_REFRESH_MARGIN_MS = 6e4;
1988
271
  var DOCS_URL = "https://webhooks.cc/docs/agents";
1989
272
  var ACCOUNT_ONLY_OPTIONS = [
1990
273
  "mockResponse",
@@ -1995,20 +278,29 @@ var ACCOUNT_ONLY_OPTIONS = [
1995
278
  "signingHeader"
1996
279
  ];
1997
280
  var AgentSession = class {
1998
- constructor(baseUrl, webhookUrl, clientName) {
281
+ constructor(baseUrl, webhookUrl, clientName, credentialsFile) {
1999
282
  this.baseUrl = baseUrl;
2000
283
  this.webhookUrl = webhookUrl;
2001
284
  this.clientName = clientName;
285
+ this.credentialsFile = credentialsFile;
2002
286
  this.sandbox = null;
2003
- this.claim = null;
2004
- this.connected = false;
287
+ this.connect = null;
288
+ }
289
+ sandboxOptions() {
290
+ return {
291
+ baseUrl: this.baseUrl,
292
+ webhookUrl: this.webhookUrl,
293
+ clientName: this.clientName,
294
+ credentialsFile: this.credentialsFile
295
+ };
2005
296
  }
2006
297
  /**
2007
- * The sandbox, registered on first use and again after it expired. Tool
2008
- * calls can run in parallel: this.sandbox is read again after every await,
2009
- * and a new sandbox is assigned with no await in between, so parallel
2010
- * calls share one registration. Calls waiting on a registration that fails
2011
- * get its error (a 429 asks them to wait); the next call starts again.
298
+ * The sandbox: the one saved by an earlier session when it is still live,
299
+ * else a new registration (and again after it expired). Tool calls can run
300
+ * in parallel: this.sandbox is read again after every await, and a new
301
+ * sandbox is assigned with no await in between, so parallel calls share
302
+ * one registration. Calls waiting on a registration that fails get its
303
+ * error (a 429 asks them to wait); the next call starts again.
2012
304
  */
2013
305
  async sandboxClient() {
2014
306
  while (this.sandbox) {
@@ -2016,35 +308,36 @@ var AgentSession = class {
2016
308
  const sandbox = await pending;
2017
309
  if (!this.expired(pending, sandbox)) return sandbox;
2018
310
  }
2019
- const created = WebhooksCC2.sandbox({
2020
- baseUrl: this.baseUrl,
2021
- webhookUrl: this.webhookUrl,
2022
- clientName: this.clientName
2023
- });
311
+ const created = WebhooksCC.sandbox(this.sandboxOptions());
2024
312
  this.sandbox = created;
2025
313
  created.catch(() => {
2026
314
  if (this.sandbox === created) this.sandbox = null;
2027
315
  });
2028
316
  return created;
2029
317
  }
2030
- /** The live sandbox, if one was registered; never registers one. */
318
+ /** The live sandbox, from this session or the credential file; never registers one. */
2031
319
  async existingSandbox() {
2032
320
  const pending = this.sandbox;
2033
- if (!pending) return null;
2034
- let sandbox;
2035
- try {
2036
- sandbox = await pending;
2037
- } catch {
2038
- return null;
321
+ if (pending) {
322
+ let sandbox;
323
+ try {
324
+ sandbox = await pending;
325
+ } catch {
326
+ return null;
327
+ }
328
+ return this.expired(pending, sandbox) ? null : sandbox;
2039
329
  }
2040
- return this.expired(pending, sandbox) ? null : sandbox;
330
+ const resumed = await SandboxClient.resume(this.sandboxOptions()).catch(() => null);
331
+ if (!resumed) return null;
332
+ if (!this.sandbox) this.sandbox = Promise.resolve(resumed);
333
+ return this.sandbox;
2041
334
  }
2042
335
  /** True when the sandbox is past its 24 hours; then it is dropped. */
2043
336
  expired(pending, sandbox) {
2044
337
  if (sandbox.expiresAt.getTime() > Date.now()) return false;
2045
338
  if (this.sandbox === pending) this.sandbox = null;
2046
- if (this.claim?.kind === "sandbox" && this.claim.claimToken === sandbox.registration.claimToken) {
2047
- this.claim = null;
339
+ if (this.connect?.via === "sandbox" && this.connect.sandbox === sandbox) {
340
+ this.connect = null;
2048
341
  }
2049
342
  return true;
2050
343
  }
@@ -2055,26 +348,11 @@ var AgentSession = class {
2055
348
  }
2056
349
  return sandbox;
2057
350
  }
2058
- forgetSandbox() {
351
+ reset() {
2059
352
  this.sandbox = null;
353
+ this.connect = null;
2060
354
  }
2061
355
  };
2062
- function claimedTokens(baseUrl, assertion, first) {
2063
- let current = first;
2064
- let pending = null;
2065
- return async (options) => {
2066
- if (!options?.forceRefresh && current.expiresAt.getTime() - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
2067
- return current.accessToken;
2068
- }
2069
- if (!pending) {
2070
- pending = WebhooksCC2.agent.exchange({ baseUrl, assertion }).finally(() => {
2071
- pending = null;
2072
- });
2073
- }
2074
- current = await pending;
2075
- return current.accessToken;
2076
- };
2077
- }
2078
356
  function minutesLeft(until) {
2079
357
  return Math.max(1, Math.round((until.getTime() - Date.now()) / 6e4));
2080
358
  }
@@ -2082,16 +360,53 @@ function onlyKind(requests, kind) {
2082
360
  if (!kind) return requests;
2083
361
  return requests.filter((request) => (request.kind ?? "http") === kind);
2084
362
  }
363
+ function savedTo(credentialsFile) {
364
+ return AgentCredentialStore.resolve(credentialsFile)?.path ?? null;
365
+ }
2085
366
  function registerSandboxTools(server, options = {}) {
2086
367
  const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL ?? "https://webhooks.cc";
368
+ const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
2087
369
  const session = new AgentSession(
2088
370
  baseUrl,
2089
- options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
2090
- options.clientName ?? "webhooks-cc-mcp"
2091
- );
2092
- const registered = [];
371
+ webhookUrl,
372
+ options.clientName ?? "webhooks-cc-mcp",
373
+ options.credentialsFile
374
+ );
375
+ let handles = [];
376
+ let shown = 0;
377
+ const swap = (register) => {
378
+ const id = ++shown;
379
+ for (const handle of handles) handle.remove();
380
+ handles = register(id);
381
+ };
382
+ const showSandbox = () => swap(() => sandboxToolSet(server, session, showAccount));
383
+ const savedAccount = (id) => options.credentialsFile === false ? null : WebhooksCC.fromSavedConnection({
384
+ baseUrl,
385
+ webhookUrl,
386
+ credentialsFile: options.credentialsFile,
387
+ // The human disconnected the agent: back to the sandbox, unless a
388
+ // newer connection is already on display.
389
+ onRevoked: () => queueMicrotask(() => {
390
+ if (shown === id) showSandbox();
391
+ })
392
+ });
393
+ function showAccount(client) {
394
+ swap((id) => registerTools(server, savedAccount(id) ?? client));
395
+ options.onConnected?.(client);
396
+ }
397
+ const saved = savedAccount(1);
398
+ if (saved) {
399
+ swap(() => registerTools(server, saved));
400
+ options.onConnected?.(saved);
401
+ } else {
402
+ showSandbox();
403
+ }
404
+ }
405
+ function sandboxToolSet(server, session, showAccount) {
406
+ const tools = [];
2093
407
  const limits = `up to ${SANDBOX.maxEndpoints} endpoints at a time, ${SANDBOX.requestsPerEndpoint} captured requests each and ${SANDBOX.requests} in all, deleted ${SANDBOX.lifetimeHours} hours after the sandbox was created`;
2094
- registered.push(
408
+ const file = savedTo(session.credentialsFile);
409
+ tools.push(
2095
410
  server.tool(
2096
411
  "create_endpoint",
2097
412
  `Create a temporary webhook capture endpoint in the agent sandbox (no account needed): ${limits}. The first call registers the sandbox, which takes a few seconds of proof of work. Mock responses, notifications and signature verification need an account: see connect_account.`,
@@ -2115,7 +430,7 @@ function registerSandboxTools(server, options = {}) {
2115
430
  })
2116
431
  )
2117
432
  );
2118
- registered.push(
433
+ tools.push(
2119
434
  server.tool(
2120
435
  "list_endpoints",
2121
436
  "List this agent's sandbox endpoints. Empty until create_endpoint starts the sandbox.",
@@ -2129,7 +444,7 @@ function registerSandboxTools(server, options = {}) {
2129
444
  })
2130
445
  )
2131
446
  );
2132
- registered.push(
447
+ tools.push(
2133
448
  server.tool(
2134
449
  "get_endpoint",
2135
450
  "Get one of this agent's sandbox endpoints by slug, with its sandbox limits and usage.",
@@ -2139,7 +454,7 @@ function registerSandboxTools(server, options = {}) {
2139
454
  })
2140
455
  )
2141
456
  );
2142
- registered.push(
457
+ tools.push(
2143
458
  server.tool(
2144
459
  "delete_endpoint",
2145
460
  "Delete one of this agent's sandbox endpoints and its captured requests. Frees a slot; the sandbox's request budget is not refunded.",
@@ -2150,7 +465,7 @@ function registerSandboxTools(server, options = {}) {
2150
465
  })
2151
466
  )
2152
467
  );
2153
- registered.push(
468
+ tools.push(
2154
469
  server.tool(
2155
470
  "list_requests",
2156
471
  "List recent requests captured by a sandbox endpoint, newest first.",
@@ -2162,7 +477,7 @@ function registerSandboxTools(server, options = {}) {
2162
477
  })
2163
478
  )
2164
479
  );
2165
- registered.push(
480
+ tools.push(
2166
481
  server.tool(
2167
482
  "get_request",
2168
483
  "Get full details for a request captured by a sandbox endpoint.",
@@ -2173,7 +488,7 @@ function registerSandboxTools(server, options = {}) {
2173
488
  })
2174
489
  )
2175
490
  );
2176
- registered.push(
491
+ tools.push(
2177
492
  server.tool(
2178
493
  "wait_for_request",
2179
494
  "Wait for a request to arrive at a sandbox endpoint.",
@@ -2185,7 +500,7 @@ function registerSandboxTools(server, options = {}) {
2185
500
  })
2186
501
  )
2187
502
  );
2188
- registered.push(
503
+ tools.push(
2189
504
  server.tool(
2190
505
  "generate_payload_schema",
2191
506
  "Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from the bodies of requests captured by a sandbox endpoint. Several requests of one event teach which fields are optional or nullable.",
@@ -2197,91 +512,82 @@ function registerSandboxTools(server, options = {}) {
2197
512
  })
2198
513
  )
2199
514
  );
2200
- registered.push(
515
+ tools.push(
2201
516
  server.tool(
2202
517
  "connect_account",
2203
518
  "Connect this agent to a human's webhooks.cc account, for the full tool set (mock responses, signatures, email, teams) and to keep the sandbox endpoints. Returns a link and a 6-digit code: show both to the human, who signs in or signs up with that email and enters the code. Then call wait_for_connection. Calling again gives a new code.",
2204
519
  {
2205
- email: z3.string().email({ message: "Invalid email address" }).describe("The human's email address; only someone signed in as it can connect")
520
+ email: z2.string().email({ message: "Invalid email address" }).describe(
521
+ "The email the human signs in to webhooks.cc with (their GitHub or Google primary email if they sign in that way); only someone signed in as it can connect"
522
+ )
2206
523
  },
2207
524
  withErrorHandling(async ({ email }) => {
2208
- if (session.connected) {
2209
- return jsonContent({ connected: true, message: "Already connected to an account." });
2210
- }
2211
525
  const normalized = email.trim().toLowerCase();
526
+ const previous = session.connect;
2212
527
  const sandbox = await session.existingSandbox();
2213
- let claim;
528
+ let attempt;
529
+ let movesEndpoints = false;
2214
530
  if (sandbox) {
2215
- claim = {
2216
- kind: "sandbox",
2217
- claimToken: sandbox.registration.claimToken,
2218
- email: normalized,
2219
- attempt: await sandbox.claim({ email: normalized }),
2220
- claimExpires: sandbox.expiresAt
2221
- };
2222
- } else if (session.claim?.kind === "service_auth" && session.claim.email === normalized && session.claim.claimExpires.getTime() > Date.now()) {
2223
- const attempt = await WebhooksCC2.agent.startClaim({
2224
- baseUrl,
2225
- claimToken: session.claim.claimToken
2226
- });
2227
- claim = { ...session.claim, attempt };
531
+ const claim = await sandbox.claim({ email: normalized });
532
+ session.connect = { via: "sandbox", sandbox, email: normalized, attempt: claim };
533
+ attempt = claim;
534
+ movesEndpoints = claim.movesEndpoints;
2228
535
  } else {
2229
- const registration = await WebhooksCC2.agent.registerServiceAuth({
2230
- baseUrl,
2231
- email: normalized,
2232
- clientName: session.clientName
2233
- });
2234
- claim = {
2235
- kind: "service_auth",
2236
- claimToken: registration.claimToken,
536
+ let pending = null;
537
+ if (previous?.via === "connect" && previous.email === normalized) {
538
+ pending = await previous.pending.restart().catch((error) => {
539
+ if (error instanceof AgentAuthError) return null;
540
+ throw error;
541
+ });
542
+ }
543
+ pending ?? (pending = await WebhooksCC.connect({
544
+ baseUrl: session.baseUrl,
545
+ webhookUrl: session.webhookUrl,
2237
546
  email: normalized,
2238
- attempt: registration.claim,
2239
- claimExpires: registration.claimTokenExpires
2240
- };
547
+ clientName: session.clientName,
548
+ credentialsFile: session.credentialsFile
549
+ }));
550
+ session.connect = { via: "connect", pending, email: normalized };
551
+ attempt = pending;
2241
552
  }
2242
- session.claim = claim;
2243
- const { verificationUri, userCode, expiresAt } = claim.attempt;
553
+ const { verificationUri, userCode, expiresAt } = attempt;
2244
554
  return jsonContent({
2245
555
  verificationUri,
2246
556
  userCode,
2247
557
  expiresAt: expiresAt.toISOString(),
2248
558
  tellTheHuman: `Open ${verificationUri}, sign in or sign up as ${normalized}, and enter the code ${userCode}.`,
2249
559
  next: `Show the human that line, then call wait_for_connection. The code works for about ${minutesLeft(expiresAt)} minutes; connect_account gives a new one.`,
2250
- ...claim.kind === "sandbox" ? { endpoints: "The sandbox endpoints move into that account with what they captured." } : {}
560
+ ...sandbox ? {
561
+ endpoints: movesEndpoints ? "The sandbox endpoints move into that account with what they captured." : "The sandbox endpoints were made in an earlier session, so they stay in the sandbox until it expires; the account starts without them."
562
+ } : {}
2251
563
  });
2252
564
  })
2253
565
  )
2254
566
  );
2255
- registered.push(
567
+ tools.push(
2256
568
  server.tool(
2257
569
  "wait_for_connection",
2258
570
  `Wait for the human to enter the code from connect_account. On success the full webhooks.cc tools for their account replace these sandbox tools. Returns after timeoutSeconds (default ${DEFAULT_WAIT_SECONDS}) if they have not finished yet; call it again to keep waiting.`,
2259
571
  {
2260
- timeoutSeconds: z3.number().int().min(1).max(MAX_WAIT_SECONDS).optional().describe(`How long to wait, in seconds (default ${DEFAULT_WAIT_SECONDS})`)
572
+ timeoutSeconds: z2.number().int().min(1).max(MAX_WAIT_SECONDS).optional().describe(`How long to wait, in seconds (default ${DEFAULT_WAIT_SECONDS})`)
2261
573
  },
2262
574
  withErrorHandling(async ({ timeoutSeconds }) => {
2263
- if (session.connected) {
2264
- return jsonContent({ connected: true, message: "Already connected to an account." });
2265
- }
2266
- const claim = session.claim;
2267
- if (!claim) {
575
+ const pending = session.connect;
576
+ if (!pending) {
2268
577
  throw new Error("Nothing to wait for: call connect_account first.");
2269
578
  }
2270
- let claimed;
579
+ const timeout = (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
580
+ const expiresAt = pending.via === "sandbox" ? pending.attempt.expiresAt : pending.pending.expiresAt;
581
+ let client;
2271
582
  try {
2272
- claimed = await WebhooksCC2.agent.waitForClaim({
2273
- baseUrl,
2274
- claimToken: claim.claimToken,
2275
- interval: claim.attempt.interval,
2276
- timeout: (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3
2277
- });
583
+ client = pending.via === "sandbox" ? await pending.sandbox.waitForClaim({ timeout }) : await pending.pending.wait({ timeout });
2278
584
  } catch (error) {
2279
585
  if (!(error instanceof AgentAuthError)) throw error;
2280
586
  if (error.code === "timeout") {
2281
587
  return jsonContent({
2282
588
  connected: false,
2283
589
  status: "waiting",
2284
- message: `Not connected yet. The code works until ${claim.attempt.expiresAt.toISOString()}; call wait_for_connection again.`
590
+ message: `Not connected yet. The code works until ${expiresAt.toISOString()}; call wait_for_connection again.`
2285
591
  });
2286
592
  }
2287
593
  if (error.code === "expired_token") {
@@ -2300,34 +606,25 @@ function registerSandboxTools(server, options = {}) {
2300
606
  }
2301
607
  throw error;
2302
608
  }
2303
- const client = new WebhooksCC2({
2304
- baseUrl,
2305
- webhookUrl: session.webhookUrl,
2306
- getAccessToken: claimedTokens(baseUrl, claimed.identityAssertion, claimed.token)
2307
- });
2308
- session.connected = true;
2309
- session.claim = null;
2310
- session.forgetSandbox();
2311
- for (const tool of registered) tool.remove();
2312
- registerTools(server, client);
2313
- options.onConnected?.(client);
609
+ const movedEndpoints = pending.via === "sandbox" && pending.attempt.movesEndpoints;
610
+ session.reset();
611
+ showAccount(client);
2314
612
  return jsonContent({
2315
613
  connected: true,
2316
- email: claim.email,
2317
- scope: claimed.token.scope,
2318
- message: `Connected to the webhooks.cc account of ${claim.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${claim.kind === "sandbox" ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY.`
614
+ email: pending.email,
615
+ message: `Connected to the webhooks.cc account of ${pending.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${movedEndpoints ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} ${file ? `The connection is saved in ${file}, so later sessions, the whk CLI and the SDKs start connected; it lasts 90 days, and the human can remove it under Account, Connected agents.` : "This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY."}`
2319
616
  });
2320
617
  })
2321
618
  )
2322
619
  );
2323
- registered.push(
620
+ tools.push(
2324
621
  server.tool(
2325
622
  "about_sandbox",
2326
623
  "Explain what this agent can do without a webhooks.cc account, the sandbox limits, and how to connect an account. No network call.",
2327
624
  {},
2328
625
  withErrorHandling(async () => {
2329
626
  return jsonContent({
2330
- mode: "sandbox (no WHK_API_KEY)",
627
+ mode: "sandbox (no WHK_API_KEY, no saved connection)",
2331
628
  sandbox: {
2332
629
  maxEndpoints: SANDBOX.maxEndpoints,
2333
630
  requestsPerEndpoint: SANDBOX.requestsPerEndpoint,
@@ -2338,24 +635,26 @@ function registerSandboxTools(server, options = {}) {
2338
635
  notAvailable: "Mock responses, response rules, notifications, signature verification, email capture, forwarding, teams and sending webhooks."
2339
636
  },
2340
637
  connectAnAccount: [
2341
- "Call connect_account with the human's email and show them the link and code it returns.",
638
+ "Call connect_account with the email the human signs in to webhooks.cc with, and show them the link and code it returns.",
2342
639
  "Call wait_for_connection until it reports connected; the full tools for their account then replace the sandbox tools.",
2343
- "Sandbox endpoints move into the account with their captured requests."
640
+ "Sandbox endpoints registered by this server process move into the account with their captured requests; a sandbox resumed from an earlier session stays in the sandbox (connect_account says which)."
2344
641
  ],
642
+ savedIn: file ? `${file} (shared with the whk CLI and the SDKs): the sandbox and the connection survive restarts.` : "nowhere: this session only.",
2345
643
  withAnApiKey: "Set WHK_API_KEY to a whcc_ key from the dashboard and restart for the full tools from the start.",
2346
644
  docs: DOCS_URL,
2347
- protocol: `${baseUrl}/auth.md`
645
+ protocol: `${session.baseUrl}/auth.md`
2348
646
  });
2349
647
  })
2350
648
  )
2351
649
  );
650
+ return tools;
2352
651
  }
2353
652
  function registerAgentRegistrationTools(server) {
2354
653
  registerSandboxTools(server);
2355
654
  }
2356
655
 
2357
656
  // src/index.ts
2358
- var VERSION = true ? "1.14.0" : "0.0.0-dev";
657
+ var VERSION = true ? "1.15.0" : "0.0.0-dev";
2359
658
  function createServer(options = {}) {
2360
659
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2361
660
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -2366,17 +665,18 @@ function createServer(options = {}) {
2366
665
  { debouncedNotificationMethods: ["notifications/tools/list_changed"] }
2367
666
  );
2368
667
  if (!apiKey) {
2369
- registerSandboxTools(server, { baseUrl, webhookUrl });
668
+ registerSandboxTools(server, { baseUrl, webhookUrl, credentialsFile: options.credentialsFile });
2370
669
  registerPrompts(server);
2371
670
  return server;
2372
671
  }
2373
- const client = new WebhooksCC3({ apiKey, webhookUrl, baseUrl });
672
+ const client = new WebhooksCC2({ apiKey, webhookUrl, baseUrl });
2374
673
  registerTools(server, client);
2375
674
  registerPrompts(server);
2376
675
  registerResources(server, client);
2377
676
  return server;
2378
677
  }
2379
678
  export {
679
+ REMOTE_MAX_WAIT_MS,
2380
680
  createServer,
2381
681
  registerAgentRegistrationTools,
2382
682
  registerPrompts,