@webhooks-cc/mcp 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,1382 +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
- MAX_GLOB_PATTERN_LEN,
411
- NotFoundError,
412
- RateLimitError,
413
- TEMPLATE_METADATA,
414
- TEMPLATE_PROVIDERS,
415
- TimeoutError,
416
- UnauthorizedError,
417
- VERIFY_PROVIDERS,
418
- verifySignature,
419
- WebhooksCCError,
420
- extractCode,
421
- extractLink,
422
- detectWebhookInfo,
423
- generateRequestSchema,
424
- PAYLOAD_SCHEMA_FORMATS
258
+ AgentAuthError,
259
+ AgentCredentialStore,
260
+ SandboxClient,
261
+ WebhooksCC
425
262
  } from "@webhooks-cc/sdk";
426
- var MAX_BODY_SIZE = MAX_OUTPUT;
427
- var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
428
- var TIME_SEPARATOR = " \u2014 ";
429
- var httpUrlSchema = z2.string().url().refine(
430
- (value) => {
431
- try {
432
- const protocol = new URL(value).protocol;
433
- return protocol === "http:" || protocol === "https:";
434
- } catch {
435
- return false;
436
- }
437
- },
438
- { message: "Only http and https URLs are supported" }
439
- );
440
- var methodSchema = z2.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
441
- var kindSchema = z2.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
442
- var EMAIL_SCAN_LIMIT = 100;
443
- var durationOrTimestampSchema = z2.union([z2.string(), z2.number()]);
444
- var ruleConditionSchema = z2.object({
445
- field: z2.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
446
- op: z2.enum(["eq", "contains", "starts_with", "matches", "exists"]).describe("Comparison operator"),
447
- value: z2.string().max(MAX_CONDITION_VALUE_LEN).optional().describe(
448
- `Value to compare against (required unless op is "exists", max ${MAX_CONDITION_VALUE_LEN} chars)`
449
- ),
450
- name: z2.string().max(MAX_CONDITION_NAME_LEN).optional().describe(
451
- `Header name or query param name (required for header/query, max ${MAX_CONDITION_NAME_LEN} chars)`
452
- ),
453
- path: z2.string().max(MAX_CONDITION_PATH_LEN).optional().describe(
454
- `JSON dot-notation path (required for body_path, max ${MAX_CONDITION_PATH_LEN} chars)`
455
- )
456
- }).refine((c) => c.op === "exists" || c.value !== void 0 && c.value.length > 0, {
457
- message: 'value is required when op is not "exists"'
458
- }).refine(
459
- (c) => !(c.field === "header" || c.field === "query") || c.name !== void 0 && c.name.length > 0,
460
- { message: "name is required for header and query conditions" }
461
- ).refine((c) => c.field !== "body_path" || c.path !== void 0 && c.path.length > 0, {
462
- message: "path is required for body_path conditions"
463
- }).refine((c) => c.op !== "matches" || !c.value || c.value.length <= MAX_GLOB_PATTERN_LEN, {
464
- message: `matches pattern must be ${MAX_GLOB_PATTERN_LEN} chars or less`
465
- });
466
- var responseRuleSchema = z2.object({
467
- name: z2.string().max(MAX_RULE_NAME_LEN).optional().describe(`Human-readable rule name (max ${MAX_RULE_NAME_LEN} chars)`),
468
- enabled: z2.boolean().optional().default(true).describe("Whether this rule is active"),
469
- logic: z2.enum(["and", "or"]).optional().default("and").describe('How to combine conditions: "and" (all match) or "or" (any match)'),
470
- conditions: z2.array(ruleConditionSchema).min(1).max(10).describe("Conditions to evaluate (1-10)"),
471
- response: z2.lazy(() => mockResponseSchema).describe("Response when conditions match")
472
- });
473
- var responseRulesSchema = z2.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
474
- var mockResponseSchema = z2.object({
475
- 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})`),
476
- body: z2.string().default("").describe("Response body string (default: empty)"),
477
- headers: z2.record(z2.string(), z2.string()).default({}).describe("Response headers (default: none)"),
478
- delay: z2.number().int().min(MOCK_RESPONSE_DELAY_MIN).max(MOCK_RESPONSE_DELAY_MAX).optional().describe(
479
- `Response delay in milliseconds (${MOCK_RESPONSE_DELAY_MIN}-${MOCK_RESPONSE_DELAY_MAX}, default: none)`
480
- )
481
- });
482
- var createEndpointShape = {
483
- name: z2.string().optional().describe("Display name for the endpoint"),
484
- ephemeral: z2.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
485
- expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"'),
486
- mockResponse: mockResponseSchema.optional().describe("Optional mock response to return when the endpoint receives a request"),
487
- responseRules: responseRulesSchema.optional().describe(
488
- "Conditional response rules. Each rule has conditions and a response. First matching rule wins."
489
- ),
490
- notificationUrl: z2.string().url().optional().describe(
491
- "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
492
- ),
493
- signingProvider: z2.string().optional().describe(
494
- "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
495
- ),
496
- signingSecret: z2.string().optional().describe(
497
- "Signing secret (encrypted server-side, never returned). Required when signingProvider is set."
498
- ),
499
- signingHeader: z2.string().optional().describe("Custom signature header name. Only used with generic-hmac provider.")
500
- };
501
- var listEndpointsShape = {
502
- team: z2.string().min(1).optional().describe(
503
- "Team id or name (case-insensitive). Keeps endpoints shared with you from that team and endpoints you own that are shared with it."
504
- )
505
- };
506
- var getEndpointShape = { slug: z2.string().describe("The endpoint slug") };
507
- var deleteEndpointShape = { slug: z2.string().describe("The endpoint slug to delete") };
508
- var listRequestsShape = {
509
- endpointSlug: z2.string().describe("The endpoint slug"),
510
- limit: z2.number().int().min(1).max(100).default(25).describe("Max requests to return"),
511
- since: z2.number().optional().describe("Only return requests after this timestamp in ms"),
512
- kind: kindSchema
513
- };
514
- var getRequestShape = { requestId: z2.string().describe("The request ID") };
515
- var waitForRequestShape = {
516
- endpointSlug: z2.string().describe("The endpoint slug to monitor"),
517
- timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
518
- pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
519
- };
520
- var payloadSchemaShape = {
521
- endpointSlug: z2.string().describe("The endpoint slug"),
522
- format: z2.enum(PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
523
- 'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
524
- ),
525
- name: z2.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
526
- event: z2.string().optional().describe(
527
- 'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
528
- ),
529
- method: z2.string().optional().describe("Only requests with this HTTP method"),
530
- path: z2.string().optional().describe("Only requests with this path"),
531
- requestIds: z2.array(z2.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
532
- limit: z2.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
533
- };
534
- function eventName(event) {
535
- const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
536
- const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
537
- if (!name) return "WebhookPayload";
538
- return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
539
- }
540
- function payloadSchemaResult(requests, args) {
541
- const method = args.method?.toUpperCase();
542
- const matching = requests.filter(
543
- (request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args.path || request.path === args.path) && (!args.event || detectWebhookInfo(request)?.event === args.event)
544
- );
545
- if (matching.length === 0) {
546
- throw new Error(
547
- `None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
548
- );
549
- }
550
- const events = [
551
- ...new Set(
552
- matching.map((request) => detectWebhookInfo(request)?.event).filter((e) => !!e)
553
- )
554
- ];
555
- const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
556
- const result = generateRequestSchema(matching, { format: args.format, name });
557
- if (result.samples === 0) {
558
- throw new Error(
559
- `None of the ${matching.length} matching requests has a JSON or form body to learn from.`
560
- );
561
- }
562
- const output = {
563
- format: result.format,
564
- name: result.name,
565
- samples: result.samples,
566
- skipped: result.skipped,
567
- ...events.length > 0 ? { events } : {},
568
- ...events.length > 1 ? {
569
- warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
570
- } : {},
571
- code: result.code
572
- };
573
- const size = JSON.stringify(output, null, 2).length;
574
- if (size > MAX_OUTPUT) {
575
- throw new Error(
576
- `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.`
577
- );
578
- }
579
- return jsonContent(output);
580
- }
581
- function textContent(text) {
582
- return { content: [{ type: "text", text }] };
583
- }
584
- function serializeJson(value, limit = MAX_BODY_SIZE) {
585
- const full = JSON.stringify(value, null, 2);
586
- if (full.length <= limit) {
587
- return full;
588
- }
589
- if (Array.isArray(value)) {
590
- let low = 0;
591
- let high = value.length;
592
- let best = JSON.stringify(
593
- { items: [], truncated: true, total: value.length, returned: 0 },
594
- null,
595
- 2
596
- );
597
- while (low <= high) {
598
- const mid = Math.floor((low + high) / 2);
599
- const candidate = JSON.stringify(
600
- {
601
- items: value.slice(0, mid),
602
- truncated: true,
603
- total: value.length,
604
- returned: mid
605
- },
606
- null,
607
- 2
608
- );
609
- if (candidate.length <= limit) {
610
- best = candidate;
611
- low = mid + 1;
612
- } else {
613
- high = mid - 1;
614
- }
615
- }
616
- return best;
617
- }
618
- return full.slice(0, limit) + `
619
- ... [truncated, ${full.length} chars total]`;
620
- }
621
- function fitArrayField(value, key) {
622
- const items = value[key];
623
- if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
624
- const cut = (count) => ({
625
- ...value,
626
- [key]: items.slice(0, count),
627
- truncated: true,
628
- returned: count
629
- });
630
- let low = 0;
631
- let high = items.length;
632
- while (low < high) {
633
- const mid = Math.ceil((low + high) / 2);
634
- if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
635
- else high = mid - 1;
636
- }
637
- return cut(low);
638
- }
639
- function jsonContent(value) {
640
- return textContent(serializeJson(value));
641
- }
642
- async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
643
- const text = await response.text();
644
- if (text.length <= limit) return text;
645
- return text.slice(0, limit) + `
646
- ... [truncated, ${text.length} chars total]`;
647
- }
648
- function sleep(ms) {
649
- return new Promise((resolve) => setTimeout(resolve, ms));
650
- }
651
- function splitHint(message) {
652
- const separatorIndex = message.indexOf(TIME_SEPARATOR);
653
- if (separatorIndex === -1) {
654
- return { message, hint: null };
655
- }
656
- return {
657
- message: message.slice(0, separatorIndex),
658
- hint: message.slice(separatorIndex + TIME_SEPARATOR.length) || null
659
- };
660
- }
661
- function serializeError(error) {
662
- const rawMessage = error instanceof Error ? error.message : String(error);
663
- const { message, hint } = splitHint(rawMessage);
664
- const payload = {
665
- error: true,
666
- code: "validation_error",
667
- message,
668
- hint,
669
- retryAfter: null
670
- };
671
- if (error instanceof UnauthorizedError) {
672
- payload.code = "unauthorized";
673
- } else if (error instanceof NotFoundError) {
674
- payload.code = "not_found";
675
- } else if (error instanceof RateLimitError) {
676
- payload.code = "rate_limited";
677
- payload.retryAfter = error.retryAfter ?? null;
678
- } else if (error instanceof TimeoutError) {
679
- payload.code = "timeout";
680
- } else if (error instanceof WebhooksCCError) {
681
- payload.code = error.statusCode >= 500 ? "server_error" : "validation_error";
682
- }
683
- return JSON.stringify(payload, null, 2);
684
- }
685
- function withErrorHandling(handler) {
686
- return async (args) => {
687
- try {
688
- return await handler(args);
689
- } catch (error) {
690
- return { ...textContent(serializeError(error)), isError: true };
691
- }
692
- };
693
- }
694
- function filterRequestsByMethod(requests, method) {
695
- if (!method) {
696
- return requests;
697
- }
698
- const target = method.toUpperCase();
699
- return requests.filter((request) => request.method.toUpperCase() === target);
700
- }
701
- async function waitForMultipleRequests(client, endpointSlug, options) {
702
- const timeoutMs = typeof options.timeout === "number" ? options.timeout : options.timeout ? Number.isNaN(Number(options.timeout)) ? parseDurationLike(options.timeout) : Number(options.timeout) : 3e4;
703
- const pollIntervalMs = typeof options.pollInterval === "number" ? options.pollInterval : options.pollInterval ? Number.isNaN(Number(options.pollInterval)) ? parseDurationLike(options.pollInterval) : Number(options.pollInterval) : 500;
704
- const startedAt = Date.now();
705
- let since = startedAt;
706
- const seenIds = /* @__PURE__ */ new Set();
707
- const requests = [];
708
- while (Date.now() - startedAt < timeoutMs) {
709
- const checkTime = Date.now();
710
- const page = await client.requests.list(endpointSlug, {
711
- since,
712
- limit: Math.max(100, options.count * 5)
713
- });
714
- since = checkTime;
715
- const filtered = filterRequestsByMethod(page, options.method).slice().sort((left, right) => left.receivedAt - right.receivedAt);
716
- for (const request of filtered) {
717
- if (seenIds.has(request.id)) {
718
- continue;
719
- }
720
- seenIds.add(request.id);
721
- requests.push(request);
722
- if (requests.length >= options.count) {
723
- return {
724
- requests,
725
- complete: true,
726
- timedOut: false,
727
- expectedCount: options.count
728
- };
729
- }
730
- }
731
- await sleep(Math.max(10, pollIntervalMs));
732
- }
733
- return {
734
- requests,
735
- complete: requests.length >= options.count,
736
- timedOut: true,
737
- expectedCount: options.count
738
- };
739
- }
740
- function parseDurationLike(value) {
741
- const trimmed = value.trim();
742
- if (trimmed.length === 0) {
743
- throw new Error("Duration value cannot be empty");
744
- }
745
- const numeric = Number(trimmed);
746
- if (!Number.isNaN(numeric)) {
747
- return numeric;
748
- }
749
- const match = trimmed.match(/^(\d+)\s*(ms|s|m|h|d)$/i);
750
- if (!match) {
751
- throw new Error(`Invalid duration: "${value}"`);
752
- }
753
- const amount = Number(match[1]);
754
- const unit = match[2].toLowerCase();
755
- const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
756
- return amount * multiplier;
757
- }
758
- function ensureVerifyArgs(args) {
759
- if (args.provider === "discord") {
760
- const publicKey = args.publicKey?.trim();
761
- if (!publicKey) {
762
- throw new Error('verify_signature for provider "discord" requires publicKey');
763
- }
764
- return {
765
- provider: "discord",
766
- publicKey
767
- };
768
- }
769
- const secret = args.secret?.trim();
770
- if (!secret) {
771
- throw new Error(`verify_signature for provider "${args.provider}" requires secret`);
772
- }
773
- return {
774
- provider: args.provider,
775
- secret,
776
- ...args.url ? { url: args.url } : {},
777
- ...args.method ? { method: args.method } : {}
778
- };
779
- }
780
- async function summarizeResponse(response) {
781
- return {
782
- status: response.status,
783
- statusText: response.statusText,
784
- body: await readBodyTruncated(response)
785
- };
786
- }
787
- async function emailEndpointView(client, slug) {
788
- const endpoint = await client.endpoints.get(slug);
789
- return {
790
- endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
791
- includeExtracts: endpoint.showEmailExtracts !== false
792
- };
793
- }
794
- function emailSlugFromAddress(address) {
795
- const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
796
- return local.split("+")[0].toLowerCase();
797
- }
798
- var MAX_SUMMARY_LINK = 8e3;
799
- function boundedLink(link, max) {
800
- if (link == null || link.length <= max) return { link: link ?? null };
801
- return {
802
- link: null,
803
- linkOmitted: `The main link is ${link.length} characters long, too long for the output`
804
- };
805
- }
806
- var MAX_SUMMARY_FIELD = 1e3;
807
- function summarizeEmail(email, includeExtracts) {
808
- const from = email.email.from[0];
809
- const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
810
- const subject = email.email.subject;
811
- return {
812
- id: email.id,
813
- receivedAt: new Date(email.receivedAt).toISOString(),
814
- address: email.path,
815
- tag: email.email.tag,
816
- subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
817
- from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
818
- ...includeExtracts ? { code: extractCode(email), ...boundedLink(extractLink(email), MAX_SUMMARY_LINK) } : {},
819
- attachments: email.email.attachments.length
820
- };
821
- }
822
- var ESSENTIAL_DETAIL_FIELDS = [
823
- "code",
824
- "link",
825
- "id",
826
- "endpoint",
827
- "receivedAt",
828
- "address",
829
- "tag",
830
- "subject",
831
- "from",
832
- "size",
833
- "test",
834
- "spam",
835
- "text",
836
- "textTruncated",
837
- "htmlSize",
838
- "html",
839
- "htmlTruncated"
840
- ];
841
- function emailDetail(client, email, view, includeHtml) {
842
- const { data } = client.emails.toJson(email, {
843
- endpoint: view.endpoint,
844
- includeExtracts: view.includeExtracts
845
- });
846
- const { html, text: fullText, ...rest } = data;
847
- const { text, cut } = cutEmailText(fullText ?? null);
848
- const restoreText = () => {
849
- detail.text = text;
850
- if (cut) detail.textTruncated = true;
851
- else delete detail.textTruncated;
852
- };
853
- const detail = {
854
- ...view.includeExtracts ? { code: extractCode(email), link: extractLink(email) } : {},
855
- ...rest,
856
- text,
857
- ...cut ? { textTruncated: true } : {},
858
- htmlSize: html?.length ?? 0,
859
- ...includeHtml && html !== null ? { html } : {}
860
- };
861
- shrinkToFit(detail, MAX_BODY_SIZE, [
862
- () => omitHeaders(detail),
863
- () => trimSpam(detail),
864
- () => capExtracts(detail),
865
- () => capLists(detail),
866
- () => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
867
- () => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
868
- // Down to the essentials (and without a main link too long to use) there
869
- // may be room for more of the text again.
870
- () => {
871
- keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
872
- if (view.includeExtracts) {
873
- Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
874
- }
875
- restoreText();
876
- cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
877
- },
878
- () => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
879
- ]);
880
- return detail;
881
- }
882
- function registerTools(server, client) {
883
- server.tool(
884
- "create_endpoint",
885
- "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
886
- createEndpointShape,
887
- withErrorHandling(
888
- async ({
889
- name,
890
- ephemeral,
891
- expiresIn,
892
- mockResponse,
893
- responseRules,
894
- notificationUrl,
895
- signingProvider,
896
- signingSecret,
897
- signingHeader
898
- }) => {
899
- if (signingProvider) {
900
- if (signingProvider !== "discord" && !signingSecret) {
901
- throw new Error(`signingSecret is required for provider "${signingProvider}"`);
902
- }
903
- if (signingProvider === "generic-hmac" && !signingHeader) {
904
- throw new Error("signingHeader is required for generic-hmac provider");
905
- }
906
- }
907
- if (signingSecret && !signingProvider) {
908
- throw new Error("signingProvider is required when signingSecret is set");
909
- }
910
- const endpoint = await client.endpoints.create({
911
- name,
912
- ephemeral,
913
- expiresIn,
914
- mockResponse,
915
- responseRules,
916
- notificationUrl
917
- });
918
- if (signingProvider && signingSecret) {
919
- try {
920
- await client.endpoints.update(endpoint.slug, {
921
- signingProvider,
922
- signingSecret,
923
- signingHeader
924
- });
925
- } catch (err) {
926
- return jsonContent({
927
- ...endpoint,
928
- warning: `Endpoint created but signing configuration failed: ${err instanceof Error ? err.message : String(err)}. Update endpoint "${endpoint.slug}" to add signing.`
929
- });
930
- }
931
- }
932
- return jsonContent(endpoint);
933
- }
934
- )
935
- );
936
- server.tool(
937
- "list_endpoints",
938
- "List webhook endpoints: those you own (with sharedWith) and those shared with you through teams (with fromTeam). Optionally keep only one team's endpoints.",
939
- listEndpointsShape,
940
- withErrorHandling(async ({ team }) => {
941
- const endpoints = await client.endpoints.list({ team });
942
- return jsonContent(endpoints);
943
- })
944
- );
945
- server.tool(
946
- "list_teams",
947
- "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.",
948
- {},
949
- withErrorHandling(async () => {
950
- const teams = await client.teams.list();
951
- return jsonContent(
952
- teams.map((team) => ({
953
- ...team,
954
- periodEnd: team.periodEnd ? new Date(team.periodEnd).toISOString() : null
955
- }))
956
- );
957
- })
958
- );
959
- server.tool(
960
- "list_team_members",
961
- "List a team's members and pending invites. Any member of the team may call this.",
962
- { teamId: z2.string().min(1).describe("Team id (from list_teams)") },
963
- withErrorHandling(async ({ teamId }) => {
964
- const result = await client.teams.members(teamId);
965
- return jsonContent(result);
966
- })
967
- );
968
- server.tool(
969
- "share_endpoint",
970
- "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.",
971
- {
972
- slug: z2.string().describe("Slug of an endpoint you own"),
973
- teamId: z2.string().min(1).describe("Team id (from list_teams)")
974
- },
975
- withErrorHandling(async ({ slug, teamId }) => {
976
- await client.teams.share(teamId, slug);
977
- return jsonContent({ shared: true, slug, teamId });
978
- })
979
- );
980
- server.tool(
981
- "unshare_endpoint",
982
- "Stop sharing an endpoint you own with a team. Works even when the team is suspended.",
983
- {
984
- slug: z2.string().describe("Slug of an endpoint you own"),
985
- teamId: z2.string().min(1).describe("Team id (from list_teams)")
986
- },
987
- withErrorHandling(async ({ slug, teamId }) => {
988
- await client.teams.unshare(teamId, slug);
989
- return jsonContent({ shared: false, slug, teamId });
990
- })
991
- );
992
- server.tool(
993
- "get_endpoint",
994
- "Get details for a specific webhook endpoint by slug.",
995
- getEndpointShape,
996
- withErrorHandling(async ({ slug }) => {
997
- const endpoint = await client.endpoints.get(slug);
998
- return jsonContent(endpoint);
999
- })
1000
- );
1001
- server.tool(
1002
- "update_endpoint",
1003
- "Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
1004
- {
1005
- slug: z2.string().describe("The endpoint slug to update"),
1006
- name: z2.string().optional().describe("New display name"),
1007
- mockResponse: mockResponseSchema.nullable().optional().describe("Default mock response (used when no rule matches), or null to clear"),
1008
- responseRules: responseRulesSchema.nullable().optional().describe("Conditional response rules (first match wins), or null to clear"),
1009
- notificationUrl: z2.string().url().nullable().optional().describe("Notification webhook URL, or null to clear it"),
1010
- signingProvider: z2.string().nullable().optional().describe(
1011
- "Signing provider for automatic verification (e.g. stripe, github), or null to disable"
1012
- ),
1013
- signingSecret: z2.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
1014
- signingHeader: z2.string().nullable().optional().describe(
1015
- "Custom signature header name (only for generic-hmac provider), or null to clear"
1016
- ),
1017
- showEmailExtracts: z2.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
1018
- },
1019
- withErrorHandling(
1020
- async ({
1021
- slug,
1022
- name,
1023
- mockResponse,
1024
- responseRules,
1025
- notificationUrl,
1026
- signingProvider,
1027
- signingSecret,
1028
- signingHeader,
1029
- showEmailExtracts
1030
- }) => {
1031
- const endpoint = await client.endpoints.update(slug, {
1032
- name,
1033
- mockResponse,
1034
- responseRules,
1035
- notificationUrl,
1036
- signingProvider,
1037
- signingSecret,
1038
- signingHeader,
1039
- showEmailExtracts
1040
- });
1041
- return jsonContent(endpoint);
1042
- }
1043
- )
1044
- );
1045
- server.tool(
1046
- "delete_endpoint",
1047
- "Delete a webhook endpoint and all its captured requests.",
1048
- deleteEndpointShape,
1049
- withErrorHandling(async ({ slug }) => {
1050
- await client.endpoints.delete(slug);
1051
- return textContent(`Endpoint "${slug}" deleted.`);
1052
- })
1053
- );
1054
- server.tool(
1055
- "create_endpoints",
1056
- "Create multiple webhook endpoints in one call.",
1057
- {
1058
- count: z2.number().int().min(1).max(20).describe("Number of endpoints to create"),
1059
- namePrefix: z2.string().optional().describe("Optional prefix for endpoint names"),
1060
- ephemeral: z2.boolean().optional().describe("Create temporary endpoints that auto-expire"),
1061
- expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"')
1062
- },
1063
- withErrorHandling(async ({ count, namePrefix, ephemeral, expiresIn }) => {
1064
- const endpoints = await Promise.all(
1065
- Array.from(
1066
- { length: count },
1067
- (_, index) => client.endpoints.create({
1068
- name: namePrefix ? `${namePrefix}-${index + 1}` : void 0,
1069
- ephemeral,
1070
- expiresIn
1071
- })
1072
- )
1073
- );
1074
- return jsonContent({ endpoints });
1075
- })
1076
- );
1077
- server.tool(
1078
- "delete_endpoints",
1079
- "Delete multiple webhook endpoints in one call.",
1080
- {
1081
- slugs: z2.array(z2.string()).min(1).max(100).describe("Endpoint slugs to delete")
1082
- },
1083
- withErrorHandling(async ({ slugs }) => {
1084
- const settled = await Promise.allSettled(
1085
- slugs.map(async (slug) => {
1086
- await client.endpoints.delete(slug);
1087
- return slug;
1088
- })
1089
- );
1090
- return jsonContent({
1091
- deleted: settled.filter(
1092
- (result) => result.status === "fulfilled"
1093
- ).map((result) => result.value),
1094
- failed: settled.flatMap(
1095
- (result, index) => result.status === "rejected" ? [
1096
- {
1097
- slug: slugs[index],
1098
- message: result.reason instanceof Error ? result.reason.message : String(result.reason)
1099
- }
1100
- ] : []
1101
- )
1102
- });
1103
- })
1104
- );
1105
- server.tool(
1106
- "send_webhook",
1107
- "Send a test webhook to a hosted endpoint. Supports provider templates and signing.",
1108
- {
1109
- slug: z2.string().describe("The endpoint slug to send to"),
1110
- method: methodSchema,
1111
- headers: z2.record(z2.string(), z2.string()).optional().describe("HTTP headers to include"),
1112
- body: z2.unknown().optional().describe("Request body"),
1113
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template to send with signed headers"),
1114
- template: z2.string().optional().describe("Provider-specific template preset"),
1115
- event: z2.string().optional().describe("Provider event or topic name"),
1116
- secret: z2.string().optional().describe(
1117
- "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1118
- )
1119
- },
1120
- withErrorHandling(
1121
- async ({ slug, method, headers, body, provider, template, event, secret }) => {
1122
- let response;
1123
- if (provider) {
1124
- const templateSecret = secret?.trim();
1125
- if (!templateSecret && TEMPLATE_METADATA[provider]?.secretRequired !== false) {
1126
- throw new Error(`send_webhook with provider "${provider}" requires a non-empty secret`);
1127
- }
1128
- response = await client.endpoints.sendTemplate(slug, {
1129
- provider,
1130
- template,
1131
- event,
1132
- secret: templateSecret,
1133
- method,
1134
- headers,
1135
- body
1136
- });
1137
- } else {
1138
- response = await client.endpoints.send(slug, { method, headers, body });
1139
- }
1140
- const responseBody = await readBodyTruncated(response);
1141
- return jsonContent({
1142
- status: response.status,
1143
- statusText: response.statusText,
1144
- body: responseBody
1145
- });
1146
- }
1147
- )
1148
- );
1149
- server.tool(
1150
- "list_requests",
1151
- "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
1152
- listRequestsShape,
1153
- withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1154
- const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1155
- return jsonContent(requests.map(compactRequest));
1156
- })
1157
- );
1158
- server.tool(
1159
- "search_requests",
1160
- "Search captured webhook requests across endpoints using retained full-text search.",
1161
- {
1162
- slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
1163
- method: z2.string().optional().describe("Filter by HTTP method"),
1164
- kind: kindSchema,
1165
- q: z2.string().optional().describe("Free-text search across path, body, and headers"),
1166
- from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
1167
- to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
1168
- limit: z2.number().int().min(1).max(200).default(50).describe("Max results to return"),
1169
- offset: z2.number().int().min(0).max(1e4).default(0).describe("Result offset"),
1170
- order: z2.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
1171
- },
1172
- withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
1173
- const results = await client.requests.search({
1174
- slug,
1175
- method,
1176
- kind,
1177
- q,
1178
- from,
1179
- to,
1180
- limit,
1181
- offset,
1182
- order
1183
- });
1184
- return jsonContent(results.map(compactRequest));
1185
- })
1186
- );
1187
- server.tool(
1188
- "count_requests",
1189
- "Count captured webhook requests that match the given filters.",
1190
- {
1191
- slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
1192
- method: z2.string().optional().describe("Filter by HTTP method"),
1193
- kind: kindSchema,
1194
- q: z2.string().optional().describe("Free-text search across path, body, and headers"),
1195
- from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
1196
- to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
1197
- },
1198
- withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
1199
- const count = await client.requests.count({ slug, method, kind, q, from, to });
1200
- return jsonContent({ count });
1201
- })
1202
- );
1203
- server.tool(
1204
- "get_request",
1205
- "Get full details for a specific captured request by ID.",
1206
- getRequestShape,
1207
- withErrorHandling(async ({ requestId }) => {
1208
- const request = await client.requests.get(requestId);
1209
- return jsonContent(compactRequest(request));
1210
- })
1211
- );
1212
- server.tool(
1213
- "wait_for_request",
1214
- "Wait for a request to arrive at an endpoint.",
1215
- waitForRequestShape,
1216
- withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
1217
- const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
1218
- return jsonContent(compactRequest(request));
1219
- })
1220
- );
1221
- server.tool(
1222
- "wait_for_requests",
1223
- "Wait for multiple requests to arrive at an endpoint.",
1224
- {
1225
- endpointSlug: z2.string().describe("The endpoint slug to monitor"),
1226
- count: z2.number().int().min(1).max(20).describe("Number of requests to collect"),
1227
- timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
1228
- pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"'),
1229
- method: z2.string().optional().describe("Only collect requests with this HTTP method")
1230
- },
1231
- withErrorHandling(async ({ endpointSlug, count, timeout, pollInterval, method }) => {
1232
- const result = await waitForMultipleRequests(client, endpointSlug, {
1233
- count,
1234
- timeout,
1235
- pollInterval,
1236
- method
1237
- });
1238
- return jsonContent(
1239
- fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
1240
- );
1241
- })
1242
- );
1243
- server.tool(
1244
- "replay_request",
1245
- "Replay a previously captured request to a target URL.",
1246
- {
1247
- requestId: z2.string().describe("The captured request ID"),
1248
- targetUrl: httpUrlSchema.describe("The URL to replay the request to")
1249
- },
1250
- withErrorHandling(async ({ requestId, targetUrl }) => {
1251
- const response = await client.requests.replay(requestId, targetUrl);
1252
- const responseBody = await readBodyTruncated(response);
1253
- return jsonContent({
1254
- status: response.status,
1255
- statusText: response.statusText,
1256
- body: responseBody
1257
- });
1258
- })
1259
- );
1260
- server.tool(
1261
- "compare_requests",
1262
- "Compare two captured requests and show the structured differences.",
1263
- {
1264
- leftRequestId: z2.string().describe("The first request ID"),
1265
- rightRequestId: z2.string().describe("The second request ID"),
1266
- ignoreHeaders: z2.array(z2.string()).optional().describe("Headers to ignore during comparison")
1267
- },
1268
- withErrorHandling(async ({ leftRequestId, rightRequestId, ignoreHeaders }) => {
1269
- const [leftRequest, rightRequest] = await Promise.all([
1270
- client.requests.get(leftRequestId),
1271
- client.requests.get(rightRequestId)
1272
- ]);
1273
- const diff = diffRequests(leftRequest, rightRequest, { ignoreHeaders });
1274
- return jsonContent(diff);
1275
- })
1276
- );
1277
- server.tool(
1278
- "generate_payload_schema",
1279
- "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.",
1280
- payloadSchemaShape,
1281
- withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
1282
- const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
1283
- return payloadSchemaResult(requests, args);
1284
- })
1285
- );
1286
- server.tool(
1287
- "extract_from_request",
1288
- "Extract specific JSON fields from a captured request body.",
1289
- {
1290
- requestId: z2.string().describe("The request ID"),
1291
- jsonPaths: z2.array(z2.string()).min(1).max(50).describe("Dot-notation JSON paths to extract")
1292
- },
1293
- withErrorHandling(async ({ requestId, jsonPaths }) => {
1294
- const request = await client.requests.get(requestId);
1295
- const extracted = Object.fromEntries(
1296
- jsonPaths.map((path) => [path, extractJsonField(request, path) ?? null])
1297
- );
1298
- return jsonContent(extracted);
1299
- })
1300
- );
1301
- server.tool(
1302
- "verify_signature",
1303
- "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.",
1304
- {
1305
- requestId: z2.string().describe("The captured request ID"),
1306
- provider: z2.enum(VERIFY_PROVIDERS).optional().describe("Provider whose signature scheme should be verified. Omit to use stored result."),
1307
- secret: z2.string().optional().describe(
1308
- "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."
1309
- ),
1310
- publicKey: z2.string().optional().describe("Discord application public key. Required for provider=discord."),
1311
- url: httpUrlSchema.optional().describe("Original signed URL. Required for Twilio, Square, and HubSpot verification."),
1312
- method: z2.string().optional().describe(
1313
- "Original HTTP method used when the signature was generated. Used by HubSpot v3 verification; the SDK assumes POST when omitted."
1314
- )
1315
- },
1316
- withErrorHandling(async ({ requestId, provider, secret, publicKey, url, method }) => {
1317
- const request = await client.requests.get(requestId);
1318
- if (!provider && !secret && !publicKey) {
1319
- const verified = request.signatureVerified;
1320
- if (verified === null || verified === void 0) {
1321
- return jsonContent({
1322
- valid: null,
1323
- details: "No signature verification configured on this endpoint. Pass provider and secret to verify client-side."
1324
- });
1325
- }
1326
- let errorData = null;
1327
- if (request.signatureError) {
1328
- try {
1329
- errorData = JSON.parse(request.signatureError);
1330
- } catch {
1331
- errorData = null;
1332
- }
1333
- }
1334
- const isSkipped = !verified && errorData?.code && (errorData.code === "missing_header" || errorData.code === "unsupported");
1335
- return jsonContent({
1336
- valid: isSkipped ? null : verified,
1337
- provider: request.signingProvider ?? null,
1338
- details: verified ? "Signature verified by server." : isSkipped ? `Verification skipped: ${errorData?.message ?? errorData?.code}` : `Signature invalid: ${request.signatureError ?? "unknown error"}`,
1339
- error: request.signatureError ?? null,
1340
- skipped: isSkipped ?? false
1341
- });
1342
- }
1343
- const verificationOptions = ensureVerifyArgs({
1344
- provider,
1345
- secret,
1346
- publicKey,
1347
- url,
1348
- method
1349
- });
1350
- const result = await verifySignature(request, verificationOptions);
1351
- return jsonContent({
1352
- valid: result.valid,
1353
- details: result.valid ? "Signature is valid." : "Signature did not match."
1354
- });
1355
- })
1356
- );
1357
- server.tool(
1358
- "clear_requests",
1359
- "Delete captured requests for an endpoint without deleting the endpoint itself.",
1360
- {
1361
- slug: z2.string().describe("The endpoint slug to clear"),
1362
- before: durationOrTimestampSchema.optional().describe('Only clear requests older than this timestamp or duration like "1h"')
1363
- },
1364
- withErrorHandling(async ({ slug, before }) => {
1365
- await client.requests.clear(slug, { before });
1366
- return jsonContent({ slug, cleared: true, before: before ?? null });
1367
- })
1368
- );
1369
- server.tool(
1370
- "send_to",
1371
- "Send a webhook directly to any URL with optional provider signing.",
1372
- {
1373
- url: httpUrlSchema.describe("Target URL"),
1374
- method: methodSchema,
1375
- headers: z2.record(z2.string(), z2.string()).optional().describe("HTTP headers to include"),
1376
- body: z2.unknown().optional().describe("Request body"),
1377
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template for signing"),
1378
- template: z2.string().optional().describe("Provider-specific template preset"),
1379
- event: z2.string().optional().describe("Provider event or topic name"),
1380
- secret: z2.string().optional().describe(
1381
- "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1382
- )
1383
- },
1384
- withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
1385
- const response = await client.sendTo(url, {
1386
- method,
1387
- headers,
1388
- body,
1389
- provider,
1390
- template,
1391
- event,
1392
- secret
1393
- });
1394
- const responseBody = await readBodyTruncated(response);
1395
- return jsonContent({
1396
- status: response.status,
1397
- statusText: response.statusText,
1398
- body: responseBody
1399
- });
1400
- })
1401
- );
1402
- server.tool(
1403
- "preview_webhook",
1404
- "Preview a webhook request without sending it. Returns the exact URL, method, headers, and body.",
1405
- {
1406
- url: httpUrlSchema.describe("Target URL"),
1407
- method: methodSchema,
1408
- headers: z2.record(z2.string(), z2.string()).optional().describe("HTTP headers to include"),
1409
- body: z2.unknown().optional().describe("Request body"),
1410
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template for signing"),
1411
- template: z2.string().optional().describe("Provider-specific template preset"),
1412
- event: z2.string().optional().describe("Provider event or topic name"),
1413
- secret: z2.string().optional().describe(
1414
- "Signing secret. Required for signed provider templates; omit for secretless providers (sendgrid, discord, plaid)."
1415
- )
1416
- },
1417
- withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
1418
- const preview = await client.buildRequest(url, {
1419
- method,
1420
- headers,
1421
- body,
1422
- provider,
1423
- template,
1424
- event,
1425
- secret
1426
- });
1427
- return jsonContent(preview);
1428
- })
1429
- );
1430
- server.tool(
1431
- "list_provider_templates",
1432
- "List supported webhook providers, templates, and signing metadata.",
1433
- {
1434
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Filter to a single provider")
1435
- },
1436
- withErrorHandling(async ({ provider }) => {
1437
- if (provider) {
1438
- return jsonContent([client.templates.get(provider)]);
1439
- }
1440
- return jsonContent(
1441
- client.templates.listProviders().map((name) => client.templates.get(name))
1442
- );
1443
- })
1444
- );
1445
- server.tool(
1446
- "get_usage",
1447
- "Check current request usage, remaining quota, plan, and period end, plus the pooled quota of every subscribed team you belong to.",
1448
- {},
1449
- withErrorHandling(async () => {
1450
- const [usageResult, teamsResult] = await Promise.allSettled([
1451
- client.usage(),
1452
- client.teams.list()
1453
- ]);
1454
- if (usageResult.status === "rejected") throw usageResult.reason;
1455
- const usage = usageResult.value;
1456
- const teams = teamsResult.status === "fulfilled" ? teamsResult.value : [];
1457
- const teamsError = teamsResult.status === "rejected" ? teamsResult.reason instanceof Error ? teamsResult.reason.message : String(teamsResult.reason) : void 0;
1458
- return jsonContent({
1459
- ...usage,
1460
- periodEnd: usage.periodEnd ? new Date(usage.periodEnd).toISOString() : null,
1461
- ...teamsError ? { teamsError } : {},
1462
- teams: teams.filter((team) => !team.suspended).map((team) => ({
1463
- id: team.id,
1464
- name: team.name,
1465
- role: team.role,
1466
- seats: team.seats,
1467
- used: team.requestsUsed,
1468
- limit: team.requestLimit,
1469
- remaining: Math.max(0, team.requestLimit - team.requestsUsed),
1470
- periodEnd: team.periodEnd ? new Date(team.periodEnd).toISOString() : null
1471
- }))
1472
- });
1473
- })
1474
- );
1475
- server.tool(
1476
- "test_webhook_flow",
1477
- "Run a full webhook test flow: create endpoint, optionally mock, send, wait, verify, replay, and clean up.",
1478
- {
1479
- provider: z2.enum(TEMPLATE_PROVIDERS).optional().describe("Optional provider template to use when sending the webhook"),
1480
- event: z2.string().optional().describe("Optional provider event or topic name"),
1481
- secret: z2.string().optional().describe(
1482
- "Signing secret. Required when provider is set or signature verification is enabled."
1483
- ),
1484
- mockStatus: z2.number().int().min(100).max(599).optional().describe("Optional mock response status to configure before sending"),
1485
- targetUrl: httpUrlSchema.optional().describe("Optional URL to replay the captured request to after capture"),
1486
- verifySignature: z2.boolean().default(false).describe("Verify the captured request signature after capture"),
1487
- cleanup: z2.boolean().default(true).describe("Delete the created endpoint after the flow completes")
1488
- },
1489
- withErrorHandling(
1490
- async ({
1491
- provider,
1492
- event,
1493
- secret,
1494
- mockStatus,
1495
- targetUrl,
1496
- verifySignature: shouldVerify,
1497
- cleanup
1498
- }) => {
1499
- const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s" });
1500
- if (mockStatus !== void 0) {
1501
- flow.setMock({
1502
- status: mockStatus,
1503
- body: "",
1504
- headers: {}
1505
- });
1506
- }
1507
- if (provider) {
1508
- const templateSecret = secret?.trim();
1509
- if (!templateSecret && TEMPLATE_METADATA[provider]?.secretRequired !== false) {
1510
- throw new Error(
1511
- `test_webhook_flow with provider "${provider}" requires a non-empty secret`
1512
- );
1513
- }
1514
- flow.sendTemplate({
1515
- provider,
1516
- event,
1517
- secret: templateSecret
1518
- });
1519
- if (shouldVerify) {
1520
- if (provider === "discord") {
1521
- throw new Error(
1522
- "test_webhook_flow cannot verify Discord signatures (Ed25519 requires a public key, not a secret)"
1523
- );
1524
- }
1525
- if (!templateSecret) {
1526
- throw new Error(
1527
- `test_webhook_flow cannot verify "${provider}" signatures \u2014 this provider's templates are unsigned`
1528
- );
1529
- }
1530
- flow.verifySignature({
1531
- provider,
1532
- secret: templateSecret
1533
- });
1534
- }
1535
- } else {
1536
- if (shouldVerify) {
1537
- throw new Error("test_webhook_flow cannot verify signatures without a provider");
1538
- }
1539
- flow.send();
1540
- }
1541
- if (targetUrl) {
1542
- flow.replayTo(targetUrl);
1543
- }
1544
- if (cleanup) {
1545
- flow.cleanup();
1546
- }
1547
- const result = await flow.run();
1548
- return jsonContent({
1549
- endpoint: result.endpoint,
1550
- request: result.request ?? null,
1551
- verification: result.verification ?? null,
1552
- replayResponse: result.replayResponse ? await summarizeResponse(result.replayResponse) : null,
1553
- cleanedUp: result.cleanedUp
1554
- });
1555
- }
1556
- )
1557
- );
1558
- server.tool(
1559
- "list_emails",
1560
- "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.",
1561
- {
1562
- endpointSlug: z2.string().describe("The endpoint slug"),
1563
- limit: z2.number().int().min(1).max(100).default(25).describe(
1564
- `Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
1565
- ),
1566
- since: z2.number().optional().describe("Only emails received after this timestamp in ms"),
1567
- tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
1568
- subject: z2.string().optional().describe("Only emails whose subject contains this text"),
1569
- from: z2.string().optional().describe("Only emails from this address (case-insensitive)")
1570
- },
1571
- withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
1572
- const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
1573
- const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
1574
- const [emails, view] = await Promise.all([
1575
- client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
1576
- emailEndpointView(client, endpointSlug)
1577
- ]);
1578
- return jsonContent(
1579
- emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
1580
- );
1581
- })
1582
- );
1583
- server.tool(
1584
- "get_email",
1585
- "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.",
1586
- {
1587
- requestId: z2.string().optional().describe("The email's request ID"),
1588
- endpointSlug: z2.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
1589
- tag: z2.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
1590
- includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
1591
- },
1592
- withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
1593
- if (requestId === void 0 === (endpointSlug === void 0)) {
1594
- throw new Error("Pass exactly one of requestId or endpointSlug");
1595
- }
1596
- const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
1597
- if (!email) {
1598
- throw new NotFoundError(
1599
- `No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
1600
- );
1601
- }
1602
- const view = await emailEndpointView(
1603
- client,
1604
- endpointSlug ?? emailSlugFromAddress(email.path)
1605
- );
1606
- return jsonContent(emailDetail(client, email, view, includeHtml));
1607
- })
1608
- );
1609
- server.tool(
1610
- "wait_for_email",
1611
- "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.",
1612
- {
1613
- endpointSlug: z2.string().describe("The endpoint slug"),
1614
- tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
1615
- subject: z2.string().optional().describe("Only emails whose subject contains this text"),
1616
- from: z2.string().optional().describe("Only emails from this address (case-insensitive)"),
1617
- timeout: durationOrTimestampSchema.default("30s").describe(
1618
- `How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
1619
- ),
1620
- since: z2.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
1621
- includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false)")
1622
- },
1623
- withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1624
- const [email, view] = await Promise.all([
1625
- client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1626
- emailEndpointView(client, endpointSlug)
1627
- ]);
1628
- return jsonContent(emailDetail(client, email, view, includeHtml));
1629
- })
1630
- );
1631
- server.tool(
1632
- "send_test_email",
1633
- "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.",
1634
- {
1635
- slug: z2.string().describe("The endpoint slug"),
1636
- tag: z2.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
1637
- },
1638
- withErrorHandling(async ({ slug, tag }) => {
1639
- const result = await client.emails.sendTest(slug, { tag });
1640
- return jsonContent({ ...result, address: client.emails.address(slug, tag) });
1641
- })
1642
- );
1643
- server.tool(
1644
- "configure_forwarding",
1645
- "Change an endpoint's forwarding: the URL, on or off, which kinds (HTTP requests, emails), the format, your own headers, and the advanced options. Only the arguments you pass change. Owner only. Saving the first URL creates the signing secret. A Slack or Discord webhook URL gets a chat message automatically.",
1646
- {
1647
- slug: z2.string().describe("The endpoint slug"),
1648
- url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1649
- enabled: z2.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)"),
1650
- http: z2.boolean().optional().describe("Forward HTTP requests"),
1651
- email: z2.boolean().optional().describe("Forward emails"),
1652
- format: z2.enum(["auto", "as_received", "json", "chat"]).optional().describe(
1653
- "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)"
1654
- ),
1655
- headers: z2.array(
1656
- z2.object({
1657
- name: z2.string().describe("Header name, e.g. Authorization"),
1658
- value: z2.string().nullable().describe("Header value, or null to keep the value stored under this name")
1659
- })
1660
- ).max(10).optional().describe(
1661
- "Your own headers for every delivery (not chat). Replaces the whole list; [] removes them"
1662
- ),
1663
- appendPath: z2.boolean().optional().describe("Add the path after the slug to the URL for relayed requests (default true)"),
1664
- retrySeconds: z2.union([z2.literal(0), z2.literal(3600), z2.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
1665
- keepOrder: z2.boolean().optional().describe("One delivery at a time, in capture order (default false)"),
1666
- sentField: z2.string().max(128).nullable().optional().describe(
1667
- "JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
1668
- )
1669
- },
1670
- withErrorHandling(async ({ slug, ...options }) => {
1671
- const endpoint = await client.forwarding.configure(slug, options);
1672
- return jsonContent({
1673
- slug: endpoint.slug,
1674
- forwardEnabled: endpoint.forwardEnabled ?? false,
1675
- forwardUrl: endpoint.forwardUrl ?? null,
1676
- hasForwardSecret: endpoint.hasForwardSecret ?? false,
1677
- forwardHttp: endpoint.forwardHttp ?? false,
1678
- forwardEmail: endpoint.forwardEmail ?? false,
1679
- forwardFormat: endpoint.forwardFormat ?? "auto",
1680
- forwardHeaders: endpoint.forwardHeaders ?? [],
1681
- forwardAppendPath: endpoint.forwardAppendPath ?? true,
1682
- forwardRetrySeconds: endpoint.forwardRetrySeconds ?? 86400,
1683
- forwardKeepOrder: endpoint.forwardKeepOrder ?? false,
1684
- forwardSentField: endpoint.forwardSentField ?? null
1685
- });
1686
- })
1687
- );
1688
- server.tool(
1689
- "get_forwarding_secret",
1690
- "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.",
1691
- {
1692
- slug: z2.string().describe("The endpoint slug"),
1693
- rotate: z2.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
1694
- },
1695
- withErrorHandling(async ({ slug, rotate }) => {
1696
- const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
1697
- return jsonContent({ secret, rotated: rotate });
1698
- })
1699
- );
1700
- server.tool(
1701
- "test_forwarding",
1702
- "Send the endpoint's newest captured request of a kind (or a sample) to its forwarding URL once, exactly as a delivery would go out, and report the answer. Works with forwarding on or off; nothing is retried or logged.",
1703
- {
1704
- slug: z2.string().describe("The endpoint slug"),
1705
- kind: z2.enum(["http", "email"]).optional().describe(
1706
- "Which kind to send (default: http when HTTP requests are forwarded, else email)"
1707
- )
1708
- },
1709
- withErrorHandling(async ({ slug, kind }) => {
1710
- return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1711
- })
1712
- );
1713
- server.tool(
1714
- "list_deliveries",
1715
- "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.",
1716
- {
1717
- endpointSlug: z2.string().optional().describe("List the endpoint's delivery log"),
1718
- requestId: z2.string().optional().describe("List every delivery and try of this request"),
1719
- limit: z2.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1720
- status: z2.enum(["all", "pending", "failed"]).default("all").describe("With endpointSlug: all, pending (queued or retrying) or failed"),
1721
- before: z2.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1722
- },
1723
- withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
1724
- if (requestId === void 0 === (endpointSlug === void 0)) {
1725
- throw new Error("Pass exactly one of endpointSlug or requestId");
1726
- }
1727
- if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1728
- const [summary, deliveries] = await Promise.all([
1729
- client.forwarding.summary(endpointSlug),
1730
- client.forwarding.deliveries(endpointSlug, { limit, status, before })
1731
- ]);
1732
- return jsonContent({
1733
- summary,
1734
- deliveries,
1735
- nextCursor: deliveries.length === limit ? deliveries[deliveries.length - 1]?.cursor ?? null : null
1736
- });
1737
- })
1738
- );
1739
- server.tool(
1740
- "redeliver",
1741
- "Forward again with the endpoint's current settings: one captured request or email (requestId; it also sends one that was never forwarded), or every request whose latest delivery failed (endpointSlug, oldest first, up to 1,000 at a time). Owner only; fails while forwarding is off.",
1742
- {
1743
- requestId: z2.string().optional().describe("The request or email to send again"),
1744
- endpointSlug: z2.string().optional().describe("Send every failed delivery of this endpoint again")
1745
- },
1746
- withErrorHandling(async ({ requestId, endpointSlug }) => {
1747
- if (requestId === void 0 === (endpointSlug === void 0)) {
1748
- throw new Error("Pass exactly one of requestId or endpointSlug");
1749
- }
1750
- return jsonContent(
1751
- requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
1752
- );
1753
- })
1754
- );
1755
- server.tool(
1756
- "describe",
1757
- "Describe all available SDK operations, parameters, and types.",
1758
- {},
1759
- withErrorHandling(async () => {
1760
- const description = client.describe();
1761
- return jsonContent(description);
1762
- })
1763
- );
1764
- }
1765
-
1766
- // src/sandbox-tools.ts
1767
263
  var SANDBOX = {
1768
264
  maxEndpoints: 3,
1769
265
  requestsPerEndpoint: 25,
@@ -1772,7 +268,6 @@ var SANDBOX = {
1772
268
  };
1773
269
  var DEFAULT_WAIT_SECONDS = 45;
1774
270
  var MAX_WAIT_SECONDS = 600;
1775
- var TOKEN_REFRESH_MARGIN_MS = 6e4;
1776
271
  var DOCS_URL = "https://webhooks.cc/docs/agents";
1777
272
  var ACCOUNT_ONLY_OPTIONS = [
1778
273
  "mockResponse",
@@ -1783,20 +278,29 @@ var ACCOUNT_ONLY_OPTIONS = [
1783
278
  "signingHeader"
1784
279
  ];
1785
280
  var AgentSession = class {
1786
- constructor(baseUrl, webhookUrl, clientName) {
281
+ constructor(baseUrl, webhookUrl, clientName, credentialsFile) {
1787
282
  this.baseUrl = baseUrl;
1788
283
  this.webhookUrl = webhookUrl;
1789
284
  this.clientName = clientName;
285
+ this.credentialsFile = credentialsFile;
1790
286
  this.sandbox = null;
1791
- this.claim = null;
1792
- 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
+ };
1793
296
  }
1794
297
  /**
1795
- * The sandbox, registered on first use and again after it expired. Tool
1796
- * calls can run in parallel: this.sandbox is read again after every await,
1797
- * and a new sandbox is assigned with no await in between, so parallel
1798
- * calls share one registration. Calls waiting on a registration that fails
1799
- * 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.
1800
304
  */
1801
305
  async sandboxClient() {
1802
306
  while (this.sandbox) {
@@ -1804,35 +308,36 @@ var AgentSession = class {
1804
308
  const sandbox = await pending;
1805
309
  if (!this.expired(pending, sandbox)) return sandbox;
1806
310
  }
1807
- const created = WebhooksCC2.sandbox({
1808
- baseUrl: this.baseUrl,
1809
- webhookUrl: this.webhookUrl,
1810
- clientName: this.clientName
1811
- });
311
+ const created = WebhooksCC.sandbox(this.sandboxOptions());
1812
312
  this.sandbox = created;
1813
313
  created.catch(() => {
1814
314
  if (this.sandbox === created) this.sandbox = null;
1815
315
  });
1816
316
  return created;
1817
317
  }
1818
- /** The live sandbox, if one was registered; never registers one. */
318
+ /** The live sandbox, from this session or the credential file; never registers one. */
1819
319
  async existingSandbox() {
1820
320
  const pending = this.sandbox;
1821
- if (!pending) return null;
1822
- let sandbox;
1823
- try {
1824
- sandbox = await pending;
1825
- } catch {
1826
- 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;
1827
329
  }
1828
- 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;
1829
334
  }
1830
335
  /** True when the sandbox is past its 24 hours; then it is dropped. */
1831
336
  expired(pending, sandbox) {
1832
337
  if (sandbox.expiresAt.getTime() > Date.now()) return false;
1833
338
  if (this.sandbox === pending) this.sandbox = null;
1834
- if (this.claim?.kind === "sandbox" && this.claim.claimToken === sandbox.registration.claimToken) {
1835
- this.claim = null;
339
+ if (this.connect?.via === "sandbox" && this.connect.sandbox === sandbox) {
340
+ this.connect = null;
1836
341
  }
1837
342
  return true;
1838
343
  }
@@ -1843,26 +348,11 @@ var AgentSession = class {
1843
348
  }
1844
349
  return sandbox;
1845
350
  }
1846
- forgetSandbox() {
351
+ reset() {
1847
352
  this.sandbox = null;
353
+ this.connect = null;
1848
354
  }
1849
355
  };
1850
- function claimedTokens(baseUrl, assertion, first) {
1851
- let current = first;
1852
- let pending = null;
1853
- return async (options) => {
1854
- if (!options?.forceRefresh && current.expiresAt.getTime() - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
1855
- return current.accessToken;
1856
- }
1857
- if (!pending) {
1858
- pending = WebhooksCC2.agent.exchange({ baseUrl, assertion }).finally(() => {
1859
- pending = null;
1860
- });
1861
- }
1862
- current = await pending;
1863
- return current.accessToken;
1864
- };
1865
- }
1866
356
  function minutesLeft(until) {
1867
357
  return Math.max(1, Math.round((until.getTime() - Date.now()) / 6e4));
1868
358
  }
@@ -1870,16 +360,53 @@ function onlyKind(requests, kind) {
1870
360
  if (!kind) return requests;
1871
361
  return requests.filter((request) => (request.kind ?? "http") === kind);
1872
362
  }
363
+ function savedTo(credentialsFile) {
364
+ return AgentCredentialStore.resolve(credentialsFile)?.path ?? null;
365
+ }
1873
366
  function registerSandboxTools(server, options = {}) {
1874
367
  const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL ?? "https://webhooks.cc";
368
+ const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
1875
369
  const session = new AgentSession(
1876
370
  baseUrl,
1877
- options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
1878
- options.clientName ?? "webhooks-cc-mcp"
1879
- );
1880
- 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 = [];
1881
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`;
1882
- registered.push(
408
+ const file = savedTo(session.credentialsFile);
409
+ tools.push(
1883
410
  server.tool(
1884
411
  "create_endpoint",
1885
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.`,
@@ -1903,7 +430,7 @@ function registerSandboxTools(server, options = {}) {
1903
430
  })
1904
431
  )
1905
432
  );
1906
- registered.push(
433
+ tools.push(
1907
434
  server.tool(
1908
435
  "list_endpoints",
1909
436
  "List this agent's sandbox endpoints. Empty until create_endpoint starts the sandbox.",
@@ -1917,7 +444,7 @@ function registerSandboxTools(server, options = {}) {
1917
444
  })
1918
445
  )
1919
446
  );
1920
- registered.push(
447
+ tools.push(
1921
448
  server.tool(
1922
449
  "get_endpoint",
1923
450
  "Get one of this agent's sandbox endpoints by slug, with its sandbox limits and usage.",
@@ -1927,7 +454,7 @@ function registerSandboxTools(server, options = {}) {
1927
454
  })
1928
455
  )
1929
456
  );
1930
- registered.push(
457
+ tools.push(
1931
458
  server.tool(
1932
459
  "delete_endpoint",
1933
460
  "Delete one of this agent's sandbox endpoints and its captured requests. Frees a slot; the sandbox's request budget is not refunded.",
@@ -1938,7 +465,7 @@ function registerSandboxTools(server, options = {}) {
1938
465
  })
1939
466
  )
1940
467
  );
1941
- registered.push(
468
+ tools.push(
1942
469
  server.tool(
1943
470
  "list_requests",
1944
471
  "List recent requests captured by a sandbox endpoint, newest first.",
@@ -1950,7 +477,7 @@ function registerSandboxTools(server, options = {}) {
1950
477
  })
1951
478
  )
1952
479
  );
1953
- registered.push(
480
+ tools.push(
1954
481
  server.tool(
1955
482
  "get_request",
1956
483
  "Get full details for a request captured by a sandbox endpoint.",
@@ -1961,7 +488,7 @@ function registerSandboxTools(server, options = {}) {
1961
488
  })
1962
489
  )
1963
490
  );
1964
- registered.push(
491
+ tools.push(
1965
492
  server.tool(
1966
493
  "wait_for_request",
1967
494
  "Wait for a request to arrive at a sandbox endpoint.",
@@ -1973,7 +500,7 @@ function registerSandboxTools(server, options = {}) {
1973
500
  })
1974
501
  )
1975
502
  );
1976
- registered.push(
503
+ tools.push(
1977
504
  server.tool(
1978
505
  "generate_payload_schema",
1979
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.",
@@ -1985,91 +512,82 @@ function registerSandboxTools(server, options = {}) {
1985
512
  })
1986
513
  )
1987
514
  );
1988
- registered.push(
515
+ tools.push(
1989
516
  server.tool(
1990
517
  "connect_account",
1991
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.",
1992
519
  {
1993
- 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
+ )
1994
523
  },
1995
524
  withErrorHandling(async ({ email }) => {
1996
- if (session.connected) {
1997
- return jsonContent({ connected: true, message: "Already connected to an account." });
1998
- }
1999
525
  const normalized = email.trim().toLowerCase();
526
+ const previous = session.connect;
2000
527
  const sandbox = await session.existingSandbox();
2001
- let claim;
528
+ let attempt;
529
+ let movesEndpoints = false;
2002
530
  if (sandbox) {
2003
- claim = {
2004
- kind: "sandbox",
2005
- claimToken: sandbox.registration.claimToken,
2006
- email: normalized,
2007
- attempt: await sandbox.claim({ email: normalized }),
2008
- claimExpires: sandbox.expiresAt
2009
- };
2010
- } else if (session.claim?.kind === "service_auth" && session.claim.email === normalized && session.claim.claimExpires.getTime() > Date.now()) {
2011
- const attempt = await WebhooksCC2.agent.startClaim({
2012
- baseUrl,
2013
- claimToken: session.claim.claimToken
2014
- });
2015
- 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;
2016
535
  } else {
2017
- const registration = await WebhooksCC2.agent.registerServiceAuth({
2018
- baseUrl,
2019
- email: normalized,
2020
- clientName: session.clientName
2021
- });
2022
- claim = {
2023
- kind: "service_auth",
2024
- 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,
2025
546
  email: normalized,
2026
- attempt: registration.claim,
2027
- claimExpires: registration.claimTokenExpires
2028
- };
547
+ clientName: session.clientName,
548
+ credentialsFile: session.credentialsFile
549
+ }));
550
+ session.connect = { via: "connect", pending, email: normalized };
551
+ attempt = pending;
2029
552
  }
2030
- session.claim = claim;
2031
- const { verificationUri, userCode, expiresAt } = claim.attempt;
553
+ const { verificationUri, userCode, expiresAt } = attempt;
2032
554
  return jsonContent({
2033
555
  verificationUri,
2034
556
  userCode,
2035
557
  expiresAt: expiresAt.toISOString(),
2036
558
  tellTheHuman: `Open ${verificationUri}, sign in or sign up as ${normalized}, and enter the code ${userCode}.`,
2037
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.`,
2038
- ...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
+ } : {}
2039
563
  });
2040
564
  })
2041
565
  )
2042
566
  );
2043
- registered.push(
567
+ tools.push(
2044
568
  server.tool(
2045
569
  "wait_for_connection",
2046
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.`,
2047
571
  {
2048
- 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})`)
2049
573
  },
2050
574
  withErrorHandling(async ({ timeoutSeconds }) => {
2051
- if (session.connected) {
2052
- return jsonContent({ connected: true, message: "Already connected to an account." });
2053
- }
2054
- const claim = session.claim;
2055
- if (!claim) {
575
+ const pending = session.connect;
576
+ if (!pending) {
2056
577
  throw new Error("Nothing to wait for: call connect_account first.");
2057
578
  }
2058
- 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;
2059
582
  try {
2060
- claimed = await WebhooksCC2.agent.waitForClaim({
2061
- baseUrl,
2062
- claimToken: claim.claimToken,
2063
- interval: claim.attempt.interval,
2064
- timeout: (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3
2065
- });
583
+ client = pending.via === "sandbox" ? await pending.sandbox.waitForClaim({ timeout }) : await pending.pending.wait({ timeout });
2066
584
  } catch (error) {
2067
585
  if (!(error instanceof AgentAuthError)) throw error;
2068
586
  if (error.code === "timeout") {
2069
587
  return jsonContent({
2070
588
  connected: false,
2071
589
  status: "waiting",
2072
- 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.`
2073
591
  });
2074
592
  }
2075
593
  if (error.code === "expired_token") {
@@ -2088,34 +606,25 @@ function registerSandboxTools(server, options = {}) {
2088
606
  }
2089
607
  throw error;
2090
608
  }
2091
- const client = new WebhooksCC2({
2092
- baseUrl,
2093
- webhookUrl: session.webhookUrl,
2094
- getAccessToken: claimedTokens(baseUrl, claimed.identityAssertion, claimed.token)
2095
- });
2096
- session.connected = true;
2097
- session.claim = null;
2098
- session.forgetSandbox();
2099
- for (const tool of registered) tool.remove();
2100
- registerTools(server, client);
2101
- options.onConnected?.(client);
609
+ const movedEndpoints = pending.via === "sandbox" && pending.attempt.movesEndpoints;
610
+ session.reset();
611
+ showAccount(client);
2102
612
  return jsonContent({
2103
613
  connected: true,
2104
- email: claim.email,
2105
- scope: claimed.token.scope,
2106
- 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."}`
2107
616
  });
2108
617
  })
2109
618
  )
2110
619
  );
2111
- registered.push(
620
+ tools.push(
2112
621
  server.tool(
2113
622
  "about_sandbox",
2114
623
  "Explain what this agent can do without a webhooks.cc account, the sandbox limits, and how to connect an account. No network call.",
2115
624
  {},
2116
625
  withErrorHandling(async () => {
2117
626
  return jsonContent({
2118
- mode: "sandbox (no WHK_API_KEY)",
627
+ mode: "sandbox (no WHK_API_KEY, no saved connection)",
2119
628
  sandbox: {
2120
629
  maxEndpoints: SANDBOX.maxEndpoints,
2121
630
  requestsPerEndpoint: SANDBOX.requestsPerEndpoint,
@@ -2126,24 +635,26 @@ function registerSandboxTools(server, options = {}) {
2126
635
  notAvailable: "Mock responses, response rules, notifications, signature verification, email capture, forwarding, teams and sending webhooks."
2127
636
  },
2128
637
  connectAnAccount: [
2129
- "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.",
2130
639
  "Call wait_for_connection until it reports connected; the full tools for their account then replace the sandbox tools.",
2131
- "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)."
2132
641
  ],
642
+ savedIn: file ? `${file} (shared with the whk CLI and the SDKs): the sandbox and the connection survive restarts.` : "nowhere: this session only.",
2133
643
  withAnApiKey: "Set WHK_API_KEY to a whcc_ key from the dashboard and restart for the full tools from the start.",
2134
644
  docs: DOCS_URL,
2135
- protocol: `${baseUrl}/auth.md`
645
+ protocol: `${session.baseUrl}/auth.md`
2136
646
  });
2137
647
  })
2138
648
  )
2139
649
  );
650
+ return tools;
2140
651
  }
2141
652
  function registerAgentRegistrationTools(server) {
2142
653
  registerSandboxTools(server);
2143
654
  }
2144
655
 
2145
656
  // src/index.ts
2146
- var VERSION = true ? "1.13.0" : "0.0.0-dev";
657
+ var VERSION = true ? "1.15.0" : "0.0.0-dev";
2147
658
  function createServer(options = {}) {
2148
659
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2149
660
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -2154,17 +665,18 @@ function createServer(options = {}) {
2154
665
  { debouncedNotificationMethods: ["notifications/tools/list_changed"] }
2155
666
  );
2156
667
  if (!apiKey) {
2157
- registerSandboxTools(server, { baseUrl, webhookUrl });
668
+ registerSandboxTools(server, { baseUrl, webhookUrl, credentialsFile: options.credentialsFile });
2158
669
  registerPrompts(server);
2159
670
  return server;
2160
671
  }
2161
- const client = new WebhooksCC3({ apiKey, webhookUrl, baseUrl });
672
+ const client = new WebhooksCC2({ apiKey, webhookUrl, baseUrl });
2162
673
  registerTools(server, client);
2163
674
  registerPrompts(server);
2164
675
  registerResources(server, client);
2165
676
  return server;
2166
677
  }
2167
678
  export {
679
+ REMOTE_MAX_WAIT_MS,
2168
680
  createServer,
2169
681
  registerAgentRegistrationTools,
2170
682
  registerPrompts,