@webhooks-cc/mcp 1.22.0 → 1.24.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 +3 -3
- package/dist/bin/mcp.js +415 -31
- package/dist/{chunk-VO7HGSRN.mjs → chunk-SOQIUGLA.mjs} +414 -30
- package/dist/index.js +415 -31
- package/dist/index.mjs +2 -2
- package/dist/tools.js +414 -30
- package/dist/tools.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ For other agents, `npx skills add kroqdotdev/webhooks-cc` installs the same skil
|
|
|
34
34
|
- **claude.ai and Claude Desktop**: Settings, Connectors, add a custom connector with the URL `https://webhooks.cc/mcp`
|
|
35
35
|
- **ChatGPT**: add it as a custom connector (developer mode)
|
|
36
36
|
|
|
37
|
-
A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has
|
|
37
|
+
A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has 91 tools: `send_to`, `replay_request` and `share_port` are left out and `targetUrl` is refused, because a hosted server cannot reach your machine, and waits are capped at 50 seconds. It needs an account (no sandbox), and serves tools only, without prompts or resources.
|
|
38
38
|
|
|
39
39
|
### One-click
|
|
40
40
|
|
|
@@ -83,7 +83,7 @@ Leave out `env` to start without a key. Get your API key at [webhooks.cc/account
|
|
|
83
83
|
|
|
84
84
|
## What it exposes
|
|
85
85
|
|
|
86
|
-
The server exposes `
|
|
86
|
+
The server exposes `94` tools, `3` prompts, and `3` resource surfaces with an API key, the same `94` tools and the prompts with a saved connection, and `11` sandbox tools otherwise. The remote server exposes `91` tools.
|
|
87
87
|
|
|
88
88
|
### Endpoint tools
|
|
89
89
|
|
|
@@ -257,7 +257,7 @@ Use `list_provider_templates` to inspect templates, default events, signature he
|
|
|
257
257
|
Typical agent flows:
|
|
258
258
|
|
|
259
259
|
- Create an ephemeral endpoint, send a provider-signed webhook, and wait for capture
|
|
260
|
-
- Search retained requests across endpoints with
|
|
260
|
+
- Search retained requests across endpoints by substring, continuing with the cursor through long histories
|
|
261
261
|
- Compare two webhook attempts and extract only the JSON fields you care about
|
|
262
262
|
- Verify a captured signature against a known secret
|
|
263
263
|
- Run `test_webhook_flow` to create, send, capture, verify, replay, and clean up in one tool call
|
package/dist/bin/mcp.js
CHANGED
|
@@ -456,7 +456,7 @@ var import_zod3 = require("zod");
|
|
|
456
456
|
var import_sdk2 = require("@webhooks-cc/sdk");
|
|
457
457
|
|
|
458
458
|
// src/version.ts
|
|
459
|
-
var VERSION = true ? "1.
|
|
459
|
+
var VERSION = true ? "1.24.0" : "0.0.0-dev";
|
|
460
460
|
var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
|
|
461
461
|
|
|
462
462
|
// src/tools.ts
|
|
@@ -707,9 +707,28 @@ function keyText(key) {
|
|
|
707
707
|
}
|
|
708
708
|
function approvalResultText(result) {
|
|
709
709
|
if (typeof result.key === "string") return keyText(result);
|
|
710
|
+
if (typeof result.spreadsheetId === "string") {
|
|
711
|
+
const tabs = Array.isArray(result.tabs) ? result.tabs.join(", ") : "";
|
|
712
|
+
return `Approved. The chosen spreadsheet is ${String(result.spreadsheetId)} (${String(result.url)}), on connection ${String(result.connectionId)}${tabs ? `; its tabs: ${tabs}` : ""}. Use them in a Sheets destination's config.`;
|
|
713
|
+
}
|
|
714
|
+
if (Array.isArray(result.fields)) {
|
|
715
|
+
return `Approved. AI setup proposed (nothing is saved until you apply it):
|
|
716
|
+
${JSON.stringify(result, null, 2)}`;
|
|
717
|
+
}
|
|
718
|
+
if (typeof result.connectionId === "string") {
|
|
719
|
+
return `Approved. The Google account ${result.email ? `${String(result.email)} ` : ""}is connected as ${result.connectionId}.`;
|
|
720
|
+
}
|
|
710
721
|
if (typeof result.url === "string") {
|
|
711
722
|
return `Approved. The share link is ${result.url} (anyone with it can read the copy until ${String(result.expiresAt)}).`;
|
|
712
723
|
}
|
|
724
|
+
if (typeof result.invitedEmail === "string") {
|
|
725
|
+
const team = typeof result.teamName === "string" ? ` to "${result.teamName}"` : "";
|
|
726
|
+
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.";
|
|
727
|
+
return `Approved. Invited ${result.invitedEmail}${team} (invite ${String(result.id)}).${warning}`;
|
|
728
|
+
}
|
|
729
|
+
if (typeof result.deleted === "string" && typeof result.teamId === "string") {
|
|
730
|
+
return `Approved. The team ${result.deleted ? `"${result.deleted}" ` : ""}was deleted; its subscription has ended.`;
|
|
731
|
+
}
|
|
713
732
|
if (typeof result.removed === "string") {
|
|
714
733
|
return `Approved. The domain ${result.removed} was removed; mail to it is refused now.`;
|
|
715
734
|
}
|
|
@@ -784,6 +803,75 @@ function fitArrayField(value, key) {
|
|
|
784
803
|
}
|
|
785
804
|
return cut(low);
|
|
786
805
|
}
|
|
806
|
+
function isoTime(ms) {
|
|
807
|
+
if (ms === null || !Number.isFinite(ms)) return null;
|
|
808
|
+
const date = new Date(ms);
|
|
809
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
810
|
+
}
|
|
811
|
+
function searchStoppedNote(searchedUntil, order, hasCursor) {
|
|
812
|
+
const [direction, rest] = order === "asc" ? ["forward", "newer"] : ["back", "older"];
|
|
813
|
+
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.`;
|
|
814
|
+
return hasCursor ? `${reached} Call search_requests again with cursor to continue.` : reached;
|
|
815
|
+
}
|
|
816
|
+
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>).";
|
|
817
|
+
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}`;
|
|
818
|
+
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}`;
|
|
819
|
+
function clip(value, max) {
|
|
820
|
+
return typeof value === "string" ? sliceText(value, max) : void 0;
|
|
821
|
+
}
|
|
822
|
+
function listedSize(request) {
|
|
823
|
+
return jsonSize([[request]]) - 8;
|
|
824
|
+
}
|
|
825
|
+
function fitSearchRequests(requests, room) {
|
|
826
|
+
const share = Math.floor(room / Math.max(1, requests.length));
|
|
827
|
+
const fitted = requests.map(
|
|
828
|
+
(request) => shortenRequest(request, share, listedSize)
|
|
829
|
+
);
|
|
830
|
+
if (fitted.every((request) => listedSize(request) <= share)) return fitted;
|
|
831
|
+
return requests.map((request) => {
|
|
832
|
+
const { id, receivedAt } = request;
|
|
833
|
+
return { id, receivedAt, shortened: true };
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function shortenRequest(request, share, sizeOf) {
|
|
837
|
+
if (sizeOf(request) <= share) return request;
|
|
838
|
+
if (typeof request.body === "string") {
|
|
839
|
+
const body = request.body;
|
|
840
|
+
let keep = body.length - (sizeOf(request) - share) - 96;
|
|
841
|
+
while (keep > 0) {
|
|
842
|
+
const cut = {
|
|
843
|
+
...request,
|
|
844
|
+
body: sliceText(body, keep),
|
|
845
|
+
bodyTruncated: true,
|
|
846
|
+
bodySize: body.length
|
|
847
|
+
};
|
|
848
|
+
if (sizeOf(cut) <= share) return cut;
|
|
849
|
+
keep = Math.floor(keep / 2);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const email = request.email;
|
|
853
|
+
const summary = {
|
|
854
|
+
id: request.id,
|
|
855
|
+
slug: clip(request.slug, 64),
|
|
856
|
+
method: clip(request.method, 16),
|
|
857
|
+
path: clip(request.path, 200),
|
|
858
|
+
contentType: clip(request.contentType, 100),
|
|
859
|
+
size: request.size,
|
|
860
|
+
receivedAt: request.receivedAt,
|
|
861
|
+
kind: request.kind,
|
|
862
|
+
...email ? { subject: clip(email.subject, 200) } : {},
|
|
863
|
+
shortened: true,
|
|
864
|
+
...typeof request.note === "string" ? { note: request.note } : {}
|
|
865
|
+
};
|
|
866
|
+
return summary;
|
|
867
|
+
}
|
|
868
|
+
function requestContent(request) {
|
|
869
|
+
const compact = compactRequest(request);
|
|
870
|
+
if (jsonSize(compact) <= MAX_BODY_SIZE) return jsonContent(compact);
|
|
871
|
+
return jsonContent(
|
|
872
|
+
shortenRequest({ ...compact, note: REQUEST_SHORTENED_NOTE }, MAX_BODY_SIZE, jsonSize)
|
|
873
|
+
);
|
|
874
|
+
}
|
|
787
875
|
function jsonContent(value) {
|
|
788
876
|
return textContent(serializeJson(value));
|
|
789
877
|
}
|
|
@@ -828,6 +916,13 @@ function serializeError(error) {
|
|
|
828
916
|
} else if (error instanceof import_sdk.WebhooksCCError) {
|
|
829
917
|
payload.code = error.statusCode >= 500 ? "server_error" : "validation_error";
|
|
830
918
|
}
|
|
919
|
+
if (error instanceof import_sdk.WebhooksCCError && payload.hint === null) {
|
|
920
|
+
if (error.code === "invalid_cursor") {
|
|
921
|
+
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.";
|
|
922
|
+
} else if (error.code === "search_busy") {
|
|
923
|
+
payload.hint = "The account's other searches are still running. Wait retryAfter seconds, then call again with the same inputs.";
|
|
924
|
+
}
|
|
925
|
+
}
|
|
831
926
|
return JSON.stringify(payload, null, 2);
|
|
832
927
|
}
|
|
833
928
|
function withErrorHandling(handler) {
|
|
@@ -1279,6 +1374,124 @@ function registerTools(target, client, options = {}) {
|
|
|
1279
1374
|
return jsonContent({ shared: false, slug, teamId });
|
|
1280
1375
|
})
|
|
1281
1376
|
);
|
|
1377
|
+
server.tool(
|
|
1378
|
+
"create_team",
|
|
1379
|
+
"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.",
|
|
1380
|
+
{ name: import_zod2.z.string().trim().min(1).max(100).describe("The team's name") },
|
|
1381
|
+
withErrorHandling(async ({ name }) => jsonContent(await client.teams.create(name)))
|
|
1382
|
+
);
|
|
1383
|
+
server.tool(
|
|
1384
|
+
"rename_team",
|
|
1385
|
+
"Rename a team you own.",
|
|
1386
|
+
{
|
|
1387
|
+
teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)"),
|
|
1388
|
+
name: import_zod2.z.string().trim().min(1).max(100).describe("The new name")
|
|
1389
|
+
},
|
|
1390
|
+
withErrorHandling(async ({ teamId, name }) => {
|
|
1391
|
+
await client.teams.rename(teamId, name);
|
|
1392
|
+
return jsonContent({ renamed: true, teamId, name });
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
server.tool(
|
|
1396
|
+
"delete_team",
|
|
1397
|
+
"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.",
|
|
1398
|
+
{ teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)") },
|
|
1399
|
+
withErrorHandling(async ({ teamId }) => {
|
|
1400
|
+
const pending = await client.teams.delete(teamId);
|
|
1401
|
+
if (!pending.id) return textContent(`Deleted the team ${teamId}.`);
|
|
1402
|
+
return textContent(approvalAskText(pending));
|
|
1403
|
+
})
|
|
1404
|
+
);
|
|
1405
|
+
server.tool(
|
|
1406
|
+
"remove_team_member",
|
|
1407
|
+
"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.",
|
|
1408
|
+
{
|
|
1409
|
+
teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)"),
|
|
1410
|
+
userId: import_zod2.z.string().min(1).describe("The member's userId (from list_team_members)")
|
|
1411
|
+
},
|
|
1412
|
+
withErrorHandling(async ({ teamId, userId }) => {
|
|
1413
|
+
await client.teams.removeMember(teamId, userId);
|
|
1414
|
+
return jsonContent({ removed: true, teamId, userId });
|
|
1415
|
+
})
|
|
1416
|
+
);
|
|
1417
|
+
server.tool(
|
|
1418
|
+
"leave_team",
|
|
1419
|
+
"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.",
|
|
1420
|
+
{ teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)") },
|
|
1421
|
+
withErrorHandling(async ({ teamId }) => {
|
|
1422
|
+
await client.teams.leave(teamId);
|
|
1423
|
+
return jsonContent({ left: true, teamId });
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
server.tool(
|
|
1427
|
+
"invite_to_team",
|
|
1428
|
+
"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.",
|
|
1429
|
+
{
|
|
1430
|
+
teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)"),
|
|
1431
|
+
email: import_zod2.z.string().trim().email().describe("The address to invite")
|
|
1432
|
+
},
|
|
1433
|
+
withErrorHandling(async ({ teamId, email }) => {
|
|
1434
|
+
const pending = await client.teams.invite(teamId, email);
|
|
1435
|
+
if (!pending.id) return textContent(approvalResultText({ ...await pending.wait() }));
|
|
1436
|
+
return textContent(approvalAskText(pending));
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
server.tool(
|
|
1440
|
+
"list_invites",
|
|
1441
|
+
"List the team invites waiting for you to answer: team, who invited you, when, and the invite id for answer_invite.",
|
|
1442
|
+
{},
|
|
1443
|
+
withErrorHandling(async () => jsonContent(await client.teams.invites.list()))
|
|
1444
|
+
);
|
|
1445
|
+
server.tool(
|
|
1446
|
+
"answer_invite",
|
|
1447
|
+
"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.",
|
|
1448
|
+
{
|
|
1449
|
+
inviteId: import_zod2.z.string().min(1).describe("The invite's id, from list_invites"),
|
|
1450
|
+
answer: import_zod2.z.enum(["accept", "decline"]).describe("accept joins the team; decline turns it down")
|
|
1451
|
+
},
|
|
1452
|
+
withErrorHandling(async ({ inviteId, answer }) => {
|
|
1453
|
+
if (answer === "accept") {
|
|
1454
|
+
await client.teams.invites.accept(inviteId);
|
|
1455
|
+
return jsonContent({ accepted: true, inviteId });
|
|
1456
|
+
}
|
|
1457
|
+
await client.teams.invites.decline(inviteId);
|
|
1458
|
+
return jsonContent({ declined: true, inviteId });
|
|
1459
|
+
})
|
|
1460
|
+
);
|
|
1461
|
+
server.tool(
|
|
1462
|
+
"withdraw_invite",
|
|
1463
|
+
"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.",
|
|
1464
|
+
{
|
|
1465
|
+
teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)"),
|
|
1466
|
+
inviteId: import_zod2.z.string().min(1).describe("The invite's id (from list_team_members' pendingInvites)")
|
|
1467
|
+
},
|
|
1468
|
+
withErrorHandling(async ({ teamId, inviteId }) => {
|
|
1469
|
+
await client.teams.withdrawInvite(teamId, inviteId);
|
|
1470
|
+
return jsonContent({ withdrawn: true, teamId, inviteId });
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
1473
|
+
server.tool(
|
|
1474
|
+
"quote_team_change",
|
|
1475
|
+
"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.",
|
|
1476
|
+
{
|
|
1477
|
+
teamId: import_zod2.z.string().min(1).describe("Team id (from list_teams)"),
|
|
1478
|
+
seats: import_zod2.z.number().int().min(1).optional().describe("Price this seat count"),
|
|
1479
|
+
plan: import_zod2.z.enum(["team", "team_business"]).optional().describe("Price a move to this plan: team (Teams) or team_business (Team Business)"),
|
|
1480
|
+
interval: import_zod2.z.enum(["month", "year"]).optional().describe("Billing interval for plan (default month; Teams is monthly only)")
|
|
1481
|
+
},
|
|
1482
|
+
withErrorHandling(async ({ teamId, seats, plan, interval }) => {
|
|
1483
|
+
if (seats === void 0 === (plan === void 0)) {
|
|
1484
|
+
throw new Error("Give seats or plan, not both.");
|
|
1485
|
+
}
|
|
1486
|
+
if (seats !== void 0) {
|
|
1487
|
+
if (interval !== void 0) throw new Error("interval goes with plan, not seats.");
|
|
1488
|
+
return jsonContent(await client.billing.quoteTeamSeats(teamId, seats));
|
|
1489
|
+
}
|
|
1490
|
+
return jsonContent(
|
|
1491
|
+
await client.billing.quoteTeamPlan(teamId, plan, interval)
|
|
1492
|
+
);
|
|
1493
|
+
})
|
|
1494
|
+
);
|
|
1282
1495
|
server.tool(
|
|
1283
1496
|
"get_endpoint",
|
|
1284
1497
|
"Get details for a specific webhook endpoint by slug.",
|
|
@@ -1372,6 +1585,35 @@ function registerTools(target, client, options = {}) {
|
|
|
1372
1585
|
return jsonContent(holder);
|
|
1373
1586
|
})
|
|
1374
1587
|
);
|
|
1588
|
+
server.tool(
|
|
1589
|
+
"list_layout_templates",
|
|
1590
|
+
"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.",
|
|
1591
|
+
{ slug: import_zod2.z.string().describe("The endpoint slug") },
|
|
1592
|
+
withErrorHandling(async ({ slug }) => jsonContent(await client.endpoints.layoutTemplates(slug)))
|
|
1593
|
+
);
|
|
1594
|
+
server.tool(
|
|
1595
|
+
"add_layout_template",
|
|
1596
|
+
"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.",
|
|
1597
|
+
{
|
|
1598
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1599
|
+
template: import_zod2.z.string().min(1).describe("The template's id, from list_layout_templates")
|
|
1600
|
+
},
|
|
1601
|
+
withErrorHandling(
|
|
1602
|
+
async ({ slug, template }) => jsonContent(await client.endpoints.addLayoutTemplate(slug, template))
|
|
1603
|
+
)
|
|
1604
|
+
);
|
|
1605
|
+
server.tool(
|
|
1606
|
+
"preview_email_fields",
|
|
1607
|
+
"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.",
|
|
1608
|
+
{
|
|
1609
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1610
|
+
requestId: import_zod2.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
|
|
1611
|
+
emailFields: import_zod2.z.array(emailFieldSchema).max(20).optional().describe("Unsaved fields to try instead of the saved ones")
|
|
1612
|
+
},
|
|
1613
|
+
withErrorHandling(
|
|
1614
|
+
async ({ slug, requestId, emailFields }) => jsonContent(await client.endpoints.previewEmailFields(slug, { requestId, emailFields }))
|
|
1615
|
+
)
|
|
1616
|
+
);
|
|
1375
1617
|
server.tool(
|
|
1376
1618
|
"preview_response",
|
|
1377
1619
|
"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.",
|
|
@@ -1507,56 +1749,95 @@ function registerTools(target, client, options = {}) {
|
|
|
1507
1749
|
);
|
|
1508
1750
|
server.tool(
|
|
1509
1751
|
"search_requests",
|
|
1510
|
-
"Search captured
|
|
1752
|
+
"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.",
|
|
1511
1753
|
{
|
|
1512
1754
|
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
1513
1755
|
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
1514
1756
|
kind: kindSchema,
|
|
1515
|
-
q: import_zod2.z.string().optional().describe(
|
|
1516
|
-
|
|
1517
|
-
|
|
1757
|
+
q: import_zod2.z.string().optional().describe(
|
|
1758
|
+
"Substring to find in the path, headers, body and decoded email fields (up to 1,000 characters)"
|
|
1759
|
+
),
|
|
1760
|
+
from: durationOrTimestampSchema.optional().describe(
|
|
1761
|
+
'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.'
|
|
1762
|
+
),
|
|
1763
|
+
to: durationOrTimestampSchema.optional().describe(
|
|
1764
|
+
'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.'
|
|
1765
|
+
),
|
|
1518
1766
|
limit: import_zod2.z.number().int().min(1).max(200).default(50).describe("Max results to return"),
|
|
1519
|
-
offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("
|
|
1520
|
-
order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
|
|
1767
|
+
offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("Matches to skip (first call only; a cursor carries what is left of it)"),
|
|
1768
|
+
order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time"),
|
|
1769
|
+
cursor: import_zod2.z.string().optional().describe(
|
|
1770
|
+
"The cursor from a previous search_requests result, to continue that search. Keep the other inputs the same."
|
|
1771
|
+
)
|
|
1521
1772
|
},
|
|
1522
|
-
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
|
|
1523
|
-
const
|
|
1524
|
-
slug,
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1773
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order, cursor }) => {
|
|
1774
|
+
const page = await client.requests.searchPage(
|
|
1775
|
+
{ slug, method, kind, q, from, to, limit, offset, order },
|
|
1776
|
+
{ cursor }
|
|
1777
|
+
);
|
|
1778
|
+
const searchedUntil = isoTime(page.searchedUntil);
|
|
1779
|
+
const envelope = (requests2, shortened) => {
|
|
1780
|
+
const notes = [
|
|
1781
|
+
...page.complete ? [] : [searchStoppedNote(searchedUntil, order, Boolean(page.cursor))],
|
|
1782
|
+
...shortened ? [SEARCH_SHORTENED_NOTE] : []
|
|
1783
|
+
];
|
|
1784
|
+
return {
|
|
1785
|
+
requests: requests2,
|
|
1786
|
+
complete: page.complete,
|
|
1787
|
+
searchedUntil,
|
|
1788
|
+
cursor: page.cursor,
|
|
1789
|
+
...notes.length > 0 ? { note: notes.join(" ") } : {}
|
|
1790
|
+
};
|
|
1791
|
+
};
|
|
1792
|
+
const requests = page.requests.map(compactRequest);
|
|
1793
|
+
if (jsonSize(envelope(requests, false)) <= MAX_BODY_SIZE) {
|
|
1794
|
+
return jsonContent(envelope(requests, false));
|
|
1795
|
+
}
|
|
1796
|
+
const room = MAX_BODY_SIZE - jsonSize(envelope([], true)) - 8;
|
|
1797
|
+
return jsonContent(envelope(fitSearchRequests(requests, room), true));
|
|
1535
1798
|
})
|
|
1536
1799
|
);
|
|
1537
1800
|
server.tool(
|
|
1538
1801
|
"count_requests",
|
|
1539
|
-
"Count captured
|
|
1802
|
+
"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).",
|
|
1540
1803
|
{
|
|
1541
1804
|
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
1542
1805
|
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
1543
1806
|
kind: kindSchema,
|
|
1544
|
-
q: import_zod2.z.string().optional().describe(
|
|
1545
|
-
|
|
1546
|
-
|
|
1807
|
+
q: import_zod2.z.string().optional().describe(
|
|
1808
|
+
"Substring to find in the path, headers, body and decoded email fields (up to 1,000 characters)"
|
|
1809
|
+
),
|
|
1810
|
+
from: durationOrTimestampSchema.optional().describe(
|
|
1811
|
+
'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.'
|
|
1812
|
+
),
|
|
1813
|
+
to: durationOrTimestampSchema.optional().describe(
|
|
1814
|
+
'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.'
|
|
1815
|
+
),
|
|
1816
|
+
cursor: import_zod2.z.string().optional().describe(
|
|
1817
|
+
"The cursor from a previous count_requests result, to count the next stretch. Keep the other inputs the same."
|
|
1818
|
+
)
|
|
1547
1819
|
},
|
|
1548
|
-
withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1820
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, cursor }) => {
|
|
1821
|
+
const page = await client.requests.countPage({ slug, method, kind, q, from, to }, { cursor });
|
|
1822
|
+
const searchedUntil = isoTime(page.searchedUntil);
|
|
1823
|
+
return jsonContent({
|
|
1824
|
+
count: page.count,
|
|
1825
|
+
exact: page.exact,
|
|
1826
|
+
searchedUntil,
|
|
1827
|
+
cursor: page.cursor,
|
|
1828
|
+
...page.exact ? {} : {
|
|
1829
|
+
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."}`
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1551
1832
|
})
|
|
1552
1833
|
);
|
|
1553
1834
|
server.tool(
|
|
1554
1835
|
"get_request",
|
|
1555
|
-
"Get
|
|
1836
|
+
"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.",
|
|
1556
1837
|
getRequestShape,
|
|
1557
1838
|
withErrorHandling(async ({ requestId }) => {
|
|
1558
1839
|
const request = await client.requests.get(requestId);
|
|
1559
|
-
return
|
|
1840
|
+
return requestContent(request);
|
|
1560
1841
|
})
|
|
1561
1842
|
);
|
|
1562
1843
|
server.tool(
|
|
@@ -1565,7 +1846,7 @@ function registerTools(target, client, options = {}) {
|
|
|
1565
1846
|
waitForRequestShape,
|
|
1566
1847
|
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
1567
1848
|
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
1568
|
-
return
|
|
1849
|
+
return requestContent(request);
|
|
1569
1850
|
})
|
|
1570
1851
|
);
|
|
1571
1852
|
server.tool(
|
|
@@ -2319,7 +2600,7 @@ function registerTools(target, client, options = {}) {
|
|
|
2319
2600
|
);
|
|
2320
2601
|
server.tool(
|
|
2321
2602
|
"wait_for_approval",
|
|
2322
|
-
"Wait for the account's owner to answer an approval (from create_api_key, create_share_link, remove_domain or
|
|
2603
|
+
"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.",
|
|
2323
2604
|
{
|
|
2324
2605
|
id: import_zod2.z.string().describe("The approval id"),
|
|
2325
2606
|
timeout: durationOrTimestampSchema.default("45s").describe('How long to wait for the person, for example "45s"')
|
|
@@ -2487,6 +2768,109 @@ function registerTools(target, client, options = {}) {
|
|
|
2487
2768
|
return textContent(approvalAskText(pending));
|
|
2488
2769
|
})
|
|
2489
2770
|
);
|
|
2771
|
+
server.tool(
|
|
2772
|
+
"list_google_connections",
|
|
2773
|
+
"List the Google accounts connected for Google Sheets destinations (id, email, when connected), and whether Google Sheets is set up on this server.",
|
|
2774
|
+
{},
|
|
2775
|
+
withErrorHandling(async () => jsonContent(await client.google.connections()))
|
|
2776
|
+
);
|
|
2777
|
+
server.tool(
|
|
2778
|
+
"connect_google",
|
|
2779
|
+
"Connect a Google account for Google Sheets destinations. The account's owner must approve it: this returns a link and a code to show them, and approving takes them to Google to choose the account. Then call wait_for_approval for the new connection's id.",
|
|
2780
|
+
{},
|
|
2781
|
+
withErrorHandling(async () => textContent(approvalAskText(await client.google.connect())))
|
|
2782
|
+
);
|
|
2783
|
+
server.tool(
|
|
2784
|
+
"pick_spreadsheet",
|
|
2785
|
+
"Have the account's owner choose an existing spreadsheet from Google Drive with a connected account (to add rows to it from a Sheets destination). Returns a link and a code to show them; the page opens Google's picker. Then call wait_for_approval for the spreadsheet's id and tabs. To use a new spreadsheet instead, call create_spreadsheet.",
|
|
2786
|
+
{
|
|
2787
|
+
connectionId: import_zod2.z.string().describe("The Google connection's id, from list_google_connections")
|
|
2788
|
+
},
|
|
2789
|
+
withErrorHandling(
|
|
2790
|
+
async ({ connectionId }) => textContent(approvalAskText(await client.google.pick(connectionId)))
|
|
2791
|
+
)
|
|
2792
|
+
);
|
|
2793
|
+
server.tool(
|
|
2794
|
+
"create_spreadsheet",
|
|
2795
|
+
"Create a new spreadsheet with a header row in a connected Google account, for a Sheets destination. Needs no approval: the app reaches only spreadsheets it created or that were chosen for it.",
|
|
2796
|
+
{
|
|
2797
|
+
connectionId: import_zod2.z.string().describe("The Google connection's id, from list_google_connections"),
|
|
2798
|
+
title: import_zod2.z.string().max(200).optional().describe("The spreadsheet's title"),
|
|
2799
|
+
sheet: import_zod2.z.string().max(100).optional().describe('The tab rows go to (default "Sheet1")'),
|
|
2800
|
+
headers: import_zod2.z.array(import_zod2.z.string().max(200)).max(50).describe("The header row, one per column")
|
|
2801
|
+
},
|
|
2802
|
+
withErrorHandling(
|
|
2803
|
+
async ({ connectionId, title, sheet, headers }) => jsonContent(await client.google.createSpreadsheet({ connectionId, title, sheet, headers }))
|
|
2804
|
+
)
|
|
2805
|
+
);
|
|
2806
|
+
server.tool(
|
|
2807
|
+
"list_sheet_tabs",
|
|
2808
|
+
"List a spreadsheet's tabs (one created by the app or chosen with pick_spreadsheet), to choose the one a Sheets destination writes to.",
|
|
2809
|
+
{
|
|
2810
|
+
connectionId: import_zod2.z.string().describe("The Google connection's id"),
|
|
2811
|
+
spreadsheetId: import_zod2.z.string().describe("The spreadsheet's id")
|
|
2812
|
+
},
|
|
2813
|
+
withErrorHandling(
|
|
2814
|
+
async ({ connectionId, spreadsheetId }) => jsonContent({ tabs: await client.google.tabs(connectionId, spreadsheetId) })
|
|
2815
|
+
)
|
|
2816
|
+
);
|
|
2817
|
+
server.tool(
|
|
2818
|
+
"disconnect_google",
|
|
2819
|
+
"Disconnect a Google account (id from list_google_connections) and revoke its access at Google. Sheets destinations using it fail until another account is chosen.",
|
|
2820
|
+
{
|
|
2821
|
+
connectionId: import_zod2.z.string().describe("The Google connection's id")
|
|
2822
|
+
},
|
|
2823
|
+
withErrorHandling(async ({ connectionId }) => {
|
|
2824
|
+
await client.google.disconnect(connectionId);
|
|
2825
|
+
return textContent(`Disconnected the Google account ${connectionId}.`);
|
|
2826
|
+
})
|
|
2827
|
+
);
|
|
2828
|
+
server.tool(
|
|
2829
|
+
"ai_setup_status",
|
|
2830
|
+
"Whether AI setup is offered for an endpoint, and how many AI assists the plan has left in a rolling 30 days.",
|
|
2831
|
+
{
|
|
2832
|
+
slug: import_zod2.z.string().describe("The endpoint's slug")
|
|
2833
|
+
},
|
|
2834
|
+
withErrorHandling(async ({ slug }) => jsonContent(await client.ai.status(slug)))
|
|
2835
|
+
);
|
|
2836
|
+
server.tool(
|
|
2837
|
+
"ai_setup",
|
|
2838
|
+
"Have AI propose email fields (or a layout, or a repaired field) from 1 to 5 of an endpoint's captured emails. It sends those emails (subject, sender, recipients, text, headers; never attachments) to Anthropic's model, so the account's owner must approve it: this returns a link and a code to show them, and the page lists the emails. Then call wait_for_approval for the proposals, each with the value it found in every email. Nothing is saved: apply the fields with update_endpoint. Uses one AI assist.",
|
|
2839
|
+
{
|
|
2840
|
+
slug: import_zod2.z.string().describe("The endpoint's slug"),
|
|
2841
|
+
requestIds: import_zod2.z.array(import_zod2.z.string()).min(1).max(5).describe("1 to 5 of the endpoint's email request ids, from list_emails"),
|
|
2842
|
+
ask: import_zod2.z.union([
|
|
2843
|
+
import_zod2.z.object({
|
|
2844
|
+
kind: import_zod2.z.literal("field"),
|
|
2845
|
+
name: import_zod2.z.string().describe("The field's name: letters, digits and _"),
|
|
2846
|
+
description: import_zod2.z.string().describe('What the field is, such as "the order number"')
|
|
2847
|
+
}),
|
|
2848
|
+
import_zod2.z.object({
|
|
2849
|
+
kind: import_zod2.z.literal("layout"),
|
|
2850
|
+
description: import_zod2.z.string().optional().describe("What these emails are")
|
|
2851
|
+
}),
|
|
2852
|
+
import_zod2.z.object({
|
|
2853
|
+
kind: import_zod2.z.literal("repair"),
|
|
2854
|
+
field: import_zod2.z.object({
|
|
2855
|
+
name: import_zod2.z.string(),
|
|
2856
|
+
from: import_zod2.z.string(),
|
|
2857
|
+
header: import_zod2.z.string().optional(),
|
|
2858
|
+
pattern: import_zod2.z.string()
|
|
2859
|
+
}).describe("The field to repair, as the endpoint has it")
|
|
2860
|
+
})
|
|
2861
|
+
]).describe("What to propose")
|
|
2862
|
+
},
|
|
2863
|
+
withErrorHandling(
|
|
2864
|
+
async ({ slug, requestIds, ask }) => textContent(
|
|
2865
|
+
approvalAskText(
|
|
2866
|
+
await client.ai.setup(slug, {
|
|
2867
|
+
requestIds,
|
|
2868
|
+
ask
|
|
2869
|
+
})
|
|
2870
|
+
)
|
|
2871
|
+
)
|
|
2872
|
+
)
|
|
2873
|
+
);
|
|
2490
2874
|
server.tool(
|
|
2491
2875
|
"stop_share",
|
|
2492
2876
|
"Stop a public preview or live tunnel by its id (from list_shares). The CLI running it is told why and exits.",
|