@webhooks-cc/mcp 1.14.0 → 1.15.1
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/README.md +43 -22
- package/dist/bin/mcp.js +304 -168
- package/dist/chunk-RHQD2GRD.mjs +1819 -0
- package/dist/index.d.mts +21 -9
- package/dist/index.d.ts +21 -9
- package/dist/index.js +219 -135
- package/dist/index.mjs +161 -1861
- package/dist/tools.d.mts +171 -0
- package/dist/tools.d.ts +171 -0
- package/dist/tools.js +1827 -0
- package/dist/tools.mjs +34 -0
- package/package.json +7 -2
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
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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.
|
|
2004
|
-
|
|
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
|
|
2008
|
-
*
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
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 =
|
|
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,
|
|
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 (
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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
|
-
|
|
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.
|
|
2047
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
2090
|
-
options.clientName ?? "webhooks-cc-mcp"
|
|
2091
|
-
|
|
2092
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
528
|
+
let attempt;
|
|
529
|
+
let movesEndpoints = false;
|
|
2214
530
|
if (sandbox) {
|
|
2215
|
-
claim = {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
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
|
-
|
|
2239
|
-
|
|
2240
|
-
};
|
|
547
|
+
clientName: session.clientName,
|
|
548
|
+
credentialsFile: session.credentialsFile
|
|
549
|
+
}));
|
|
550
|
+
session.connect = { via: "connect", pending, email: normalized };
|
|
551
|
+
attempt = pending;
|
|
2241
552
|
}
|
|
2242
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
2304
|
-
|
|
2305
|
-
|
|
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:
|
|
2317
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
657
|
+
var VERSION = true ? "1.15.1" : "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
|
|
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,
|