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