@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/bin/mcp.js
CHANGED
|
@@ -418,28 +418,75 @@ var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HT
|
|
|
418
418
|
var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
|
|
419
419
|
var EMAIL_SCAN_LIMIT = 100;
|
|
420
420
|
var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
|
|
421
|
-
var
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
var CONDITION_FIELD_NAMES = Object.keys(import_sdk.CONDITION_FIELDS);
|
|
422
|
+
var CONDITION_OP_NAMES = import_sdk.CONDITION_OPS;
|
|
423
|
+
function conditionSchema(use) {
|
|
424
|
+
const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => import_sdk.CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
|
|
425
|
+
return import_zod2.z.object({
|
|
426
|
+
field: import_zod2.z.enum(fields).describe(
|
|
427
|
+
use === "response" ? "What to read: method, path, header (name), query (name), body (text), body_path (path into a JSON or form body), content_type, size" : "What to read: method, path, header (name), query (name), body, body_path (path), content_type, size, kind (http or email), email (name: subject, from, to, text or tag), spam_score, email_auth (name: spf, dkim or dmarc), signature (verified, failed or missing)"
|
|
428
|
+
),
|
|
429
|
+
op: import_zod2.z.enum(CONDITION_OP_NAMES).describe(
|
|
430
|
+
"eq, neq, contains, not_contains, starts_with, ends_with, matches (glob: * one path segment, ** any, ? one character), regex (RE2, searched anywhere; (?i) ignores case), exists, not_exists, gt, gte, lt, lte (numbers)"
|
|
431
|
+
),
|
|
432
|
+
value: import_zod2.z.string().max(import_sdk.MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
|
|
433
|
+
name: import_zod2.z.string().max(import_sdk.MAX_CONDITION_NAME_LEN).optional().describe("Header or query parameter name; the email part; spf, dkim or dmarc"),
|
|
434
|
+
path: import_zod2.z.string().max(import_sdk.MAX_CONDITION_PATH_LEN).optional().describe('Dot path for body_path, e.g. data.object.status or items.0.sku or a["odd.key"]')
|
|
435
|
+
}).superRefine((condition, ctx) => {
|
|
436
|
+
for (const issue of (0, import_sdk.conditionIssues)(condition, { use })) {
|
|
437
|
+
ctx.addIssue({ code: "custom", message: issue });
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
var ruleConditionSchema = conditionSchema("response");
|
|
442
|
+
var destinationFields = {
|
|
443
|
+
name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
|
|
444
|
+
enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
|
|
445
|
+
http: import_zod2.z.boolean().optional().describe("Takes HTTP requests (default true)"),
|
|
446
|
+
email: import_zod2.z.boolean().optional().describe("Takes emails (default true)"),
|
|
447
|
+
filter: import_zod2.z.object({
|
|
448
|
+
logic: import_zod2.z.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
|
|
449
|
+
conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
|
|
450
|
+
}).nullable().optional().describe(
|
|
451
|
+
"Send only captures that match (null: everything it takes). Conditions like response rules plus kind, email (name: subject, from, to, text, tag), spam_score, email_auth (name: spf, dkim, dmarc) and signature (verified, failed, missing)"
|
|
426
452
|
),
|
|
427
|
-
|
|
428
|
-
|
|
453
|
+
format: import_zod2.z.enum([
|
|
454
|
+
"auto",
|
|
455
|
+
"as_received",
|
|
456
|
+
"json",
|
|
457
|
+
"template",
|
|
458
|
+
"chat",
|
|
459
|
+
"postmark",
|
|
460
|
+
"sendgrid",
|
|
461
|
+
"mailgun",
|
|
462
|
+
"cloudmailin"
|
|
463
|
+
]).optional().describe(
|
|
464
|
+
"auto (pick from the URL), as_received (relay HTTP requests unchanged; emails as signed JSON), json (signed JSON envelope), template (your bodyTemplate, signed like json), chat (Slack or Discord message), or an inbound mail provider's shape for emails (postmark, sendgrid, mailgun, cloudmailin; HTTP requests then go as received), to test the inbound handler you wrote for that provider"
|
|
429
465
|
),
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
(
|
|
437
|
-
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
466
|
+
mailgunSigningKey: import_zod2.z.string().max(200).nullable().optional().describe(
|
|
467
|
+
"For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
|
|
468
|
+
),
|
|
469
|
+
bodyTemplate: import_zod2.z.unknown().optional().describe(
|
|
470
|
+
`For format template: any JSON value whose strings hold {{ placeholders }}, e.g. {"event": "{{ json.type }}", "amount": "{{ json.data.amount }}", "code": "{{ codes.0 }}"}. A string that is only a placeholder keeps the value's type; a missing field becomes null. HTTP requests read method, path, query.x, headers.x, body, json.path, id, receivedAt, signature; emails read the email.received fields (subject, from.address, to, text, codes, links, auth, spam, tag)`
|
|
471
|
+
),
|
|
472
|
+
messageTemplate: import_zod2.z.string().max(4e3).nullable().optional().describe(
|
|
473
|
+
"For format chat: the message text with {{ placeholders }}; null sends the standard message"
|
|
474
|
+
),
|
|
475
|
+
headers: import_zod2.z.array(
|
|
476
|
+
import_zod2.z.object({
|
|
477
|
+
name: import_zod2.z.string().describe("Header name, e.g. Authorization"),
|
|
478
|
+
value: import_zod2.z.string().nullable().describe("Value, or null to keep the stored one")
|
|
479
|
+
})
|
|
480
|
+
).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
|
|
481
|
+
appendPath: import_zod2.z.boolean().optional().describe("Append the captured path to the URL (as_received)"),
|
|
482
|
+
retrySeconds: import_zod2.z.union([import_zod2.z.literal(0), import_zod2.z.literal(3600), import_zod2.z.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
|
|
483
|
+
keepOrder: import_zod2.z.boolean().optional().describe("One delivery at a time, in capture order"),
|
|
484
|
+
sentField: import_zod2.z.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
|
|
485
|
+
throttleSeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe(
|
|
486
|
+
"At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
|
|
487
|
+
),
|
|
488
|
+
delaySeconds: import_zod2.z.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
|
|
489
|
+
};
|
|
443
490
|
var responseRuleSchema = import_zod2.z.object({
|
|
444
491
|
name: import_zod2.z.string().max(import_sdk.MAX_RULE_NAME_LEN).optional().describe(`Human-readable rule name (max ${import_sdk.MAX_RULE_NAME_LEN} chars)`),
|
|
445
492
|
enabled: import_zod2.z.boolean().optional().default(true).describe("Whether this rule is active"),
|
|
@@ -450,8 +497,10 @@ var responseRuleSchema = import_zod2.z.object({
|
|
|
450
497
|
var responseRulesSchema = import_zod2.z.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
|
|
451
498
|
var mockResponseSchema = import_zod2.z.object({
|
|
452
499
|
status: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_STATUS_MIN).max(import_sdk.MOCK_RESPONSE_STATUS_MAX).describe(`HTTP status code (${import_sdk.MOCK_RESPONSE_STATUS_MIN}-${import_sdk.MOCK_RESPONSE_STATUS_MAX})`),
|
|
453
|
-
body: import_zod2.z.string().default("").describe(
|
|
454
|
-
|
|
500
|
+
body: import_zod2.z.string().default("").describe(
|
|
501
|
+
'Response body (default: empty). {{ placeholders }} are filled in from the request: {{ json.id }} (a JSON or form body field), {{ body }}, {{ headers.user-agent }}, {{ query.page }}, {{ method }}, {{ path }}, {{ id }}, {{ uuid }}, {{ now }}; filters: | default: "x", | json, | lower, | upper, | truncate: 80'
|
|
502
|
+
),
|
|
503
|
+
headers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
|
|
455
504
|
delay: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_DELAY_MIN).max(import_sdk.MOCK_RESPONSE_DELAY_MAX).optional().describe(
|
|
456
505
|
`Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
|
|
457
506
|
)
|
|
@@ -705,7 +754,9 @@ async function waitForMultipleRequests(client, endpointSlug, options) {
|
|
|
705
754
|
};
|
|
706
755
|
}
|
|
707
756
|
}
|
|
708
|
-
await sleep(
|
|
757
|
+
await sleep(
|
|
758
|
+
Math.min(Math.max(10, pollIntervalMs), Math.max(0, timeoutMs - (Date.now() - startedAt)))
|
|
759
|
+
);
|
|
709
760
|
}
|
|
710
761
|
return {
|
|
711
762
|
requests,
|
|
@@ -806,6 +857,8 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
806
857
|
"tag",
|
|
807
858
|
"subject",
|
|
808
859
|
"from",
|
|
860
|
+
"fields",
|
|
861
|
+
"replyText",
|
|
809
862
|
"size",
|
|
810
863
|
"test",
|
|
811
864
|
"spam",
|
|
@@ -815,6 +868,11 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
815
868
|
"html",
|
|
816
869
|
"htmlTruncated"
|
|
817
870
|
];
|
|
871
|
+
function shrinkRendered(rendered, max) {
|
|
872
|
+
const body = typeof rendered.body === "string" ? rendered.body : "";
|
|
873
|
+
if (body.length <= max - 1024) return rendered;
|
|
874
|
+
return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
|
|
875
|
+
}
|
|
818
876
|
function emailDetail(client, email, view, includeHtml) {
|
|
819
877
|
const { data } = client.emails.toJson(email, {
|
|
820
878
|
endpoint: view.endpoint,
|
|
@@ -856,7 +914,67 @@ function emailDetail(client, email, view, includeHtml) {
|
|
|
856
914
|
]);
|
|
857
915
|
return detail;
|
|
858
916
|
}
|
|
859
|
-
|
|
917
|
+
var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
|
|
918
|
+
var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
|
|
919
|
+
var REMOTE_MAX_WAIT_MS = 5e4;
|
|
920
|
+
var REMOTE_MIN_POLL_MS = 1e3;
|
|
921
|
+
function durationMs(value) {
|
|
922
|
+
if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
|
|
923
|
+
if (typeof value !== "string") return null;
|
|
924
|
+
try {
|
|
925
|
+
const ms = (0, import_sdk.parseDuration)(value);
|
|
926
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
927
|
+
} catch {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function toolTarget(target, remote, handles) {
|
|
932
|
+
return {
|
|
933
|
+
tool(name, description, shape, handler) {
|
|
934
|
+
if (remote && LOCAL_ONLY_TOOLS.has(name)) return { remove() {
|
|
935
|
+
} };
|
|
936
|
+
const wrapped = remote ? async (args2) => {
|
|
937
|
+
const input = args2;
|
|
938
|
+
if (TARGET_URL_TOOLS.has(name) && input.targetUrl !== void 0) {
|
|
939
|
+
return {
|
|
940
|
+
...textContent(
|
|
941
|
+
"targetUrl needs the local MCP server (npx -y @webhooks-cc/mcp): the hosted server cannot reach your machine. Leave it out here."
|
|
942
|
+
),
|
|
943
|
+
isError: true
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const bounded = { ...input };
|
|
947
|
+
if ("pollInterval" in shape && input.pollInterval === void 0) {
|
|
948
|
+
bounded.pollInterval = REMOTE_MIN_POLL_MS;
|
|
949
|
+
}
|
|
950
|
+
for (const [key, bound] of [
|
|
951
|
+
["timeout", (ms) => Math.min(ms, REMOTE_MAX_WAIT_MS)],
|
|
952
|
+
[
|
|
953
|
+
"pollInterval",
|
|
954
|
+
(ms) => Math.min(Math.max(ms, REMOTE_MIN_POLL_MS), REMOTE_MAX_WAIT_MS)
|
|
955
|
+
]
|
|
956
|
+
]) {
|
|
957
|
+
if (bounded[key] === void 0) continue;
|
|
958
|
+
const ms = durationMs(bounded[key]);
|
|
959
|
+
if (ms === null) {
|
|
960
|
+
return {
|
|
961
|
+
...textContent(`${key} must be a duration such as "30s" or milliseconds.`),
|
|
962
|
+
isError: true
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
bounded[key] = bound(ms);
|
|
966
|
+
}
|
|
967
|
+
return handler(bounded);
|
|
968
|
+
} : handler;
|
|
969
|
+
const handle = target.tool(name, description, shape, wrapped);
|
|
970
|
+
handles.push(handle);
|
|
971
|
+
return handle;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function registerTools(target, client, options = {}) {
|
|
976
|
+
const handles = [];
|
|
977
|
+
const server = toolTarget(target, options.remote === true, handles);
|
|
860
978
|
server.tool(
|
|
861
979
|
"create_endpoint",
|
|
862
980
|
"Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
|
|
@@ -977,7 +1095,7 @@ function registerTools(server, client) {
|
|
|
977
1095
|
);
|
|
978
1096
|
server.tool(
|
|
979
1097
|
"update_endpoint",
|
|
980
|
-
"Update an endpoint name, mock response, conditional response rules, signing configuration,
|
|
1098
|
+
"Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
|
|
981
1099
|
{
|
|
982
1100
|
slug: import_zod2.z.string().describe("The endpoint slug to update"),
|
|
983
1101
|
name: import_zod2.z.string().optional().describe("New display name"),
|
|
@@ -991,7 +1109,17 @@ function registerTools(server, client) {
|
|
|
991
1109
|
signingHeader: import_zod2.z.string().nullable().optional().describe(
|
|
992
1110
|
"Custom signature header name (only for generic-hmac provider), or null to clear"
|
|
993
1111
|
),
|
|
994
|
-
showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
|
|
1112
|
+
showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
|
|
1113
|
+
emailFields: import_zod2.z.array(
|
|
1114
|
+
import_zod2.z.object({
|
|
1115
|
+
name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
|
|
1116
|
+
from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
|
|
1117
|
+
header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
|
|
1118
|
+
pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
|
|
1119
|
+
})
|
|
1120
|
+
).max(20).nullable().optional().describe(
|
|
1121
|
+
"Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
|
|
1122
|
+
)
|
|
995
1123
|
},
|
|
996
1124
|
withErrorHandling(
|
|
997
1125
|
async ({
|
|
@@ -1003,7 +1131,8 @@ function registerTools(server, client) {
|
|
|
1003
1131
|
signingProvider,
|
|
1004
1132
|
signingSecret,
|
|
1005
1133
|
signingHeader,
|
|
1006
|
-
showEmailExtracts
|
|
1134
|
+
showEmailExtracts,
|
|
1135
|
+
emailFields
|
|
1007
1136
|
}) => {
|
|
1008
1137
|
const endpoint = await client.endpoints.update(slug, {
|
|
1009
1138
|
name,
|
|
@@ -1013,12 +1142,33 @@ function registerTools(server, client) {
|
|
|
1013
1142
|
signingProvider,
|
|
1014
1143
|
signingSecret,
|
|
1015
1144
|
signingHeader,
|
|
1016
|
-
showEmailExtracts
|
|
1145
|
+
showEmailExtracts,
|
|
1146
|
+
emailFields
|
|
1017
1147
|
});
|
|
1018
1148
|
return jsonContent(endpoint);
|
|
1019
1149
|
}
|
|
1020
1150
|
)
|
|
1021
1151
|
);
|
|
1152
|
+
server.tool(
|
|
1153
|
+
"preview_response",
|
|
1154
|
+
"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.",
|
|
1155
|
+
{
|
|
1156
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1157
|
+
requestId: import_zod2.z.string().optional().describe(
|
|
1158
|
+
"A captured request on the endpoint (default: the newest HTTP request, or a sample)"
|
|
1159
|
+
),
|
|
1160
|
+
mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
|
|
1161
|
+
responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
|
|
1162
|
+
},
|
|
1163
|
+
withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
|
|
1164
|
+
const preview = await client.endpoints.previewResponse(slug, {
|
|
1165
|
+
...requestId ? { requestId } : {},
|
|
1166
|
+
...mockResponse !== void 0 ? { mockResponse } : {},
|
|
1167
|
+
...responseRules !== void 0 ? { responseRules } : {}
|
|
1168
|
+
});
|
|
1169
|
+
return jsonContent(preview);
|
|
1170
|
+
})
|
|
1171
|
+
);
|
|
1022
1172
|
server.tool(
|
|
1023
1173
|
"delete_endpoint",
|
|
1024
1174
|
"Delete a webhook endpoint and all its captured requests.",
|
|
@@ -1234,6 +1384,39 @@ function registerTools(server, client) {
|
|
|
1234
1384
|
});
|
|
1235
1385
|
})
|
|
1236
1386
|
);
|
|
1387
|
+
server.tool(
|
|
1388
|
+
"render_email",
|
|
1389
|
+
"A captured email in the shape an inbound mail provider posts it (postmark, sendgrid, mailgun, cloudmailin), to test the inbound handler you wrote for that provider against real mail. Returns the method, headers and body; with targetUrl (your local handler), posts it there and returns the answer. A Mailgun form is signed when you pass the signing key your handler checks.",
|
|
1390
|
+
{
|
|
1391
|
+
requestId: import_zod2.z.string().describe("The captured email's request ID"),
|
|
1392
|
+
format: import_zod2.z.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
|
|
1393
|
+
mailgunSigningKey: import_zod2.z.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
|
|
1394
|
+
targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
|
|
1395
|
+
},
|
|
1396
|
+
withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
|
|
1397
|
+
const rendered = await client.emails.render(requestId, {
|
|
1398
|
+
format,
|
|
1399
|
+
...mailgunSigningKey ? { mailgunSigningKey } : {}
|
|
1400
|
+
});
|
|
1401
|
+
if (!targetUrl) {
|
|
1402
|
+
return jsonContent(
|
|
1403
|
+
shrinkRendered(rendered, MAX_BODY_SIZE)
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
const response = await fetch(targetUrl, {
|
|
1407
|
+
method: "POST",
|
|
1408
|
+
headers: { "content-type": rendered.headers["content-type"] },
|
|
1409
|
+
body: rendered.body,
|
|
1410
|
+
redirect: "manual"
|
|
1411
|
+
});
|
|
1412
|
+
return jsonContent({
|
|
1413
|
+
format,
|
|
1414
|
+
status: response.status,
|
|
1415
|
+
statusText: response.statusText,
|
|
1416
|
+
body: await readBodyTruncated(response)
|
|
1417
|
+
});
|
|
1418
|
+
})
|
|
1419
|
+
);
|
|
1237
1420
|
server.tool(
|
|
1238
1421
|
"compare_requests",
|
|
1239
1422
|
"Compare two captured requests and show the structured differences.",
|
|
@@ -1473,7 +1656,7 @@ function registerTools(server, client) {
|
|
|
1473
1656
|
verifySignature: shouldVerify,
|
|
1474
1657
|
cleanup
|
|
1475
1658
|
}) => {
|
|
1476
|
-
const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s" });
|
|
1659
|
+
const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s", pollInterval: "1s" });
|
|
1477
1660
|
if (mockStatus !== void 0) {
|
|
1478
1661
|
flow.setMock({
|
|
1479
1662
|
status: mockStatus,
|
|
@@ -1598,10 +1781,14 @@ function registerTools(server, client) {
|
|
|
1598
1781
|
includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
|
|
1599
1782
|
},
|
|
1600
1783
|
withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1784
|
+
const view = await emailEndpointView(client, endpointSlug);
|
|
1785
|
+
const email = await client.emails.waitFor(endpointSlug, {
|
|
1786
|
+
tag,
|
|
1787
|
+
subject,
|
|
1788
|
+
from,
|
|
1789
|
+
timeout,
|
|
1790
|
+
since
|
|
1791
|
+
});
|
|
1605
1792
|
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1606
1793
|
})
|
|
1607
1794
|
);
|
|
@@ -1617,9 +1804,79 @@ function registerTools(server, client) {
|
|
|
1617
1804
|
return jsonContent({ ...result, address: client.emails.address(slug, tag) });
|
|
1618
1805
|
})
|
|
1619
1806
|
);
|
|
1807
|
+
server.tool(
|
|
1808
|
+
"list_destinations",
|
|
1809
|
+
"List where an endpoint's captures go: its destinations (up to 10) with their URL, kinds, filter, format, templates and timing, and with summary: true their last 24 hours. Owner only.",
|
|
1810
|
+
{
|
|
1811
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1812
|
+
summary: import_zod2.z.boolean().default(true).describe("Add each destination's last 24 hours")
|
|
1813
|
+
},
|
|
1814
|
+
withErrorHandling(async ({ slug, summary }) => {
|
|
1815
|
+
return jsonContent(await client.destinations.list(slug, { summary }));
|
|
1816
|
+
})
|
|
1817
|
+
);
|
|
1818
|
+
server.tool(
|
|
1819
|
+
"add_destination",
|
|
1820
|
+
"Add a destination for an endpoint's captures: your server (as received or signed JSON), a Slack or Discord channel (chat, optionally your own messageTemplate, throttleSeconds 1 for a heads-up), or a JSON body you shape with bodyTemplate (for Zapier, n8n or your API). filter sends only matching captures; delaySeconds makes it a callback. Each destination gets its own signing secret. Check it with preview_destination before relying on it. Owner only.",
|
|
1821
|
+
{
|
|
1822
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1823
|
+
url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
|
|
1824
|
+
...destinationFields
|
|
1825
|
+
},
|
|
1826
|
+
withErrorHandling(async ({ slug, ...input }) => {
|
|
1827
|
+
return jsonContent(await client.destinations.create(slug, input));
|
|
1828
|
+
})
|
|
1829
|
+
);
|
|
1830
|
+
server.tool(
|
|
1831
|
+
"update_destination",
|
|
1832
|
+
"Change a destination: any of its fields; only the ones you pass change. Owner only.",
|
|
1833
|
+
{
|
|
1834
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1835
|
+
destinationId: import_zod2.z.string().describe("The destination's id (from list_destinations)"),
|
|
1836
|
+
url: httpUrlSchema.optional().describe("A new URL"),
|
|
1837
|
+
...destinationFields
|
|
1838
|
+
},
|
|
1839
|
+
withErrorHandling(async ({ slug, destinationId, ...input }) => {
|
|
1840
|
+
return jsonContent(await client.destinations.update(slug, destinationId, input));
|
|
1841
|
+
})
|
|
1842
|
+
);
|
|
1843
|
+
server.tool(
|
|
1844
|
+
"remove_destination",
|
|
1845
|
+
"Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
|
|
1846
|
+
{
|
|
1847
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1848
|
+
destinationId: import_zod2.z.string().describe("The destination's id")
|
|
1849
|
+
},
|
|
1850
|
+
withErrorHandling(async ({ slug, destinationId }) => {
|
|
1851
|
+
await client.destinations.delete(slug, destinationId);
|
|
1852
|
+
return textContent(`Destination ${destinationId} removed.`);
|
|
1853
|
+
})
|
|
1854
|
+
);
|
|
1855
|
+
server.tool(
|
|
1856
|
+
"preview_destination",
|
|
1857
|
+
"Dry run of a destination against a captured request: whether its filter lets the request through and the exact request it would send (method, URL, headers, body with placeholders filled in). Nothing is sent. Use destinationId for a saved destination, destination for unsaved fields (on top of the saved ones, or with a url on their own), or both.",
|
|
1858
|
+
{
|
|
1859
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1860
|
+
destinationId: import_zod2.z.string().optional().describe("A saved destination"),
|
|
1861
|
+
destination: import_zod2.z.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
|
|
1862
|
+
requestId: import_zod2.z.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
|
|
1863
|
+
},
|
|
1864
|
+
withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
|
|
1865
|
+
if (!destinationId && !destination?.url) {
|
|
1866
|
+
throw new Error("Pass destinationId, or a destination with a url");
|
|
1867
|
+
}
|
|
1868
|
+
return jsonContent(
|
|
1869
|
+
await client.destinations.preview(slug, {
|
|
1870
|
+
...destinationId ? { destinationId } : {},
|
|
1871
|
+
...destination ? { destination } : {},
|
|
1872
|
+
...requestId ? { requestId } : {}
|
|
1873
|
+
})
|
|
1874
|
+
);
|
|
1875
|
+
})
|
|
1876
|
+
);
|
|
1620
1877
|
server.tool(
|
|
1621
1878
|
"configure_forwarding",
|
|
1622
|
-
"Change an endpoint's forwarding: the URL, on or off, which kinds
|
|
1879
|
+
"Change an endpoint's primary forwarding destination (the one the endpoint's forward* fields stand for): the URL, on or off, which kinds, the format, your own headers, and the advanced options. Only the arguments you pass change. Owner only. For several destinations, filters, templates, throttling or delays, use add_destination and update_destination.",
|
|
1623
1880
|
{
|
|
1624
1881
|
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1625
1882
|
url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
|
|
@@ -1644,8 +1901,8 @@ function registerTools(server, client) {
|
|
|
1644
1901
|
"JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
|
|
1645
1902
|
)
|
|
1646
1903
|
},
|
|
1647
|
-
withErrorHandling(async ({ slug, ...
|
|
1648
|
-
const endpoint = await client.forwarding.configure(slug,
|
|
1904
|
+
withErrorHandling(async ({ slug, ...options2 }) => {
|
|
1905
|
+
const endpoint = await client.forwarding.configure(slug, options2);
|
|
1649
1906
|
return jsonContent({
|
|
1650
1907
|
slug: endpoint.slug,
|
|
1651
1908
|
forwardEnabled: endpoint.forwardEnabled ?? false,
|
|
@@ -1667,23 +1924,32 @@ function registerTools(server, client) {
|
|
|
1667
1924
|
"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.",
|
|
1668
1925
|
{
|
|
1669
1926
|
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1927
|
+
destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
|
|
1670
1928
|
rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
|
|
1671
1929
|
},
|
|
1672
|
-
withErrorHandling(async ({ slug, rotate }) => {
|
|
1673
|
-
const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1930
|
+
withErrorHandling(async ({ slug, destinationId, rotate }) => {
|
|
1931
|
+
const secret = destinationId ? rotate ? await client.destinations.rotateSecret(slug, destinationId) : await client.destinations.secret(slug, destinationId) : rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1674
1932
|
return jsonContent({ secret, rotated: rotate });
|
|
1675
1933
|
})
|
|
1676
1934
|
);
|
|
1677
1935
|
server.tool(
|
|
1678
1936
|
"test_forwarding",
|
|
1679
|
-
"Send the endpoint's newest captured request of a kind (or a sample) to
|
|
1937
|
+
"Send the endpoint's newest captured request of a kind (or a sample, or requestId) to a destination once, exactly as a delivery would go out, and report the answer. Works whether it is on or off, and whatever its filter says; nothing is retried or logged.",
|
|
1680
1938
|
{
|
|
1681
1939
|
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
)
|
|
1940
|
+
destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
|
|
1941
|
+
kind: import_zod2.z.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
|
|
1942
|
+
requestId: import_zod2.z.string().optional().describe("With destinationId: a captured request to send")
|
|
1685
1943
|
},
|
|
1686
|
-
withErrorHandling(async ({ slug, kind }) => {
|
|
1944
|
+
withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
|
|
1945
|
+
if (destinationId) {
|
|
1946
|
+
return jsonContent(
|
|
1947
|
+
await client.destinations.test(slug, destinationId, {
|
|
1948
|
+
...kind ? { kind } : {},
|
|
1949
|
+
...requestId ? { requestId } : {}
|
|
1950
|
+
})
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1687
1953
|
return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
|
|
1688
1954
|
})
|
|
1689
1955
|
);
|
|
@@ -1694,17 +1960,25 @@ function registerTools(server, client) {
|
|
|
1694
1960
|
endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's delivery log"),
|
|
1695
1961
|
requestId: import_zod2.z.string().optional().describe("List every delivery and try of this request"),
|
|
1696
1962
|
limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
|
|
1697
|
-
status: import_zod2.z.enum(["all", "pending", "failed"]).default("all").describe(
|
|
1963
|
+
status: import_zod2.z.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
|
|
1964
|
+
"With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
|
|
1965
|
+
),
|
|
1966
|
+
destinationId: import_zod2.z.string().optional().describe("With endpointSlug: only this destination"),
|
|
1698
1967
|
before: import_zod2.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
|
|
1699
1968
|
},
|
|
1700
|
-
withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
|
|
1969
|
+
withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
|
|
1701
1970
|
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1702
1971
|
throw new Error("Pass exactly one of endpointSlug or requestId");
|
|
1703
1972
|
}
|
|
1704
1973
|
if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
|
|
1705
1974
|
const [summary, deliveries] = await Promise.all([
|
|
1706
1975
|
client.forwarding.summary(endpointSlug),
|
|
1707
|
-
client.forwarding.deliveries(endpointSlug, {
|
|
1976
|
+
client.forwarding.deliveries(endpointSlug, {
|
|
1977
|
+
limit,
|
|
1978
|
+
status,
|
|
1979
|
+
before,
|
|
1980
|
+
...destinationId ? { destinationId } : {}
|
|
1981
|
+
})
|
|
1708
1982
|
]);
|
|
1709
1983
|
return jsonContent({
|
|
1710
1984
|
summary,
|
|
@@ -1715,17 +1989,19 @@ function registerTools(server, client) {
|
|
|
1715
1989
|
);
|
|
1716
1990
|
server.tool(
|
|
1717
1991
|
"redeliver",
|
|
1718
|
-
"Forward again with the
|
|
1992
|
+
"Forward again with the current settings: one captured request or email (requestId; to every destination that takes it, or with destinationId to that one whatever its filter says), or every request whose latest delivery failed (endpointSlug, optionally only destinationId, oldest first, up to 1,000 at a time per destination). Owner only.",
|
|
1719
1993
|
{
|
|
1720
1994
|
requestId: import_zod2.z.string().optional().describe("The request or email to send again"),
|
|
1721
|
-
endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again")
|
|
1995
|
+
endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again"),
|
|
1996
|
+
destinationId: import_zod2.z.string().optional().describe("Only this destination")
|
|
1722
1997
|
},
|
|
1723
|
-
withErrorHandling(async ({ requestId, endpointSlug }) => {
|
|
1998
|
+
withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
|
|
1724
1999
|
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1725
2000
|
throw new Error("Pass exactly one of requestId or endpointSlug");
|
|
1726
2001
|
}
|
|
2002
|
+
const options2 = destinationId ? { destinationId } : {};
|
|
1727
2003
|
return jsonContent(
|
|
1728
|
-
requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
|
|
2004
|
+
requestId ? await client.forwarding.redeliver(requestId, options2) : await client.forwarding.redeliverFailed(endpointSlug, options2)
|
|
1729
2005
|
);
|
|
1730
2006
|
})
|
|
1731
2007
|
);
|
|
@@ -1738,6 +2014,7 @@ function registerTools(server, client) {
|
|
|
1738
2014
|
return jsonContent(description);
|
|
1739
2015
|
})
|
|
1740
2016
|
);
|
|
2017
|
+
return handles;
|
|
1741
2018
|
}
|
|
1742
2019
|
|
|
1743
2020
|
// src/sandbox-tools.ts
|
|
@@ -1749,7 +2026,6 @@ var SANDBOX = {
|
|
|
1749
2026
|
};
|
|
1750
2027
|
var DEFAULT_WAIT_SECONDS = 45;
|
|
1751
2028
|
var MAX_WAIT_SECONDS = 600;
|
|
1752
|
-
var TOKEN_REFRESH_MARGIN_MS = 6e4;
|
|
1753
2029
|
var DOCS_URL = "https://webhooks.cc/docs/agents";
|
|
1754
2030
|
var ACCOUNT_ONLY_OPTIONS = [
|
|
1755
2031
|
"mockResponse",
|
|
@@ -1760,20 +2036,29 @@ var ACCOUNT_ONLY_OPTIONS = [
|
|
|
1760
2036
|
"signingHeader"
|
|
1761
2037
|
];
|
|
1762
2038
|
var AgentSession = class {
|
|
1763
|
-
constructor(baseUrl, webhookUrl, clientName) {
|
|
2039
|
+
constructor(baseUrl, webhookUrl, clientName, credentialsFile) {
|
|
1764
2040
|
this.baseUrl = baseUrl;
|
|
1765
2041
|
this.webhookUrl = webhookUrl;
|
|
1766
2042
|
this.clientName = clientName;
|
|
2043
|
+
this.credentialsFile = credentialsFile;
|
|
1767
2044
|
this.sandbox = null;
|
|
1768
|
-
this.
|
|
1769
|
-
|
|
2045
|
+
this.connect = null;
|
|
2046
|
+
}
|
|
2047
|
+
sandboxOptions() {
|
|
2048
|
+
return {
|
|
2049
|
+
baseUrl: this.baseUrl,
|
|
2050
|
+
webhookUrl: this.webhookUrl,
|
|
2051
|
+
clientName: this.clientName,
|
|
2052
|
+
credentialsFile: this.credentialsFile
|
|
2053
|
+
};
|
|
1770
2054
|
}
|
|
1771
2055
|
/**
|
|
1772
|
-
* The sandbox
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
2056
|
+
* The sandbox: the one saved by an earlier session when it is still live,
|
|
2057
|
+
* else a new registration (and again after it expired). Tool calls can run
|
|
2058
|
+
* in parallel: this.sandbox is read again after every await, and a new
|
|
2059
|
+
* sandbox is assigned with no await in between, so parallel calls share
|
|
2060
|
+
* one registration. Calls waiting on a registration that fails get its
|
|
2061
|
+
* error (a 429 asks them to wait); the next call starts again.
|
|
1777
2062
|
*/
|
|
1778
2063
|
async sandboxClient() {
|
|
1779
2064
|
while (this.sandbox) {
|
|
@@ -1781,35 +2066,36 @@ var AgentSession = class {
|
|
|
1781
2066
|
const sandbox = await pending;
|
|
1782
2067
|
if (!this.expired(pending, sandbox)) return sandbox;
|
|
1783
2068
|
}
|
|
1784
|
-
const created = import_sdk2.WebhooksCC.sandbox(
|
|
1785
|
-
baseUrl: this.baseUrl,
|
|
1786
|
-
webhookUrl: this.webhookUrl,
|
|
1787
|
-
clientName: this.clientName
|
|
1788
|
-
});
|
|
2069
|
+
const created = import_sdk2.WebhooksCC.sandbox(this.sandboxOptions());
|
|
1789
2070
|
this.sandbox = created;
|
|
1790
2071
|
created.catch(() => {
|
|
1791
2072
|
if (this.sandbox === created) this.sandbox = null;
|
|
1792
2073
|
});
|
|
1793
2074
|
return created;
|
|
1794
2075
|
}
|
|
1795
|
-
/** The live sandbox,
|
|
2076
|
+
/** The live sandbox, from this session or the credential file; never registers one. */
|
|
1796
2077
|
async existingSandbox() {
|
|
1797
2078
|
const pending = this.sandbox;
|
|
1798
|
-
if (
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2079
|
+
if (pending) {
|
|
2080
|
+
let sandbox;
|
|
2081
|
+
try {
|
|
2082
|
+
sandbox = await pending;
|
|
2083
|
+
} catch {
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
return this.expired(pending, sandbox) ? null : sandbox;
|
|
1804
2087
|
}
|
|
1805
|
-
|
|
2088
|
+
const resumed = await import_sdk2.SandboxClient.resume(this.sandboxOptions()).catch(() => null);
|
|
2089
|
+
if (!resumed) return null;
|
|
2090
|
+
if (!this.sandbox) this.sandbox = Promise.resolve(resumed);
|
|
2091
|
+
return this.sandbox;
|
|
1806
2092
|
}
|
|
1807
2093
|
/** True when the sandbox is past its 24 hours; then it is dropped. */
|
|
1808
2094
|
expired(pending, sandbox) {
|
|
1809
2095
|
if (sandbox.expiresAt.getTime() > Date.now()) return false;
|
|
1810
2096
|
if (this.sandbox === pending) this.sandbox = null;
|
|
1811
|
-
if (this.
|
|
1812
|
-
this.
|
|
2097
|
+
if (this.connect?.via === "sandbox" && this.connect.sandbox === sandbox) {
|
|
2098
|
+
this.connect = null;
|
|
1813
2099
|
}
|
|
1814
2100
|
return true;
|
|
1815
2101
|
}
|
|
@@ -1820,26 +2106,11 @@ var AgentSession = class {
|
|
|
1820
2106
|
}
|
|
1821
2107
|
return sandbox;
|
|
1822
2108
|
}
|
|
1823
|
-
|
|
2109
|
+
reset() {
|
|
1824
2110
|
this.sandbox = null;
|
|
2111
|
+
this.connect = null;
|
|
1825
2112
|
}
|
|
1826
2113
|
};
|
|
1827
|
-
function claimedTokens(baseUrl, assertion, first) {
|
|
1828
|
-
let current = first;
|
|
1829
|
-
let pending = null;
|
|
1830
|
-
return async (options) => {
|
|
1831
|
-
if (!options?.forceRefresh && current.expiresAt.getTime() - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
|
|
1832
|
-
return current.accessToken;
|
|
1833
|
-
}
|
|
1834
|
-
if (!pending) {
|
|
1835
|
-
pending = import_sdk2.WebhooksCC.agent.exchange({ baseUrl, assertion }).finally(() => {
|
|
1836
|
-
pending = null;
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
current = await pending;
|
|
1840
|
-
return current.accessToken;
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
2114
|
function minutesLeft(until) {
|
|
1844
2115
|
return Math.max(1, Math.round((until.getTime() - Date.now()) / 6e4));
|
|
1845
2116
|
}
|
|
@@ -1847,16 +2118,53 @@ function onlyKind(requests, kind) {
|
|
|
1847
2118
|
if (!kind) return requests;
|
|
1848
2119
|
return requests.filter((request) => (request.kind ?? "http") === kind);
|
|
1849
2120
|
}
|
|
2121
|
+
function savedTo(credentialsFile) {
|
|
2122
|
+
return import_sdk2.AgentCredentialStore.resolve(credentialsFile)?.path ?? null;
|
|
2123
|
+
}
|
|
1850
2124
|
function registerSandboxTools(server, options = {}) {
|
|
1851
2125
|
const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL ?? "https://webhooks.cc";
|
|
2126
|
+
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
|
1852
2127
|
const session = new AgentSession(
|
|
1853
2128
|
baseUrl,
|
|
1854
|
-
|
|
1855
|
-
options.clientName ?? "webhooks-cc-mcp"
|
|
2129
|
+
webhookUrl,
|
|
2130
|
+
options.clientName ?? "webhooks-cc-mcp",
|
|
2131
|
+
options.credentialsFile
|
|
1856
2132
|
);
|
|
1857
|
-
|
|
2133
|
+
let handles = [];
|
|
2134
|
+
let shown = 0;
|
|
2135
|
+
const swap = (register) => {
|
|
2136
|
+
const id = ++shown;
|
|
2137
|
+
for (const handle of handles) handle.remove();
|
|
2138
|
+
handles = register(id);
|
|
2139
|
+
};
|
|
2140
|
+
const showSandbox = () => swap(() => sandboxToolSet(server, session, showAccount));
|
|
2141
|
+
const savedAccount = (id) => options.credentialsFile === false ? null : import_sdk2.WebhooksCC.fromSavedConnection({
|
|
2142
|
+
baseUrl,
|
|
2143
|
+
webhookUrl,
|
|
2144
|
+
credentialsFile: options.credentialsFile,
|
|
2145
|
+
// The human disconnected the agent: back to the sandbox, unless a
|
|
2146
|
+
// newer connection is already on display.
|
|
2147
|
+
onRevoked: () => queueMicrotask(() => {
|
|
2148
|
+
if (shown === id) showSandbox();
|
|
2149
|
+
})
|
|
2150
|
+
});
|
|
2151
|
+
function showAccount(client) {
|
|
2152
|
+
swap((id) => registerTools(server, savedAccount(id) ?? client));
|
|
2153
|
+
options.onConnected?.(client);
|
|
2154
|
+
}
|
|
2155
|
+
const saved = savedAccount(1);
|
|
2156
|
+
if (saved) {
|
|
2157
|
+
swap(() => registerTools(server, saved));
|
|
2158
|
+
options.onConnected?.(saved);
|
|
2159
|
+
} else {
|
|
2160
|
+
showSandbox();
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function sandboxToolSet(server, session, showAccount) {
|
|
2164
|
+
const tools = [];
|
|
1858
2165
|
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`;
|
|
1859
|
-
|
|
2166
|
+
const file = savedTo(session.credentialsFile);
|
|
2167
|
+
tools.push(
|
|
1860
2168
|
server.tool(
|
|
1861
2169
|
"create_endpoint",
|
|
1862
2170
|
`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.`,
|
|
@@ -1880,7 +2188,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1880
2188
|
})
|
|
1881
2189
|
)
|
|
1882
2190
|
);
|
|
1883
|
-
|
|
2191
|
+
tools.push(
|
|
1884
2192
|
server.tool(
|
|
1885
2193
|
"list_endpoints",
|
|
1886
2194
|
"List this agent's sandbox endpoints. Empty until create_endpoint starts the sandbox.",
|
|
@@ -1894,7 +2202,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1894
2202
|
})
|
|
1895
2203
|
)
|
|
1896
2204
|
);
|
|
1897
|
-
|
|
2205
|
+
tools.push(
|
|
1898
2206
|
server.tool(
|
|
1899
2207
|
"get_endpoint",
|
|
1900
2208
|
"Get one of this agent's sandbox endpoints by slug, with its sandbox limits and usage.",
|
|
@@ -1904,7 +2212,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1904
2212
|
})
|
|
1905
2213
|
)
|
|
1906
2214
|
);
|
|
1907
|
-
|
|
2215
|
+
tools.push(
|
|
1908
2216
|
server.tool(
|
|
1909
2217
|
"delete_endpoint",
|
|
1910
2218
|
"Delete one of this agent's sandbox endpoints and its captured requests. Frees a slot; the sandbox's request budget is not refunded.",
|
|
@@ -1915,7 +2223,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1915
2223
|
})
|
|
1916
2224
|
)
|
|
1917
2225
|
);
|
|
1918
|
-
|
|
2226
|
+
tools.push(
|
|
1919
2227
|
server.tool(
|
|
1920
2228
|
"list_requests",
|
|
1921
2229
|
"List recent requests captured by a sandbox endpoint, newest first.",
|
|
@@ -1927,7 +2235,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1927
2235
|
})
|
|
1928
2236
|
)
|
|
1929
2237
|
);
|
|
1930
|
-
|
|
2238
|
+
tools.push(
|
|
1931
2239
|
server.tool(
|
|
1932
2240
|
"get_request",
|
|
1933
2241
|
"Get full details for a request captured by a sandbox endpoint.",
|
|
@@ -1938,7 +2246,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1938
2246
|
})
|
|
1939
2247
|
)
|
|
1940
2248
|
);
|
|
1941
|
-
|
|
2249
|
+
tools.push(
|
|
1942
2250
|
server.tool(
|
|
1943
2251
|
"wait_for_request",
|
|
1944
2252
|
"Wait for a request to arrive at a sandbox endpoint.",
|
|
@@ -1950,7 +2258,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1950
2258
|
})
|
|
1951
2259
|
)
|
|
1952
2260
|
);
|
|
1953
|
-
|
|
2261
|
+
tools.push(
|
|
1954
2262
|
server.tool(
|
|
1955
2263
|
"generate_payload_schema",
|
|
1956
2264
|
"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.",
|
|
@@ -1962,62 +2270,59 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1962
2270
|
})
|
|
1963
2271
|
)
|
|
1964
2272
|
);
|
|
1965
|
-
|
|
2273
|
+
tools.push(
|
|
1966
2274
|
server.tool(
|
|
1967
2275
|
"connect_account",
|
|
1968
2276
|
"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.",
|
|
1969
2277
|
{
|
|
1970
|
-
email: import_zod3.z.string().email({ message: "Invalid email address" }).describe(
|
|
2278
|
+
email: import_zod3.z.string().email({ message: "Invalid email address" }).describe(
|
|
2279
|
+
"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"
|
|
2280
|
+
)
|
|
1971
2281
|
},
|
|
1972
2282
|
withErrorHandling(async ({ email }) => {
|
|
1973
|
-
if (session.connected) {
|
|
1974
|
-
return jsonContent({ connected: true, message: "Already connected to an account." });
|
|
1975
|
-
}
|
|
1976
2283
|
const normalized = email.trim().toLowerCase();
|
|
2284
|
+
const previous = session.connect;
|
|
1977
2285
|
const sandbox = await session.existingSandbox();
|
|
1978
|
-
let
|
|
2286
|
+
let attempt;
|
|
2287
|
+
let movesEndpoints = false;
|
|
1979
2288
|
if (sandbox) {
|
|
1980
|
-
claim = {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
attempt: await sandbox.claim({ email: normalized }),
|
|
1985
|
-
claimExpires: sandbox.expiresAt
|
|
1986
|
-
};
|
|
1987
|
-
} else if (session.claim?.kind === "service_auth" && session.claim.email === normalized && session.claim.claimExpires.getTime() > Date.now()) {
|
|
1988
|
-
const attempt = await import_sdk2.WebhooksCC.agent.startClaim({
|
|
1989
|
-
baseUrl,
|
|
1990
|
-
claimToken: session.claim.claimToken
|
|
1991
|
-
});
|
|
1992
|
-
claim = { ...session.claim, attempt };
|
|
2289
|
+
const claim = await sandbox.claim({ email: normalized });
|
|
2290
|
+
session.connect = { via: "sandbox", sandbox, email: normalized, attempt: claim };
|
|
2291
|
+
attempt = claim;
|
|
2292
|
+
movesEndpoints = claim.movesEndpoints;
|
|
1993
2293
|
} else {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2294
|
+
let pending = null;
|
|
2295
|
+
if (previous?.via === "connect" && previous.email === normalized) {
|
|
2296
|
+
pending = await previous.pending.restart().catch((error) => {
|
|
2297
|
+
if (error instanceof import_sdk2.AgentAuthError) return null;
|
|
2298
|
+
throw error;
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
pending ?? (pending = await import_sdk2.WebhooksCC.connect({
|
|
2302
|
+
baseUrl: session.baseUrl,
|
|
2303
|
+
webhookUrl: session.webhookUrl,
|
|
2002
2304
|
email: normalized,
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
};
|
|
2305
|
+
clientName: session.clientName,
|
|
2306
|
+
credentialsFile: session.credentialsFile
|
|
2307
|
+
}));
|
|
2308
|
+
session.connect = { via: "connect", pending, email: normalized };
|
|
2309
|
+
attempt = pending;
|
|
2006
2310
|
}
|
|
2007
|
-
|
|
2008
|
-
const { verificationUri, userCode, expiresAt } = claim.attempt;
|
|
2311
|
+
const { verificationUri, userCode, expiresAt } = attempt;
|
|
2009
2312
|
return jsonContent({
|
|
2010
2313
|
verificationUri,
|
|
2011
2314
|
userCode,
|
|
2012
2315
|
expiresAt: expiresAt.toISOString(),
|
|
2013
2316
|
tellTheHuman: `Open ${verificationUri}, sign in or sign up as ${normalized}, and enter the code ${userCode}.`,
|
|
2014
2317
|
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.`,
|
|
2015
|
-
...
|
|
2318
|
+
...sandbox ? {
|
|
2319
|
+
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."
|
|
2320
|
+
} : {}
|
|
2016
2321
|
});
|
|
2017
2322
|
})
|
|
2018
2323
|
)
|
|
2019
2324
|
);
|
|
2020
|
-
|
|
2325
|
+
tools.push(
|
|
2021
2326
|
server.tool(
|
|
2022
2327
|
"wait_for_connection",
|
|
2023
2328
|
`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.`,
|
|
@@ -2025,28 +2330,22 @@ function registerSandboxTools(server, options = {}) {
|
|
|
2025
2330
|
timeoutSeconds: import_zod3.z.number().int().min(1).max(MAX_WAIT_SECONDS).optional().describe(`How long to wait, in seconds (default ${DEFAULT_WAIT_SECONDS})`)
|
|
2026
2331
|
},
|
|
2027
2332
|
withErrorHandling(async ({ timeoutSeconds }) => {
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
}
|
|
2031
|
-
const claim = session.claim;
|
|
2032
|
-
if (!claim) {
|
|
2333
|
+
const pending = session.connect;
|
|
2334
|
+
if (!pending) {
|
|
2033
2335
|
throw new Error("Nothing to wait for: call connect_account first.");
|
|
2034
2336
|
}
|
|
2035
|
-
|
|
2337
|
+
const timeout = (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
|
|
2338
|
+
const expiresAt = pending.via === "sandbox" ? pending.attempt.expiresAt : pending.pending.expiresAt;
|
|
2339
|
+
let client;
|
|
2036
2340
|
try {
|
|
2037
|
-
|
|
2038
|
-
baseUrl,
|
|
2039
|
-
claimToken: claim.claimToken,
|
|
2040
|
-
interval: claim.attempt.interval,
|
|
2041
|
-
timeout: (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3
|
|
2042
|
-
});
|
|
2341
|
+
client = pending.via === "sandbox" ? await pending.sandbox.waitForClaim({ timeout }) : await pending.pending.wait({ timeout });
|
|
2043
2342
|
} catch (error) {
|
|
2044
2343
|
if (!(error instanceof import_sdk2.AgentAuthError)) throw error;
|
|
2045
2344
|
if (error.code === "timeout") {
|
|
2046
2345
|
return jsonContent({
|
|
2047
2346
|
connected: false,
|
|
2048
2347
|
status: "waiting",
|
|
2049
|
-
message: `Not connected yet. The code works until ${
|
|
2348
|
+
message: `Not connected yet. The code works until ${expiresAt.toISOString()}; call wait_for_connection again.`
|
|
2050
2349
|
});
|
|
2051
2350
|
}
|
|
2052
2351
|
if (error.code === "expired_token") {
|
|
@@ -2065,34 +2364,25 @@ function registerSandboxTools(server, options = {}) {
|
|
|
2065
2364
|
}
|
|
2066
2365
|
throw error;
|
|
2067
2366
|
}
|
|
2068
|
-
const
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
getAccessToken: claimedTokens(baseUrl, claimed.identityAssertion, claimed.token)
|
|
2072
|
-
});
|
|
2073
|
-
session.connected = true;
|
|
2074
|
-
session.claim = null;
|
|
2075
|
-
session.forgetSandbox();
|
|
2076
|
-
for (const tool of registered) tool.remove();
|
|
2077
|
-
registerTools(server, client);
|
|
2078
|
-
options.onConnected?.(client);
|
|
2367
|
+
const movedEndpoints = pending.via === "sandbox" && pending.attempt.movesEndpoints;
|
|
2368
|
+
session.reset();
|
|
2369
|
+
showAccount(client);
|
|
2079
2370
|
return jsonContent({
|
|
2080
2371
|
connected: true,
|
|
2081
|
-
email:
|
|
2082
|
-
|
|
2083
|
-
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.`
|
|
2372
|
+
email: pending.email,
|
|
2373
|
+
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."}`
|
|
2084
2374
|
});
|
|
2085
2375
|
})
|
|
2086
2376
|
)
|
|
2087
2377
|
);
|
|
2088
|
-
|
|
2378
|
+
tools.push(
|
|
2089
2379
|
server.tool(
|
|
2090
2380
|
"about_sandbox",
|
|
2091
2381
|
"Explain what this agent can do without a webhooks.cc account, the sandbox limits, and how to connect an account. No network call.",
|
|
2092
2382
|
{},
|
|
2093
2383
|
withErrorHandling(async () => {
|
|
2094
2384
|
return jsonContent({
|
|
2095
|
-
mode: "sandbox (no WHK_API_KEY)",
|
|
2385
|
+
mode: "sandbox (no WHK_API_KEY, no saved connection)",
|
|
2096
2386
|
sandbox: {
|
|
2097
2387
|
maxEndpoints: SANDBOX.maxEndpoints,
|
|
2098
2388
|
requestsPerEndpoint: SANDBOX.requestsPerEndpoint,
|
|
@@ -2103,21 +2393,23 @@ function registerSandboxTools(server, options = {}) {
|
|
|
2103
2393
|
notAvailable: "Mock responses, response rules, notifications, signature verification, email capture, forwarding, teams and sending webhooks."
|
|
2104
2394
|
},
|
|
2105
2395
|
connectAnAccount: [
|
|
2106
|
-
"Call connect_account with the human
|
|
2396
|
+
"Call connect_account with the email the human signs in to webhooks.cc with, and show them the link and code it returns.",
|
|
2107
2397
|
"Call wait_for_connection until it reports connected; the full tools for their account then replace the sandbox tools.",
|
|
2108
|
-
"Sandbox endpoints move into the account with their captured requests."
|
|
2398
|
+
"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)."
|
|
2109
2399
|
],
|
|
2400
|
+
savedIn: file ? `${file} (shared with the whk CLI and the SDKs): the sandbox and the connection survive restarts.` : "nowhere: this session only.",
|
|
2110
2401
|
withAnApiKey: "Set WHK_API_KEY to a whcc_ key from the dashboard and restart for the full tools from the start.",
|
|
2111
2402
|
docs: DOCS_URL,
|
|
2112
|
-
protocol: `${baseUrl}/auth.md`
|
|
2403
|
+
protocol: `${session.baseUrl}/auth.md`
|
|
2113
2404
|
});
|
|
2114
2405
|
})
|
|
2115
2406
|
)
|
|
2116
2407
|
);
|
|
2408
|
+
return tools;
|
|
2117
2409
|
}
|
|
2118
2410
|
|
|
2119
2411
|
// src/index.ts
|
|
2120
|
-
var VERSION = true ? "1.
|
|
2412
|
+
var VERSION = true ? "1.15.0" : "0.0.0-dev";
|
|
2121
2413
|
function createServer(options = {}) {
|
|
2122
2414
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2123
2415
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
|
@@ -2128,7 +2420,7 @@ function createServer(options = {}) {
|
|
|
2128
2420
|
{ debouncedNotificationMethods: ["notifications/tools/list_changed"] }
|
|
2129
2421
|
);
|
|
2130
2422
|
if (!apiKey) {
|
|
2131
|
-
registerSandboxTools(server, { baseUrl, webhookUrl });
|
|
2423
|
+
registerSandboxTools(server, { baseUrl, webhookUrl, credentialsFile: options.credentialsFile });
|
|
2132
2424
|
registerPrompts(server);
|
|
2133
2425
|
return server;
|
|
2134
2426
|
}
|
|
@@ -2157,13 +2449,16 @@ function mcpServerConfig(apiKey) {
|
|
|
2157
2449
|
return {
|
|
2158
2450
|
command: "npx",
|
|
2159
2451
|
args: ["-y", "@webhooks-cc/mcp"],
|
|
2160
|
-
env: { WHK_API_KEY: apiKey }
|
|
2452
|
+
...apiKey ? { env: { WHK_API_KEY: apiKey } } : {}
|
|
2161
2453
|
};
|
|
2162
2454
|
}
|
|
2455
|
+
function vscodeServerConfig(apiKey) {
|
|
2456
|
+
return { type: "stdio", ...mcpServerConfig(apiKey) };
|
|
2457
|
+
}
|
|
2163
2458
|
function resolveApiKey(flags) {
|
|
2164
2459
|
return flags["api-key"] ?? process.env.WHK_API_KEY ?? null;
|
|
2165
2460
|
}
|
|
2166
|
-
function mergeJsonConfig(filePath, serverName, serverConfig) {
|
|
2461
|
+
function mergeJsonConfig(filePath, serverName, serverConfig, key = "mcpServers") {
|
|
2167
2462
|
let existing = {};
|
|
2168
2463
|
if ((0, import_fs.existsSync)(filePath)) {
|
|
2169
2464
|
try {
|
|
@@ -2174,9 +2469,9 @@ function mergeJsonConfig(filePath, serverName, serverConfig) {
|
|
|
2174
2469
|
(0, import_fs.copyFileSync)(filePath, `${filePath}.bak`);
|
|
2175
2470
|
}
|
|
2176
2471
|
}
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
existing
|
|
2472
|
+
const servers = existing[key] ?? {};
|
|
2473
|
+
servers[serverName] = serverConfig;
|
|
2474
|
+
existing[key] = servers;
|
|
2180
2475
|
const dir = (0, import_path.dirname)(filePath);
|
|
2181
2476
|
if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
2182
2477
|
(0, import_fs.writeFileSync)(filePath, JSON.stringify(existing, null, 2) + "\n");
|
|
@@ -2213,8 +2508,7 @@ function setupClaudeCode(apiKey) {
|
|
|
2213
2508
|
"--transport",
|
|
2214
2509
|
"stdio",
|
|
2215
2510
|
"webhooks-cc",
|
|
2216
|
-
"-e",
|
|
2217
|
-
`WHK_API_KEY=${apiKey}`,
|
|
2511
|
+
...apiKey ? ["-e", `WHK_API_KEY=${apiKey}`] : [],
|
|
2218
2512
|
"--",
|
|
2219
2513
|
"npx",
|
|
2220
2514
|
"-y",
|
|
@@ -2229,7 +2523,7 @@ function setupClaudeCode(apiKey) {
|
|
|
2229
2523
|
console.error("Install it: npm install -g @anthropic-ai/claude-code");
|
|
2230
2524
|
console.error("\nManual alternative \u2014 run this command:");
|
|
2231
2525
|
console.error(
|
|
2232
|
-
` claude mcp add -s user --transport stdio webhooks-cc -e WHK_API_KEY=<your-key> -- npx -y @webhooks-cc/mcp`
|
|
2526
|
+
` claude mcp add -s user --transport stdio webhooks-cc${apiKey ? " -e WHK_API_KEY=<your-key>" : ""} -- npx -y @webhooks-cc/mcp`
|
|
2233
2527
|
);
|
|
2234
2528
|
process.exit(1);
|
|
2235
2529
|
}
|
|
@@ -2248,7 +2542,7 @@ function setupCursor(apiKey) {
|
|
|
2248
2542
|
}
|
|
2249
2543
|
function setupVSCode(apiKey) {
|
|
2250
2544
|
const configPath = (0, import_path.join)(process.cwd(), ".vscode", "mcp.json");
|
|
2251
|
-
mergeJsonConfig(configPath, "webhooks-cc",
|
|
2545
|
+
mergeJsonConfig(configPath, "webhooks-cc", vscodeServerConfig(apiKey), "servers");
|
|
2252
2546
|
console.log(`Done! Config written to ${configPath}`);
|
|
2253
2547
|
console.log("Reload VS Code window for changes to take effect.");
|
|
2254
2548
|
}
|
|
@@ -2260,8 +2554,7 @@ function setupCodex(apiKey) {
|
|
|
2260
2554
|
"mcp",
|
|
2261
2555
|
"add",
|
|
2262
2556
|
"webhooks-cc",
|
|
2263
|
-
"-e",
|
|
2264
|
-
`WHK_API_KEY=${apiKey}`,
|
|
2557
|
+
...apiKey ? ["-e", `WHK_API_KEY=${apiKey}`] : [],
|
|
2265
2558
|
"--",
|
|
2266
2559
|
"npx",
|
|
2267
2560
|
"-y",
|
|
@@ -2274,13 +2567,13 @@ function setupCodex(apiKey) {
|
|
|
2274
2567
|
console.error("\nFailed to run 'codex mcp add'. Is OpenAI Codex CLI installed?");
|
|
2275
2568
|
console.error("\nManual alternative \u2014 add to ~/.codex/config.toml:");
|
|
2276
2569
|
console.error(`
|
|
2277
|
-
[
|
|
2570
|
+
[mcp_servers.webhooks-cc]
|
|
2278
2571
|
command = "npx"
|
|
2279
2572
|
args = ["-y", "@webhooks-cc/mcp"]
|
|
2280
|
-
|
|
2281
|
-
[
|
|
2573
|
+
${apiKey ? `
|
|
2574
|
+
[mcp_servers.webhooks-cc.env]
|
|
2282
2575
|
WHK_API_KEY = "<your-key>"
|
|
2283
|
-
`);
|
|
2576
|
+
` : ""}`);
|
|
2284
2577
|
process.exit(1);
|
|
2285
2578
|
}
|
|
2286
2579
|
}
|
|
@@ -2297,6 +2590,9 @@ function printUsage() {
|
|
|
2297
2590
|
Usage:
|
|
2298
2591
|
npx @webhooks-cc/mcp setup <tool> [--api-key <key>]
|
|
2299
2592
|
|
|
2593
|
+
Without a key the server starts in the keyless agent sandbox; your agent can
|
|
2594
|
+
connect your account later (connect_account), and the connection is saved.
|
|
2595
|
+
|
|
2300
2596
|
Tools:
|
|
2301
2597
|
claude-code Claude Code CLI (runs 'claude mcp add')
|
|
2302
2598
|
claude-desktop Claude Desktop app
|
|
@@ -2306,9 +2602,10 @@ Tools:
|
|
|
2306
2602
|
windsurf Windsurf editor
|
|
2307
2603
|
|
|
2308
2604
|
Options:
|
|
2309
|
-
--api-key <key> Your webhooks.cc API key (or set WHK_API_KEY env var)
|
|
2605
|
+
--api-key <key> Your webhooks.cc API key (or set WHK_API_KEY env var); optional
|
|
2310
2606
|
|
|
2311
2607
|
Examples:
|
|
2608
|
+
npx @webhooks-cc/mcp setup claude-code
|
|
2312
2609
|
npx @webhooks-cc/mcp setup claude-code --api-key whcc_abc123
|
|
2313
2610
|
WHK_API_KEY=whcc_abc123 npx @webhooks-cc/mcp setup cursor
|
|
2314
2611
|
npx @webhooks-cc/mcp setup vscode --api-key whcc_abc123
|
|
@@ -2336,14 +2633,13 @@ function runSetup(args2) {
|
|
|
2336
2633
|
process.exit(tool ? 1 : 0);
|
|
2337
2634
|
}
|
|
2338
2635
|
const apiKey = resolveApiKey(flags);
|
|
2339
|
-
if (
|
|
2340
|
-
console.error("Error: API key required. Pass --api-key or set WHK_API_KEY env var.");
|
|
2341
|
-
console.error("Get your API key at https://webhooks.cc/account/api-keys\n");
|
|
2342
|
-
process.exit(1);
|
|
2343
|
-
}
|
|
2344
|
-
validateApiKeyFormat(apiKey);
|
|
2636
|
+
if (apiKey) validateApiKeyFormat(apiKey);
|
|
2345
2637
|
console.log(`Setting up webhooks-cc MCP server for ${tool}...
|
|
2346
2638
|
`);
|
|
2639
|
+
if (!apiKey) {
|
|
2640
|
+
console.log("No API key: the server starts in the keyless agent sandbox. Your agent can");
|
|
2641
|
+
console.log("connect your account later with connect_account; the connection is saved.\n");
|
|
2642
|
+
}
|
|
2347
2643
|
switch (tool) {
|
|
2348
2644
|
case "claude-code":
|
|
2349
2645
|
setupClaudeCode(apiKey);
|
|
@@ -2381,17 +2677,25 @@ Usage:
|
|
|
2381
2677
|
npx @webhooks-cc/mcp --help Show this help
|
|
2382
2678
|
|
|
2383
2679
|
Environment:
|
|
2384
|
-
WHK_API_KEY Your webhooks.cc API key (
|
|
2680
|
+
WHK_API_KEY Your webhooks.cc API key (optional; without it the server
|
|
2681
|
+
starts connected to the account your agent was connected
|
|
2682
|
+
to before, or in the keyless agent sandbox)
|
|
2683
|
+
WHK_AGENT_FILE Where the sandbox and the connection are saved (default:
|
|
2684
|
+
~/.config/whk/agent.json, shared with the whk CLI and SDKs)
|
|
2385
2685
|
WHK_WEBHOOK_URL Custom webhook receiver URL (optional)
|
|
2386
2686
|
WHK_BASE_URL Custom API base URL (optional)
|
|
2387
2687
|
|
|
2388
2688
|
Setup:
|
|
2389
|
-
npx @webhooks-cc/mcp setup claude-code
|
|
2390
|
-
npx @webhooks-cc/mcp setup cursor
|
|
2391
|
-
npx @webhooks-cc/mcp setup vscode
|
|
2392
|
-
npx @webhooks-cc/mcp setup codex
|
|
2393
|
-
npx @webhooks-cc/mcp setup windsurf
|
|
2394
|
-
npx @webhooks-cc/mcp setup claude-desktop
|
|
2689
|
+
npx @webhooks-cc/mcp setup claude-code
|
|
2690
|
+
npx @webhooks-cc/mcp setup cursor
|
|
2691
|
+
npx @webhooks-cc/mcp setup vscode
|
|
2692
|
+
npx @webhooks-cc/mcp setup codex
|
|
2693
|
+
npx @webhooks-cc/mcp setup windsurf
|
|
2694
|
+
npx @webhooks-cc/mcp setup claude-desktop
|
|
2695
|
+
(add --api-key whcc_... to start with your account's tools)
|
|
2696
|
+
|
|
2697
|
+
Hosted agents (claude.ai, ChatGPT): add https://webhooks.cc/mcp as a remote
|
|
2698
|
+
MCP server instead; you approve it once in your browser.
|
|
2395
2699
|
|
|
2396
2700
|
Get your API key at https://webhooks.cc/account/api-keys
|
|
2397
2701
|
`);
|