@webhooks-cc/mcp 1.23.0 → 2.0.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 +43 -16
- package/dist/bin/mcp.js +1355 -638
- package/dist/{chunk-TMX4P6ZZ.mjs → chunk-VANGXSK7.mjs} +522 -33
- package/dist/index.d.mts +84 -10
- package/dist/index.d.ts +84 -10
- package/dist/index.js +1322 -581
- package/dist/index.mjs +290 -34
- package/dist/tools-Bg9zP80n.d.mts +276 -0
- package/dist/tools-Bg9zP80n.d.ts +276 -0
- package/dist/tools.d.mts +3 -200
- package/dist/tools.d.ts +3 -200
- package/dist/tools.js +526 -33
- package/dist/tools.mjs +15 -1
- package/package.json +5 -4
|
@@ -241,6 +241,209 @@ function compactRequest(request) {
|
|
|
241
241
|
return compact;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
// src/catalog.ts
|
|
245
|
+
var TOOL_GROUPS = {
|
|
246
|
+
core: "Endpoints, captured requests and emails, sending test webhooks, signatures, forwarding destinations, deliveries, previews of your local app and their logs, usage and approvals",
|
|
247
|
+
forwarding: "The rest of forwarding: preview what a destination would send, the endpoint's forwarding settings and signing secret, test deliveries",
|
|
248
|
+
layouts: "Email layouts and named fields: try layouts and fields on a captured email, add ready-made layouts, and AI setup",
|
|
249
|
+
extras: "Bulk endpoint changes, favourites, request counts and payload schemas",
|
|
250
|
+
teams: "Teams: members, invites, sharing endpoints with a team, and pricing team changes",
|
|
251
|
+
account: "The account: billing, API keys, connected agents, public share links to requests, and the display name",
|
|
252
|
+
domains: "Custom inbound email domains: add, verify, route and remove them",
|
|
253
|
+
sheets: "Google Sheets destinations: Google accounts, spreadsheets and their tabs",
|
|
254
|
+
names: "Preview names: reserve, release and password-protect them"
|
|
255
|
+
};
|
|
256
|
+
var TOOL_GROUP_NAMES = Object.keys(TOOL_GROUPS);
|
|
257
|
+
var read = { readOnly: true };
|
|
258
|
+
var write = {};
|
|
259
|
+
var update = { idempotent: true };
|
|
260
|
+
var remove = { destructive: true, idempotent: true };
|
|
261
|
+
var replace = { destructive: true, idempotent: true };
|
|
262
|
+
var outward = { openWorld: true };
|
|
263
|
+
function group(name, tools) {
|
|
264
|
+
return Object.fromEntries(
|
|
265
|
+
Object.entries(tools).map(([tool, behaviour]) => [tool, { group: name, ...behaviour }])
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
var TOOL_CATALOG = {
|
|
269
|
+
...group("core", {
|
|
270
|
+
create_endpoint: write,
|
|
271
|
+
list_endpoints: read,
|
|
272
|
+
get_endpoint: read,
|
|
273
|
+
// Replaces what it is given (a mock response, the signing secret).
|
|
274
|
+
update_endpoint: replace,
|
|
275
|
+
delete_endpoint: remove,
|
|
276
|
+
list_requests: read,
|
|
277
|
+
search_requests: read,
|
|
278
|
+
get_request: read,
|
|
279
|
+
wait_for_request: read,
|
|
280
|
+
wait_for_requests: read,
|
|
281
|
+
replay_request: outward,
|
|
282
|
+
compare_requests: read,
|
|
283
|
+
extract_from_request: read,
|
|
284
|
+
clear_requests: remove,
|
|
285
|
+
send_webhook: write,
|
|
286
|
+
send_to: outward,
|
|
287
|
+
test_webhook_flow: outward,
|
|
288
|
+
preview_webhook: read,
|
|
289
|
+
preview_response: read,
|
|
290
|
+
list_provider_templates: read,
|
|
291
|
+
verify_signature: read,
|
|
292
|
+
list_emails: read,
|
|
293
|
+
get_email: read,
|
|
294
|
+
wait_for_email: read,
|
|
295
|
+
send_test_email: write,
|
|
296
|
+
render_email: outward,
|
|
297
|
+
list_destinations: read,
|
|
298
|
+
add_destination: write,
|
|
299
|
+
update_destination: replace,
|
|
300
|
+
remove_destination: remove,
|
|
301
|
+
list_deliveries: read,
|
|
302
|
+
redeliver: outward,
|
|
303
|
+
share_port: outward,
|
|
304
|
+
list_shares: read,
|
|
305
|
+
share_requests: read,
|
|
306
|
+
stop_share: remove,
|
|
307
|
+
get_usage: read,
|
|
308
|
+
// Collecting an approved key hands it over once: not a pure read.
|
|
309
|
+
wait_for_approval: write,
|
|
310
|
+
describe: read
|
|
311
|
+
}),
|
|
312
|
+
...group("forwarding", {
|
|
313
|
+
preview_destination: read,
|
|
314
|
+
configure_forwarding: replace,
|
|
315
|
+
// rotate: true replaces the secret, and receivers holding the old one fail.
|
|
316
|
+
get_forwarding_secret: { destructive: true },
|
|
317
|
+
test_forwarding: outward
|
|
318
|
+
}),
|
|
319
|
+
...group("layouts", {
|
|
320
|
+
preview_layouts: read,
|
|
321
|
+
list_layout_templates: read,
|
|
322
|
+
add_layout_template: write,
|
|
323
|
+
preview_email_fields: read,
|
|
324
|
+
ai_setup_status: read,
|
|
325
|
+
// Sends the chosen emails to Anthropic once the owner approves.
|
|
326
|
+
ai_setup: outward
|
|
327
|
+
}),
|
|
328
|
+
...group("extras", {
|
|
329
|
+
create_endpoints: write,
|
|
330
|
+
delete_endpoints: remove,
|
|
331
|
+
favorite_endpoint: update,
|
|
332
|
+
count_requests: read,
|
|
333
|
+
generate_payload_schema: read
|
|
334
|
+
}),
|
|
335
|
+
...group("teams", {
|
|
336
|
+
list_teams: read,
|
|
337
|
+
list_team_members: read,
|
|
338
|
+
share_endpoint: update,
|
|
339
|
+
unshare_endpoint: remove,
|
|
340
|
+
create_team: write,
|
|
341
|
+
rename_team: update,
|
|
342
|
+
delete_team: remove,
|
|
343
|
+
remove_team_member: remove,
|
|
344
|
+
leave_team: remove,
|
|
345
|
+
invite_to_team: outward,
|
|
346
|
+
list_invites: read,
|
|
347
|
+
answer_invite: write,
|
|
348
|
+
withdraw_invite: remove,
|
|
349
|
+
quote_team_change: read
|
|
350
|
+
}),
|
|
351
|
+
...group("account", {
|
|
352
|
+
get_billing: read,
|
|
353
|
+
list_api_keys: read,
|
|
354
|
+
create_api_key: write,
|
|
355
|
+
revoke_api_key: remove,
|
|
356
|
+
rename_account: update,
|
|
357
|
+
list_connected_agents: read,
|
|
358
|
+
disconnect_agent: remove,
|
|
359
|
+
list_share_links: read,
|
|
360
|
+
create_share_link: write,
|
|
361
|
+
revoke_share_link: remove
|
|
362
|
+
}),
|
|
363
|
+
...group("domains", {
|
|
364
|
+
list_domains: read,
|
|
365
|
+
add_domain: write,
|
|
366
|
+
// A failed check counts toward lapsing a verified domain: not safe to repeat.
|
|
367
|
+
verify_domain: { destructive: true },
|
|
368
|
+
// Replaces the routing; a missing catch-all refuses mail.
|
|
369
|
+
route_domain: replace,
|
|
370
|
+
remove_domain: remove
|
|
371
|
+
}),
|
|
372
|
+
...group("sheets", {
|
|
373
|
+
list_google_connections: read,
|
|
374
|
+
connect_google: outward,
|
|
375
|
+
pick_spreadsheet: outward,
|
|
376
|
+
create_spreadsheet: outward,
|
|
377
|
+
list_sheet_tabs: { readOnly: true, openWorld: true },
|
|
378
|
+
disconnect_google: remove
|
|
379
|
+
}),
|
|
380
|
+
...group("names", {
|
|
381
|
+
// The log of what visitors asked a running preview for.
|
|
382
|
+
list_names: read,
|
|
383
|
+
reserve_name: write,
|
|
384
|
+
release_name: remove,
|
|
385
|
+
set_name_password: update,
|
|
386
|
+
// Opens the name to anyone again.
|
|
387
|
+
clear_name_password: { destructive: true, idempotent: true }
|
|
388
|
+
})
|
|
389
|
+
};
|
|
390
|
+
var ENABLE_TOOLS = "enable_tools";
|
|
391
|
+
function toolAnnotations(name) {
|
|
392
|
+
const entry = TOOL_CATALOG[name];
|
|
393
|
+
const readOnly = entry?.readOnly === true;
|
|
394
|
+
return {
|
|
395
|
+
title: toolTitle(name),
|
|
396
|
+
readOnlyHint: readOnly,
|
|
397
|
+
// MCP reads a missing destructiveHint as true on any tool that writes.
|
|
398
|
+
destructiveHint: !readOnly && entry?.destructive === true,
|
|
399
|
+
idempotentHint: readOnly || entry?.idempotent === true,
|
|
400
|
+
openWorldHint: entry?.openWorld === true
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function toolTitle(name) {
|
|
404
|
+
const words = name.split("_").map((word) => TITLE_WORDS[word] ?? word);
|
|
405
|
+
const first = words[0] ?? "";
|
|
406
|
+
return [first.charAt(0).toUpperCase() + first.slice(1), ...words.slice(1)].join(" ");
|
|
407
|
+
}
|
|
408
|
+
var TITLE_WORDS = {
|
|
409
|
+
api: "API",
|
|
410
|
+
ai: "AI",
|
|
411
|
+
mcp: "MCP"
|
|
412
|
+
};
|
|
413
|
+
function parseToolGroups(setting) {
|
|
414
|
+
if (setting === null || setting === void 0 || setting.trim() === "") return null;
|
|
415
|
+
const groups = /* @__PURE__ */ new Set(["core"]);
|
|
416
|
+
const unknown = [];
|
|
417
|
+
for (const raw of setting.split(/[\s,+]+/)) {
|
|
418
|
+
const name = raw.trim().toLowerCase();
|
|
419
|
+
if (!name) continue;
|
|
420
|
+
if (name === "all") {
|
|
421
|
+
for (const g of TOOL_GROUP_NAMES) groups.add(g);
|
|
422
|
+
} else if (TOOL_GROUP_NAMES.includes(name)) {
|
|
423
|
+
groups.add(name);
|
|
424
|
+
} else {
|
|
425
|
+
unknown.push(raw.trim());
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { groups, unknown };
|
|
429
|
+
}
|
|
430
|
+
var LOADS_EVERYTHING = [
|
|
431
|
+
/^claude-code$/i,
|
|
432
|
+
/^claude-ai$/i,
|
|
433
|
+
/^claude$/i,
|
|
434
|
+
/codex/i,
|
|
435
|
+
/^openai-mcp$/i,
|
|
436
|
+
/chatgpt/i
|
|
437
|
+
];
|
|
438
|
+
function clientLoadsEverything(clientName) {
|
|
439
|
+
if (!clientName) return false;
|
|
440
|
+
const name = clientName.trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
441
|
+
return LOADS_EVERYTHING.some((pattern) => pattern.test(name));
|
|
442
|
+
}
|
|
443
|
+
function defaultToolGroups(clientName) {
|
|
444
|
+
return clientLoadsEverything(clientName) ? new Set(TOOL_GROUP_NAMES) : /* @__PURE__ */ new Set(["core"]);
|
|
445
|
+
}
|
|
446
|
+
|
|
244
447
|
// src/tools.ts
|
|
245
448
|
var MAX_BODY_SIZE = MAX_OUTPUT;
|
|
246
449
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
@@ -501,6 +704,14 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
501
704
|
if (typeof result.url === "string") {
|
|
502
705
|
return `Approved. The share link is ${result.url} (anyone with it can read the copy until ${String(result.expiresAt)}).`;
|
|
503
706
|
}
|
|
707
|
+
if (typeof result.invitedEmail === "string") {
|
|
708
|
+
const team = typeof result.teamName === "string" ? ` to "${result.teamName}"` : "";
|
|
709
|
+
const warning = typeof result.warning === "string" ? ` ${result.warning} Tell them to sign up with that address and accept from their Teams page.` : " They get an email; accepting claims one of the team's seats.";
|
|
710
|
+
return `Approved. Invited ${result.invitedEmail}${team} (invite ${String(result.id)}).${warning}`;
|
|
711
|
+
}
|
|
712
|
+
if (typeof result.deleted === "string" && typeof result.teamId === "string") {
|
|
713
|
+
return `Approved. The team ${result.deleted ? `"${result.deleted}" ` : ""}was deleted; its subscription has ended.`;
|
|
714
|
+
}
|
|
504
715
|
if (typeof result.removed === "string") {
|
|
505
716
|
return `Approved. The domain ${result.removed} was removed; mail to it is refused now.`;
|
|
506
717
|
}
|
|
@@ -575,6 +786,75 @@ function fitArrayField(value, key) {
|
|
|
575
786
|
}
|
|
576
787
|
return cut(low);
|
|
577
788
|
}
|
|
789
|
+
function isoTime(ms) {
|
|
790
|
+
if (ms === null || !Number.isFinite(ms)) return null;
|
|
791
|
+
const date = new Date(ms);
|
|
792
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
793
|
+
}
|
|
794
|
+
function searchStoppedNote(searchedUntil, order, hasCursor) {
|
|
795
|
+
const [direction, rest] = order === "asc" ? ["forward", "newer"] : ["back", "older"];
|
|
796
|
+
const reached = searchedUntil ? `Searched ${direction} to ${searchedUntil}; ${rest} requests were not searched yet.` : `The search stopped before it read everything; ${rest} requests were not searched yet.`;
|
|
797
|
+
return hasCursor ? `${reached} Call search_requests again with cursor to continue.` : reached;
|
|
798
|
+
}
|
|
799
|
+
var FULL_REQUEST_HINT = "The full request is available from the API (GET /api/requests/{id}), the SDKs (requests.get(id)) or the CLI (whk requests get <id>).";
|
|
800
|
+
var SEARCH_SHORTENED_NOTE = `Some requests were shortened to fit the output (bodyTruncated with bodySize, or shortened: true for a summary); get_request with the id returns as much of one request as fits. ${FULL_REQUEST_HINT}`;
|
|
801
|
+
var REQUEST_SHORTENED_NOTE = `This request was cut to fit the output: its body (bodyTruncated, with bodySize), or everything but a summary (shortened: true). ${FULL_REQUEST_HINT}`;
|
|
802
|
+
function clip(value, max) {
|
|
803
|
+
return typeof value === "string" ? sliceText(value, max) : void 0;
|
|
804
|
+
}
|
|
805
|
+
function listedSize(request) {
|
|
806
|
+
return jsonSize([[request]]) - 8;
|
|
807
|
+
}
|
|
808
|
+
function fitSearchRequests(requests, room) {
|
|
809
|
+
const share = Math.floor(room / Math.max(1, requests.length));
|
|
810
|
+
const fitted = requests.map(
|
|
811
|
+
(request) => shortenRequest(request, share, listedSize)
|
|
812
|
+
);
|
|
813
|
+
if (fitted.every((request) => listedSize(request) <= share)) return fitted;
|
|
814
|
+
return requests.map((request) => {
|
|
815
|
+
const { id, receivedAt } = request;
|
|
816
|
+
return { id, receivedAt, shortened: true };
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function shortenRequest(request, share, sizeOf) {
|
|
820
|
+
if (sizeOf(request) <= share) return request;
|
|
821
|
+
if (typeof request.body === "string") {
|
|
822
|
+
const body = request.body;
|
|
823
|
+
let keep = body.length - (sizeOf(request) - share) - 96;
|
|
824
|
+
while (keep > 0) {
|
|
825
|
+
const cut = {
|
|
826
|
+
...request,
|
|
827
|
+
body: sliceText(body, keep),
|
|
828
|
+
bodyTruncated: true,
|
|
829
|
+
bodySize: body.length
|
|
830
|
+
};
|
|
831
|
+
if (sizeOf(cut) <= share) return cut;
|
|
832
|
+
keep = Math.floor(keep / 2);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const email = request.email;
|
|
836
|
+
const summary = {
|
|
837
|
+
id: request.id,
|
|
838
|
+
slug: clip(request.slug, 64),
|
|
839
|
+
method: clip(request.method, 16),
|
|
840
|
+
path: clip(request.path, 200),
|
|
841
|
+
contentType: clip(request.contentType, 100),
|
|
842
|
+
size: request.size,
|
|
843
|
+
receivedAt: request.receivedAt,
|
|
844
|
+
kind: request.kind,
|
|
845
|
+
...email ? { subject: clip(email.subject, 200) } : {},
|
|
846
|
+
shortened: true,
|
|
847
|
+
...typeof request.note === "string" ? { note: request.note } : {}
|
|
848
|
+
};
|
|
849
|
+
return summary;
|
|
850
|
+
}
|
|
851
|
+
function requestContent(request) {
|
|
852
|
+
const compact = compactRequest(request);
|
|
853
|
+
if (jsonSize(compact) <= MAX_BODY_SIZE) return jsonContent(compact);
|
|
854
|
+
return jsonContent(
|
|
855
|
+
shortenRequest({ ...compact, note: REQUEST_SHORTENED_NOTE }, MAX_BODY_SIZE, jsonSize)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
578
858
|
function jsonContent(value) {
|
|
579
859
|
return textContent(serializeJson(value));
|
|
580
860
|
}
|
|
@@ -619,6 +899,13 @@ function serializeError(error) {
|
|
|
619
899
|
} else if (error instanceof WebhooksCCError) {
|
|
620
900
|
payload.code = error.statusCode >= 500 ? "server_error" : "validation_error";
|
|
621
901
|
}
|
|
902
|
+
if (error instanceof WebhooksCCError && payload.hint === null) {
|
|
903
|
+
if (error.code === "invalid_cursor") {
|
|
904
|
+
payload.hint = "The cursor is not one this search returned, or the order changed since. Pass a cursor exactly as a result gave it, or search again without one.";
|
|
905
|
+
} else if (error.code === "search_busy") {
|
|
906
|
+
payload.hint = "The account's other searches are still running. Wait retryAfter seconds, then call again with the same inputs.";
|
|
907
|
+
}
|
|
908
|
+
}
|
|
622
909
|
return JSON.stringify(payload, null, 2);
|
|
623
910
|
}
|
|
624
911
|
function withErrorHandling(handler) {
|
|
@@ -843,11 +1130,18 @@ function durationMs(value) {
|
|
|
843
1130
|
return null;
|
|
844
1131
|
}
|
|
845
1132
|
}
|
|
846
|
-
function toolTarget(target,
|
|
1133
|
+
function toolTarget(target, options, handles) {
|
|
1134
|
+
const remote = options.remote === true;
|
|
847
1135
|
return {
|
|
848
1136
|
tool(name, description, shape, handler) {
|
|
849
1137
|
if (remote && LOCAL_ONLY_TOOLS.has(name)) return { remove() {
|
|
850
1138
|
} };
|
|
1139
|
+
const entry = TOOL_CATALOG[name];
|
|
1140
|
+
if (!entry) throw new Error(`${name} has no entry in catalog.ts`);
|
|
1141
|
+
if (options.groups && !options.groups.has(entry.group)) return { remove() {
|
|
1142
|
+
} };
|
|
1143
|
+
if (options.readOnly && entry.readOnly !== true) return { remove() {
|
|
1144
|
+
} };
|
|
851
1145
|
const wrapped = remote ? async (args) => {
|
|
852
1146
|
const input = args;
|
|
853
1147
|
if (TARGET_URL_TOOLS.has(name) && input.targetUrl !== void 0) {
|
|
@@ -881,7 +1175,7 @@ function toolTarget(target, remote, handles) {
|
|
|
881
1175
|
}
|
|
882
1176
|
return handler(bounded);
|
|
883
1177
|
} : handler;
|
|
884
|
-
const handle = target.tool(name, description, shape, wrapped);
|
|
1178
|
+
const handle = target.tool(name, description, shape, wrapped, toolAnnotations(name));
|
|
885
1179
|
handles.push(handle);
|
|
886
1180
|
return handle;
|
|
887
1181
|
}
|
|
@@ -957,7 +1251,7 @@ function withShareHints(list, remote = false) {
|
|
|
957
1251
|
function registerTools(target, client, options = {}) {
|
|
958
1252
|
const handles = [];
|
|
959
1253
|
const remote = options.remote === true;
|
|
960
|
-
const server = toolTarget(target,
|
|
1254
|
+
const server = toolTarget(target, options, handles);
|
|
961
1255
|
const shareWith = remote ? REMOTE_SHARE : "share_port";
|
|
962
1256
|
server.tool(
|
|
963
1257
|
"create_endpoint",
|
|
@@ -1070,6 +1364,124 @@ function registerTools(target, client, options = {}) {
|
|
|
1070
1364
|
return jsonContent({ shared: false, slug, teamId });
|
|
1071
1365
|
})
|
|
1072
1366
|
);
|
|
1367
|
+
server.tool(
|
|
1368
|
+
"create_team",
|
|
1369
|
+
"Create a team you own, to share endpoints with its members under a pooled quota. It does nothing until it has a subscription, which the person starts in the browser (Account > Teams); until then it is suspended.",
|
|
1370
|
+
{ name: z.string().trim().min(1).max(100).describe("The team's name") },
|
|
1371
|
+
withErrorHandling(async ({ name }) => jsonContent(await client.teams.create(name)))
|
|
1372
|
+
);
|
|
1373
|
+
server.tool(
|
|
1374
|
+
"rename_team",
|
|
1375
|
+
"Rename a team you own.",
|
|
1376
|
+
{
|
|
1377
|
+
teamId: z.string().min(1).describe("Team id (from list_teams)"),
|
|
1378
|
+
name: z.string().trim().min(1).max(100).describe("The new name")
|
|
1379
|
+
},
|
|
1380
|
+
withErrorHandling(async ({ teamId, name }) => {
|
|
1381
|
+
await client.teams.rename(teamId, name);
|
|
1382
|
+
return jsonContent({ renamed: true, teamId, name });
|
|
1383
|
+
})
|
|
1384
|
+
);
|
|
1385
|
+
server.tool(
|
|
1386
|
+
"delete_team",
|
|
1387
|
+
"Delete a team you own: its subscription ends at once, every member loses the endpoints shared with it, and it cannot be undone. Only when the person asks for it. The team's owner must approve it: this returns a link and a code to show them, then call wait_for_approval.",
|
|
1388
|
+
{ teamId: z.string().min(1).describe("Team id (from list_teams)") },
|
|
1389
|
+
withErrorHandling(async ({ teamId }) => {
|
|
1390
|
+
const pending = await client.teams.delete(teamId);
|
|
1391
|
+
if (!pending.id) return textContent(`Deleted the team ${teamId}.`);
|
|
1392
|
+
return textContent(approvalAskText(pending));
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
server.tool(
|
|
1396
|
+
"remove_team_member",
|
|
1397
|
+
"Remove a member from a team you own: the endpoints they shared with the team stop being shared, and their seat is free for someone else. Only when the person asks for it.",
|
|
1398
|
+
{
|
|
1399
|
+
teamId: z.string().min(1).describe("Team id (from list_teams)"),
|
|
1400
|
+
userId: z.string().min(1).describe("The member's userId (from list_team_members)")
|
|
1401
|
+
},
|
|
1402
|
+
withErrorHandling(async ({ teamId, userId }) => {
|
|
1403
|
+
await client.teams.removeMember(teamId, userId);
|
|
1404
|
+
return jsonContent({ removed: true, teamId, userId });
|
|
1405
|
+
})
|
|
1406
|
+
);
|
|
1407
|
+
server.tool(
|
|
1408
|
+
"leave_team",
|
|
1409
|
+
"Leave a team you belong to: the endpoints you shared with it stop being shared, and you lose access to the ones shared with you through it. An owner cannot leave; they delete the team instead (delete_team). Only when the person asks for it.",
|
|
1410
|
+
{ teamId: z.string().min(1).describe("Team id (from list_teams)") },
|
|
1411
|
+
withErrorHandling(async ({ teamId }) => {
|
|
1412
|
+
await client.teams.leave(teamId);
|
|
1413
|
+
return jsonContent({ left: true, teamId });
|
|
1414
|
+
})
|
|
1415
|
+
);
|
|
1416
|
+
server.tool(
|
|
1417
|
+
"invite_to_team",
|
|
1418
|
+
"Invite an email address to a team you own. It emails that address, so the team's owner must approve it: this returns a link and a code to show them, then call wait_for_approval, which returns the invite. Accepting the invite claims one of the team's paid seats. Only when the person asks for it.",
|
|
1419
|
+
{
|
|
1420
|
+
teamId: z.string().min(1).describe("Team id (from list_teams)"),
|
|
1421
|
+
email: z.string().trim().email().describe("The address to invite")
|
|
1422
|
+
},
|
|
1423
|
+
withErrorHandling(async ({ teamId, email }) => {
|
|
1424
|
+
const pending = await client.teams.invite(teamId, email);
|
|
1425
|
+
if (!pending.id) return textContent(approvalResultText({ ...await pending.wait() }));
|
|
1426
|
+
return textContent(approvalAskText(pending));
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1429
|
+
server.tool(
|
|
1430
|
+
"list_invites",
|
|
1431
|
+
"List the team invites waiting for you to answer: team, who invited you, when, and the invite id for answer_invite.",
|
|
1432
|
+
{},
|
|
1433
|
+
withErrorHandling(async () => jsonContent(await client.teams.invites.list()))
|
|
1434
|
+
);
|
|
1435
|
+
server.tool(
|
|
1436
|
+
"answer_invite",
|
|
1437
|
+
"Accept or decline a team invite waiting for you (from list_invites). Accepting claims one of the team's paid seats and you join the team, seeing the endpoints shared with it; it fails when the team is full or suspended. Only when the person says which.",
|
|
1438
|
+
{
|
|
1439
|
+
inviteId: z.string().min(1).describe("The invite's id, from list_invites"),
|
|
1440
|
+
answer: z.enum(["accept", "decline"]).describe("accept joins the team; decline turns it down")
|
|
1441
|
+
},
|
|
1442
|
+
withErrorHandling(async ({ inviteId, answer }) => {
|
|
1443
|
+
if (answer === "accept") {
|
|
1444
|
+
await client.teams.invites.accept(inviteId);
|
|
1445
|
+
return jsonContent({ accepted: true, inviteId });
|
|
1446
|
+
}
|
|
1447
|
+
await client.teams.invites.decline(inviteId);
|
|
1448
|
+
return jsonContent({ declined: true, inviteId });
|
|
1449
|
+
})
|
|
1450
|
+
);
|
|
1451
|
+
server.tool(
|
|
1452
|
+
"withdraw_invite",
|
|
1453
|
+
"Withdraw an invite to a team you own before it is answered (pending invites and their ids are in list_team_members). The invite link stops working.",
|
|
1454
|
+
{
|
|
1455
|
+
teamId: z.string().min(1).describe("Team id (from list_teams)"),
|
|
1456
|
+
inviteId: z.string().min(1).describe("The invite's id (from list_team_members' pendingInvites)")
|
|
1457
|
+
},
|
|
1458
|
+
withErrorHandling(async ({ teamId, inviteId }) => {
|
|
1459
|
+
await client.teams.withdrawInvite(teamId, inviteId);
|
|
1460
|
+
return jsonContent({ withdrawn: true, teamId, inviteId });
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
server.tool(
|
|
1464
|
+
"quote_team_change",
|
|
1465
|
+
"What changing a team's seat count or plan would cost, read only (owner only): what is due now, any credit, and the price from the renewal. Give seats, or plan (with interval). The change itself is made by the person in the browser.",
|
|
1466
|
+
{
|
|
1467
|
+
teamId: z.string().min(1).describe("Team id (from list_teams)"),
|
|
1468
|
+
seats: z.number().int().min(1).optional().describe("Price this seat count"),
|
|
1469
|
+
plan: z.enum(["team", "team_business"]).optional().describe("Price a move to this plan: team (Teams) or team_business (Team Business)"),
|
|
1470
|
+
interval: z.enum(["month", "year"]).optional().describe("Billing interval for plan (default month; Teams is monthly only)")
|
|
1471
|
+
},
|
|
1472
|
+
withErrorHandling(async ({ teamId, seats, plan, interval }) => {
|
|
1473
|
+
if (seats === void 0 === (plan === void 0)) {
|
|
1474
|
+
throw new Error("Give seats or plan, not both.");
|
|
1475
|
+
}
|
|
1476
|
+
if (seats !== void 0) {
|
|
1477
|
+
if (interval !== void 0) throw new Error("interval goes with plan, not seats.");
|
|
1478
|
+
return jsonContent(await client.billing.quoteTeamSeats(teamId, seats));
|
|
1479
|
+
}
|
|
1480
|
+
return jsonContent(
|
|
1481
|
+
await client.billing.quoteTeamPlan(teamId, plan, interval)
|
|
1482
|
+
);
|
|
1483
|
+
})
|
|
1484
|
+
);
|
|
1073
1485
|
server.tool(
|
|
1074
1486
|
"get_endpoint",
|
|
1075
1487
|
"Get details for a specific webhook endpoint by slug.",
|
|
@@ -1163,6 +1575,35 @@ function registerTools(target, client, options = {}) {
|
|
|
1163
1575
|
return jsonContent(holder);
|
|
1164
1576
|
})
|
|
1165
1577
|
);
|
|
1578
|
+
server.tool(
|
|
1579
|
+
"list_layout_templates",
|
|
1580
|
+
"List the ready-made email layouts (order emails from Shopify, WooCommerce, Etsy and more, payments, bookings, attachments): each with its id, sender, kind, description, the fields it reads and whether it reads line items. Add one with add_layout_template.",
|
|
1581
|
+
{ slug: z.string().describe("The endpoint slug") },
|
|
1582
|
+
withErrorHandling(async ({ slug }) => jsonContent(await client.endpoints.layoutTemplates(slug)))
|
|
1583
|
+
);
|
|
1584
|
+
server.tool(
|
|
1585
|
+
"add_layout_template",
|
|
1586
|
+
"Add a ready-made layout (an id from list_layout_templates) to the end of an endpoint's layouts. Emails it matches are parsed from the next one on; check it on a captured email with preview_layouts. Owner only.",
|
|
1587
|
+
{
|
|
1588
|
+
slug: z.string().describe("The endpoint slug"),
|
|
1589
|
+
template: z.string().min(1).describe("The template's id, from list_layout_templates")
|
|
1590
|
+
},
|
|
1591
|
+
withErrorHandling(
|
|
1592
|
+
async ({ slug, template }) => jsonContent(await client.endpoints.addLayoutTemplate(slug, template))
|
|
1593
|
+
)
|
|
1594
|
+
);
|
|
1595
|
+
server.tool(
|
|
1596
|
+
"preview_email_fields",
|
|
1597
|
+
"Dry run of an endpoint's named email fields on a captured email: each field's value (null where it finds nothing), and problems with fields you pass. Nothing is saved. Pass emailFields to try fields before saving them with update_endpoint. Owner only.",
|
|
1598
|
+
{
|
|
1599
|
+
slug: z.string().describe("The endpoint slug"),
|
|
1600
|
+
requestId: z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
|
|
1601
|
+
emailFields: z.array(emailFieldSchema).max(20).optional().describe("Unsaved fields to try instead of the saved ones")
|
|
1602
|
+
},
|
|
1603
|
+
withErrorHandling(
|
|
1604
|
+
async ({ slug, requestId, emailFields }) => jsonContent(await client.endpoints.previewEmailFields(slug, { requestId, emailFields }))
|
|
1605
|
+
)
|
|
1606
|
+
);
|
|
1166
1607
|
server.tool(
|
|
1167
1608
|
"preview_response",
|
|
1168
1609
|
"Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
|
|
@@ -1298,56 +1739,95 @@ function registerTools(target, client, options = {}) {
|
|
|
1298
1739
|
);
|
|
1299
1740
|
server.tool(
|
|
1300
1741
|
"search_requests",
|
|
1301
|
-
"Search captured
|
|
1742
|
+
"Search captured requests and emails across endpoints: a substring match on path, headers, body and decoded email fields. One call reads back through history for about 2 seconds and returns { requests, complete, searchedUntil, cursor }. When complete is false, older requests were not searched yet: call again with the cursor and the same filters to continue.",
|
|
1302
1743
|
{
|
|
1303
1744
|
slug: z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
1304
1745
|
method: z.string().optional().describe("Filter by HTTP method"),
|
|
1305
1746
|
kind: kindSchema,
|
|
1306
|
-
q: z.string().optional().describe(
|
|
1307
|
-
|
|
1308
|
-
|
|
1747
|
+
q: z.string().optional().describe(
|
|
1748
|
+
"Substring to find in the path, headers, body and decoded email fields (up to 1,000 characters)"
|
|
1749
|
+
),
|
|
1750
|
+
from: durationOrTimestampSchema.optional().describe(
|
|
1751
|
+
'Start time as a timestamp (Unix ms) or a duration back from now like "1h" or "7d". A duration is counted from each call, so when you continue with a cursor pass a timestamp to keep the same range.'
|
|
1752
|
+
),
|
|
1753
|
+
to: durationOrTimestampSchema.optional().describe(
|
|
1754
|
+
'End time as a timestamp (Unix ms) or a duration back from now like "1h" or "7d". A duration is counted from each call, so when you continue with a cursor pass a timestamp to keep the same range.'
|
|
1755
|
+
),
|
|
1309
1756
|
limit: z.number().int().min(1).max(200).default(50).describe("Max results to return"),
|
|
1310
|
-
offset: z.number().int().min(0).max(1e4).default(0).describe("
|
|
1311
|
-
order: z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
|
|
1757
|
+
offset: z.number().int().min(0).max(1e4).default(0).describe("Matches to skip (first call only; a cursor carries what is left of it)"),
|
|
1758
|
+
order: z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time"),
|
|
1759
|
+
cursor: z.string().optional().describe(
|
|
1760
|
+
"The cursor from a previous search_requests result, to continue that search. Keep the other inputs the same."
|
|
1761
|
+
)
|
|
1312
1762
|
},
|
|
1313
|
-
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
|
|
1314
|
-
const
|
|
1315
|
-
slug,
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1763
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order, cursor }) => {
|
|
1764
|
+
const page = await client.requests.searchPage(
|
|
1765
|
+
{ slug, method, kind, q, from, to, limit, offset, order },
|
|
1766
|
+
{ cursor }
|
|
1767
|
+
);
|
|
1768
|
+
const searchedUntil = isoTime(page.searchedUntil);
|
|
1769
|
+
const envelope = (requests2, shortened) => {
|
|
1770
|
+
const notes = [
|
|
1771
|
+
...page.complete ? [] : [searchStoppedNote(searchedUntil, order, Boolean(page.cursor))],
|
|
1772
|
+
...shortened ? [SEARCH_SHORTENED_NOTE] : []
|
|
1773
|
+
];
|
|
1774
|
+
return {
|
|
1775
|
+
requests: requests2,
|
|
1776
|
+
complete: page.complete,
|
|
1777
|
+
searchedUntil,
|
|
1778
|
+
cursor: page.cursor,
|
|
1779
|
+
...notes.length > 0 ? { note: notes.join(" ") } : {}
|
|
1780
|
+
};
|
|
1781
|
+
};
|
|
1782
|
+
const requests = page.requests.map(compactRequest);
|
|
1783
|
+
if (jsonSize(envelope(requests, false)) <= MAX_BODY_SIZE) {
|
|
1784
|
+
return jsonContent(envelope(requests, false));
|
|
1785
|
+
}
|
|
1786
|
+
const room = MAX_BODY_SIZE - jsonSize(envelope([], true)) - 8;
|
|
1787
|
+
return jsonContent(envelope(fitSearchRequests(requests, room), true));
|
|
1326
1788
|
})
|
|
1327
1789
|
);
|
|
1328
1790
|
server.tool(
|
|
1329
1791
|
"count_requests",
|
|
1330
|
-
"Count captured
|
|
1792
|
+
"Count captured requests and emails that match the given filters (q is a substring match on path, headers, body and decoded email fields). Returns { count, exact, searchedUntil, cursor }. With q or method a call counts back through history for about 2 seconds: when exact is false, count covers history back to searchedUntil only, and calling again with the cursor counts the next stretch (add the counts up).",
|
|
1331
1793
|
{
|
|
1332
1794
|
slug: z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
1333
1795
|
method: z.string().optional().describe("Filter by HTTP method"),
|
|
1334
1796
|
kind: kindSchema,
|
|
1335
|
-
q: z.string().optional().describe(
|
|
1336
|
-
|
|
1337
|
-
|
|
1797
|
+
q: z.string().optional().describe(
|
|
1798
|
+
"Substring to find in the path, headers, body and decoded email fields (up to 1,000 characters)"
|
|
1799
|
+
),
|
|
1800
|
+
from: durationOrTimestampSchema.optional().describe(
|
|
1801
|
+
'Start time as a timestamp (Unix ms) or a duration back from now like "1h" or "7d". A duration is counted from each call, so when you continue with a cursor pass a timestamp to keep the same range.'
|
|
1802
|
+
),
|
|
1803
|
+
to: durationOrTimestampSchema.optional().describe(
|
|
1804
|
+
'End time as a timestamp (Unix ms) or a duration back from now like "1h" or "7d". A duration is counted from each call, so when you continue with a cursor pass a timestamp to keep the same range.'
|
|
1805
|
+
),
|
|
1806
|
+
cursor: z.string().optional().describe(
|
|
1807
|
+
"The cursor from a previous count_requests result, to count the next stretch. Keep the other inputs the same."
|
|
1808
|
+
)
|
|
1338
1809
|
},
|
|
1339
|
-
withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1810
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, cursor }) => {
|
|
1811
|
+
const page = await client.requests.countPage({ slug, method, kind, q, from, to }, { cursor });
|
|
1812
|
+
const searchedUntil = isoTime(page.searchedUntil);
|
|
1813
|
+
return jsonContent({
|
|
1814
|
+
count: page.count,
|
|
1815
|
+
exact: page.exact,
|
|
1816
|
+
searchedUntil,
|
|
1817
|
+
cursor: page.cursor,
|
|
1818
|
+
...page.exact ? {} : {
|
|
1819
|
+
note: `At least ${page.count}, counted ${searchedUntil ? `back to ${searchedUntil}` : "over part of the history"}; ${page.cursor ? "call count_requests again with cursor to count further (the counts add up)." : "there is no cursor to count further with."}`
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1342
1822
|
})
|
|
1343
1823
|
);
|
|
1344
1824
|
server.tool(
|
|
1345
1825
|
"get_request",
|
|
1346
|
-
"Get
|
|
1826
|
+
"Get the details of a captured request by ID: method, path, headers and body. A request larger than the tool output has its body cut (bodyTruncated, bodySize) and a note saying where the whole request is available.",
|
|
1347
1827
|
getRequestShape,
|
|
1348
1828
|
withErrorHandling(async ({ requestId }) => {
|
|
1349
1829
|
const request = await client.requests.get(requestId);
|
|
1350
|
-
return
|
|
1830
|
+
return requestContent(request);
|
|
1351
1831
|
})
|
|
1352
1832
|
);
|
|
1353
1833
|
server.tool(
|
|
@@ -1356,7 +1836,7 @@ function registerTools(target, client, options = {}) {
|
|
|
1356
1836
|
waitForRequestShape,
|
|
1357
1837
|
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
1358
1838
|
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
1359
|
-
return
|
|
1839
|
+
return requestContent(request);
|
|
1360
1840
|
})
|
|
1361
1841
|
);
|
|
1362
1842
|
server.tool(
|
|
@@ -2110,7 +2590,7 @@ function registerTools(target, client, options = {}) {
|
|
|
2110
2590
|
);
|
|
2111
2591
|
server.tool(
|
|
2112
2592
|
"wait_for_approval",
|
|
2113
|
-
"Wait for the account's owner to answer an approval (from create_api_key, create_share_link, remove_domain, rename_account, connect_google, pick_spreadsheet or ai_setup) and collect what it grants or did, such as the new API key, the share link or AI setup's proposals. If they have not answered yet, call it again.",
|
|
2593
|
+
"Wait for the account's owner to answer an approval (from create_api_key, create_share_link, remove_domain, rename_account, delete_team, invite_to_team, connect_google, pick_spreadsheet or ai_setup) and collect what it grants or did, such as the new API key, the share link or AI setup's proposals. If they have not answered yet, call it again.",
|
|
2114
2594
|
{
|
|
2115
2595
|
id: z.string().describe("The approval id"),
|
|
2116
2596
|
timeout: durationOrTimestampSchema.default("45s").describe('How long to wait for the person, for example "45s"')
|
|
@@ -2473,6 +2953,15 @@ function registerTools(target, client, options = {}) {
|
|
|
2473
2953
|
}
|
|
2474
2954
|
|
|
2475
2955
|
export {
|
|
2956
|
+
TOOL_GROUPS,
|
|
2957
|
+
TOOL_GROUP_NAMES,
|
|
2958
|
+
TOOL_CATALOG,
|
|
2959
|
+
ENABLE_TOOLS,
|
|
2960
|
+
toolAnnotations,
|
|
2961
|
+
toolTitle,
|
|
2962
|
+
parseToolGroups,
|
|
2963
|
+
clientLoadsEverything,
|
|
2964
|
+
defaultToolGroups,
|
|
2476
2965
|
compactRequest,
|
|
2477
2966
|
createEndpointShape,
|
|
2478
2967
|
listEndpointsShape,
|