@webhooks-cc/mcp 1.12.1 → 1.14.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 +27 -10
- package/dist/bin/mcp.js +337 -44
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +335 -42
- package/dist/index.mjs +342 -44
- package/package.json +9 -8
package/dist/index.mjs
CHANGED
|
@@ -70,6 +70,7 @@ function registerPrompts(server) {
|
|
|
70
70
|
"If a target URL is provided, use preview_webhook before send_to so the request shape is visible.",
|
|
71
71
|
targetUrl ? `Target URL: ${targetUrl}.` : null,
|
|
72
72
|
"Send a realistic provider webhook, wait for capture, and verify the signature if a secret is available.",
|
|
73
|
+
"Once real deliveries are captured, use generate_payload_schema to write the payload types for the handler.",
|
|
73
74
|
"Return the endpoint URL, the exact tools used, and the next step for the developer."
|
|
74
75
|
].filter(Boolean).join(" ")
|
|
75
76
|
}
|
|
@@ -406,7 +407,6 @@ import {
|
|
|
406
407
|
MAX_CONDITION_NAME_LEN,
|
|
407
408
|
MAX_CONDITION_PATH_LEN,
|
|
408
409
|
MAX_RULE_NAME_LEN,
|
|
409
|
-
MAX_GLOB_PATTERN_LEN,
|
|
410
410
|
NotFoundError,
|
|
411
411
|
RateLimitError,
|
|
412
412
|
TEMPLATE_METADATA,
|
|
@@ -417,7 +417,13 @@ import {
|
|
|
417
417
|
verifySignature,
|
|
418
418
|
WebhooksCCError,
|
|
419
419
|
extractCode,
|
|
420
|
-
extractLink
|
|
420
|
+
extractLink,
|
|
421
|
+
detectWebhookInfo,
|
|
422
|
+
generateRequestSchema,
|
|
423
|
+
PAYLOAD_SCHEMA_FORMATS,
|
|
424
|
+
CONDITION_FIELDS,
|
|
425
|
+
CONDITION_OPS,
|
|
426
|
+
conditionIssues
|
|
421
427
|
} from "@webhooks-cc/sdk";
|
|
422
428
|
var MAX_BODY_SIZE = MAX_OUTPUT;
|
|
423
429
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
@@ -437,28 +443,75 @@ var methodSchema = z2.enum(HTTP_METHODS).default("POST").describe("HTTP method (
|
|
|
437
443
|
var kindSchema = z2.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
|
|
438
444
|
var EMAIL_SCAN_LIMIT = 100;
|
|
439
445
|
var durationOrTimestampSchema = z2.union([z2.string(), z2.number()]);
|
|
440
|
-
var
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
446
|
+
var CONDITION_FIELD_NAMES = Object.keys(CONDITION_FIELDS);
|
|
447
|
+
var CONDITION_OP_NAMES = CONDITION_OPS;
|
|
448
|
+
function conditionSchema(use) {
|
|
449
|
+
const fields = use === "response" ? CONDITION_FIELD_NAMES.filter((field) => CONDITION_FIELDS[field].response) : CONDITION_FIELD_NAMES;
|
|
450
|
+
return z2.object({
|
|
451
|
+
field: z2.enum(fields).describe(
|
|
452
|
+
use === "response" ? "What to read: method, path, header (name), query (name), body (text), body_path (path into a JSON or form body), content_type, size" : "What to read: method, path, header (name), query (name), body, body_path (path), content_type, size, kind (http or email), email (name: subject, from, to, text or tag), spam_score, email_auth (name: spf, dkim or dmarc), signature (verified, failed or missing)"
|
|
453
|
+
),
|
|
454
|
+
op: z2.enum(CONDITION_OP_NAMES).describe(
|
|
455
|
+
"eq, neq, contains, not_contains, starts_with, ends_with, matches (glob: * one path segment, ** any, ? one character), regex (RE2, searched anywhere; (?i) ignores case), exists, not_exists, gt, gte, lt, lte (numbers)"
|
|
456
|
+
),
|
|
457
|
+
value: z2.string().max(MAX_CONDITION_VALUE_LEN).optional().describe(`What to compare with (not for exists and not_exists)`),
|
|
458
|
+
name: z2.string().max(MAX_CONDITION_NAME_LEN).optional().describe("Header or query parameter name; the email part; spf, dkim or dmarc"),
|
|
459
|
+
path: z2.string().max(MAX_CONDITION_PATH_LEN).optional().describe('Dot path for body_path, e.g. data.object.status or items.0.sku or a["odd.key"]')
|
|
460
|
+
}).superRefine((condition, ctx) => {
|
|
461
|
+
for (const issue of conditionIssues(condition, { use })) {
|
|
462
|
+
ctx.addIssue({ code: "custom", message: issue });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
var ruleConditionSchema = conditionSchema("response");
|
|
467
|
+
var destinationFields = {
|
|
468
|
+
name: z2.string().max(100).nullable().optional().describe("A name for the list and the log"),
|
|
469
|
+
enabled: z2.boolean().optional().describe("Send to it (default true)"),
|
|
470
|
+
http: z2.boolean().optional().describe("Takes HTTP requests (default true)"),
|
|
471
|
+
email: z2.boolean().optional().describe("Takes emails (default true)"),
|
|
472
|
+
filter: z2.object({
|
|
473
|
+
logic: z2.enum(["and", "or"]).optional().describe("and (default): every condition; or: any"),
|
|
474
|
+
conditions: z2.array(conditionSchema("filter")).max(10)
|
|
475
|
+
}).nullable().optional().describe(
|
|
476
|
+
"Send only captures that match (null: everything it takes). Conditions like response rules plus kind, email (name: subject, from, to, text, tag), spam_score, email_auth (name: spf, dkim, dmarc) and signature (verified, failed, missing)"
|
|
445
477
|
),
|
|
446
|
-
|
|
447
|
-
|
|
478
|
+
format: z2.enum([
|
|
479
|
+
"auto",
|
|
480
|
+
"as_received",
|
|
481
|
+
"json",
|
|
482
|
+
"template",
|
|
483
|
+
"chat",
|
|
484
|
+
"postmark",
|
|
485
|
+
"sendgrid",
|
|
486
|
+
"mailgun",
|
|
487
|
+
"cloudmailin"
|
|
488
|
+
]).optional().describe(
|
|
489
|
+
"auto (pick from the URL), as_received (relay HTTP requests unchanged; emails as signed JSON), json (signed JSON envelope), template (your bodyTemplate, signed like json), chat (Slack or Discord message), or an inbound mail provider's shape for emails (postmark, sendgrid, mailgun, cloudmailin; HTTP requests then go as received), to test the inbound handler you wrote for that provider"
|
|
448
490
|
),
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
(
|
|
456
|
-
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
491
|
+
mailgunSigningKey: z2.string().max(200).nullable().optional().describe(
|
|
492
|
+
"For format mailgun: your Mailgun HTTP webhook signing key, so the form's signature verifies; null removes it. Write-only."
|
|
493
|
+
),
|
|
494
|
+
bodyTemplate: z2.unknown().optional().describe(
|
|
495
|
+
`For format template: any JSON value whose strings hold {{ placeholders }}, e.g. {"event": "{{ json.type }}", "amount": "{{ json.data.amount }}", "code": "{{ codes.0 }}"}. A string that is only a placeholder keeps the value's type; a missing field becomes null. HTTP requests read method, path, query.x, headers.x, body, json.path, id, receivedAt, signature; emails read the email.received fields (subject, from.address, to, text, codes, links, auth, spam, tag)`
|
|
496
|
+
),
|
|
497
|
+
messageTemplate: z2.string().max(4e3).nullable().optional().describe(
|
|
498
|
+
"For format chat: the message text with {{ placeholders }}; null sends the standard message"
|
|
499
|
+
),
|
|
500
|
+
headers: z2.array(
|
|
501
|
+
z2.object({
|
|
502
|
+
name: z2.string().describe("Header name, e.g. Authorization"),
|
|
503
|
+
value: z2.string().nullable().describe("Value, or null to keep the stored one")
|
|
504
|
+
})
|
|
505
|
+
).max(10).optional().describe("Your own headers for every delivery (not chat). Replaces the whole list"),
|
|
506
|
+
appendPath: z2.boolean().optional().describe("Append the captured path to the URL (as_received)"),
|
|
507
|
+
retrySeconds: z2.union([z2.literal(0), z2.literal(3600), z2.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
|
|
508
|
+
keepOrder: z2.boolean().optional().describe("One delivery at a time, in capture order"),
|
|
509
|
+
sentField: z2.string().max(128).nullable().optional().describe("JSON body field with the sender's own timestamp"),
|
|
510
|
+
throttleSeconds: z2.number().int().min(0).max(3600).optional().describe(
|
|
511
|
+
"At most one delivery every N seconds; the rest are not sent (1 makes a heads-up channel)"
|
|
512
|
+
),
|
|
513
|
+
delaySeconds: z2.number().int().min(0).max(3600).optional().describe("Wait this long after the capture before sending, like a callback")
|
|
514
|
+
};
|
|
462
515
|
var responseRuleSchema = z2.object({
|
|
463
516
|
name: z2.string().max(MAX_RULE_NAME_LEN).optional().describe(`Human-readable rule name (max ${MAX_RULE_NAME_LEN} chars)`),
|
|
464
517
|
enabled: z2.boolean().optional().default(true).describe("Whether this rule is active"),
|
|
@@ -469,8 +522,10 @@ var responseRuleSchema = z2.object({
|
|
|
469
522
|
var responseRulesSchema = z2.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
|
|
470
523
|
var mockResponseSchema = z2.object({
|
|
471
524
|
status: z2.number().int().min(MOCK_RESPONSE_STATUS_MIN).max(MOCK_RESPONSE_STATUS_MAX).describe(`HTTP status code (${MOCK_RESPONSE_STATUS_MIN}-${MOCK_RESPONSE_STATUS_MAX})`),
|
|
472
|
-
body: z2.string().default("").describe(
|
|
473
|
-
|
|
525
|
+
body: z2.string().default("").describe(
|
|
526
|
+
'Response body (default: empty). {{ placeholders }} are filled in from the request: {{ json.id }} (a JSON or form body field), {{ body }}, {{ headers.user-agent }}, {{ query.page }}, {{ method }}, {{ path }}, {{ id }}, {{ uuid }}, {{ now }}; filters: | default: "x", | json, | lower, | upper, | truncate: 80'
|
|
527
|
+
),
|
|
528
|
+
headers: z2.record(z2.string(), z2.string()).default({}).describe("Response headers (default: none); values may use {{ placeholders }} too"),
|
|
474
529
|
delay: z2.number().int().min(MOCK_RESPONSE_DELAY_MIN).max(MOCK_RESPONSE_DELAY_MAX).optional().describe(
|
|
475
530
|
`Response delay in milliseconds (${MOCK_RESPONSE_DELAY_MIN}-${MOCK_RESPONSE_DELAY_MAX}, default: none)`
|
|
476
531
|
)
|
|
@@ -513,6 +568,67 @@ var waitForRequestShape = {
|
|
|
513
568
|
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
514
569
|
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
|
|
515
570
|
};
|
|
571
|
+
var payloadSchemaShape = {
|
|
572
|
+
endpointSlug: z2.string().describe("The endpoint slug"),
|
|
573
|
+
format: z2.enum(PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
|
|
574
|
+
'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
|
|
575
|
+
),
|
|
576
|
+
name: z2.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
|
|
577
|
+
event: z2.string().optional().describe(
|
|
578
|
+
'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
|
|
579
|
+
),
|
|
580
|
+
method: z2.string().optional().describe("Only requests with this HTTP method"),
|
|
581
|
+
path: z2.string().optional().describe("Only requests with this path"),
|
|
582
|
+
requestIds: z2.array(z2.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
|
|
583
|
+
limit: z2.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
|
|
584
|
+
};
|
|
585
|
+
function eventName(event) {
|
|
586
|
+
const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
587
|
+
const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
588
|
+
if (!name) return "WebhookPayload";
|
|
589
|
+
return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
|
|
590
|
+
}
|
|
591
|
+
function payloadSchemaResult(requests, args) {
|
|
592
|
+
const method = args.method?.toUpperCase();
|
|
593
|
+
const matching = requests.filter(
|
|
594
|
+
(request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args.path || request.path === args.path) && (!args.event || detectWebhookInfo(request)?.event === args.event)
|
|
595
|
+
);
|
|
596
|
+
if (matching.length === 0) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const events = [
|
|
602
|
+
...new Set(
|
|
603
|
+
matching.map((request) => detectWebhookInfo(request)?.event).filter((e) => !!e)
|
|
604
|
+
)
|
|
605
|
+
];
|
|
606
|
+
const name = args.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
|
|
607
|
+
const result = generateRequestSchema(matching, { format: args.format, name });
|
|
608
|
+
if (result.samples === 0) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
`None of the ${matching.length} matching requests has a JSON or form body to learn from.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const output = {
|
|
614
|
+
format: result.format,
|
|
615
|
+
name: result.name,
|
|
616
|
+
samples: result.samples,
|
|
617
|
+
skipped: result.skipped,
|
|
618
|
+
...events.length > 0 ? { events } : {},
|
|
619
|
+
...events.length > 1 ? {
|
|
620
|
+
warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
|
|
621
|
+
} : {},
|
|
622
|
+
code: result.code
|
|
623
|
+
};
|
|
624
|
+
const size = JSON.stringify(output, null, 2).length;
|
|
625
|
+
if (size > MAX_OUTPUT) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`The schema is ${result.code.length} characters, more than this tool can return. Narrow it with event or requestIds, or call generateRequestSchema() from @webhooks-cc/sdk in code.`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return jsonContent(output);
|
|
631
|
+
}
|
|
516
632
|
function textContent(text) {
|
|
517
633
|
return { content: [{ type: "text", text }] };
|
|
518
634
|
}
|
|
@@ -764,6 +880,8 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
764
880
|
"tag",
|
|
765
881
|
"subject",
|
|
766
882
|
"from",
|
|
883
|
+
"fields",
|
|
884
|
+
"replyText",
|
|
767
885
|
"size",
|
|
768
886
|
"test",
|
|
769
887
|
"spam",
|
|
@@ -773,6 +891,11 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
773
891
|
"html",
|
|
774
892
|
"htmlTruncated"
|
|
775
893
|
];
|
|
894
|
+
function shrinkRendered(rendered, max) {
|
|
895
|
+
const body = typeof rendered.body === "string" ? rendered.body : "";
|
|
896
|
+
if (body.length <= max - 1024) return rendered;
|
|
897
|
+
return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
|
|
898
|
+
}
|
|
776
899
|
function emailDetail(client, email, view, includeHtml) {
|
|
777
900
|
const { data } = client.emails.toJson(email, {
|
|
778
901
|
endpoint: view.endpoint,
|
|
@@ -935,7 +1058,7 @@ function registerTools(server, client) {
|
|
|
935
1058
|
);
|
|
936
1059
|
server.tool(
|
|
937
1060
|
"update_endpoint",
|
|
938
|
-
"Update an endpoint name, mock response, conditional response rules, signing configuration,
|
|
1061
|
+
"Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
|
|
939
1062
|
{
|
|
940
1063
|
slug: z2.string().describe("The endpoint slug to update"),
|
|
941
1064
|
name: z2.string().optional().describe("New display name"),
|
|
@@ -949,7 +1072,17 @@ function registerTools(server, client) {
|
|
|
949
1072
|
signingHeader: z2.string().nullable().optional().describe(
|
|
950
1073
|
"Custom signature header name (only for generic-hmac provider), or null to clear"
|
|
951
1074
|
),
|
|
952
|
-
showEmailExtracts: z2.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
|
|
1075
|
+
showEmailExtracts: z2.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
|
|
1076
|
+
emailFields: z2.array(
|
|
1077
|
+
z2.object({
|
|
1078
|
+
name: z2.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
|
|
1079
|
+
from: z2.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
|
|
1080
|
+
header: z2.string().optional().describe('The header name, for from "header"'),
|
|
1081
|
+
pattern: z2.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
|
|
1082
|
+
})
|
|
1083
|
+
).max(20).nullable().optional().describe(
|
|
1084
|
+
"Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
|
|
1085
|
+
)
|
|
953
1086
|
},
|
|
954
1087
|
withErrorHandling(
|
|
955
1088
|
async ({
|
|
@@ -961,7 +1094,8 @@ function registerTools(server, client) {
|
|
|
961
1094
|
signingProvider,
|
|
962
1095
|
signingSecret,
|
|
963
1096
|
signingHeader,
|
|
964
|
-
showEmailExtracts
|
|
1097
|
+
showEmailExtracts,
|
|
1098
|
+
emailFields
|
|
965
1099
|
}) => {
|
|
966
1100
|
const endpoint = await client.endpoints.update(slug, {
|
|
967
1101
|
name,
|
|
@@ -971,12 +1105,33 @@ function registerTools(server, client) {
|
|
|
971
1105
|
signingProvider,
|
|
972
1106
|
signingSecret,
|
|
973
1107
|
signingHeader,
|
|
974
|
-
showEmailExtracts
|
|
1108
|
+
showEmailExtracts,
|
|
1109
|
+
emailFields
|
|
975
1110
|
});
|
|
976
1111
|
return jsonContent(endpoint);
|
|
977
1112
|
}
|
|
978
1113
|
)
|
|
979
1114
|
);
|
|
1115
|
+
server.tool(
|
|
1116
|
+
"preview_response",
|
|
1117
|
+
"Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
|
|
1118
|
+
{
|
|
1119
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1120
|
+
requestId: z2.string().optional().describe(
|
|
1121
|
+
"A captured request on the endpoint (default: the newest HTTP request, or a sample)"
|
|
1122
|
+
),
|
|
1123
|
+
mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
|
|
1124
|
+
responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
|
|
1125
|
+
},
|
|
1126
|
+
withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
|
|
1127
|
+
const preview = await client.endpoints.previewResponse(slug, {
|
|
1128
|
+
...requestId ? { requestId } : {},
|
|
1129
|
+
...mockResponse !== void 0 ? { mockResponse } : {},
|
|
1130
|
+
...responseRules !== void 0 ? { responseRules } : {}
|
|
1131
|
+
});
|
|
1132
|
+
return jsonContent(preview);
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
980
1135
|
server.tool(
|
|
981
1136
|
"delete_endpoint",
|
|
982
1137
|
"Delete a webhook endpoint and all its captured requests.",
|
|
@@ -1192,6 +1347,39 @@ function registerTools(server, client) {
|
|
|
1192
1347
|
});
|
|
1193
1348
|
})
|
|
1194
1349
|
);
|
|
1350
|
+
server.tool(
|
|
1351
|
+
"render_email",
|
|
1352
|
+
"A captured email in the shape an inbound mail provider posts it (postmark, sendgrid, mailgun, cloudmailin), to test the inbound handler you wrote for that provider against real mail. Returns the method, headers and body; with targetUrl (your local handler), posts it there and returns the answer. A Mailgun form is signed when you pass the signing key your handler checks.",
|
|
1353
|
+
{
|
|
1354
|
+
requestId: z2.string().describe("The captured email's request ID"),
|
|
1355
|
+
format: z2.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
|
|
1356
|
+
mailgunSigningKey: z2.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
|
|
1357
|
+
targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
|
|
1358
|
+
},
|
|
1359
|
+
withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
|
|
1360
|
+
const rendered = await client.emails.render(requestId, {
|
|
1361
|
+
format,
|
|
1362
|
+
...mailgunSigningKey ? { mailgunSigningKey } : {}
|
|
1363
|
+
});
|
|
1364
|
+
if (!targetUrl) {
|
|
1365
|
+
return jsonContent(
|
|
1366
|
+
shrinkRendered(rendered, MAX_BODY_SIZE)
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const response = await fetch(targetUrl, {
|
|
1370
|
+
method: "POST",
|
|
1371
|
+
headers: { "content-type": rendered.headers["content-type"] },
|
|
1372
|
+
body: rendered.body,
|
|
1373
|
+
redirect: "manual"
|
|
1374
|
+
});
|
|
1375
|
+
return jsonContent({
|
|
1376
|
+
format,
|
|
1377
|
+
status: response.status,
|
|
1378
|
+
statusText: response.statusText,
|
|
1379
|
+
body: await readBodyTruncated(response)
|
|
1380
|
+
});
|
|
1381
|
+
})
|
|
1382
|
+
);
|
|
1195
1383
|
server.tool(
|
|
1196
1384
|
"compare_requests",
|
|
1197
1385
|
"Compare two captured requests and show the structured differences.",
|
|
@@ -1209,6 +1397,15 @@ function registerTools(server, client) {
|
|
|
1209
1397
|
return jsonContent(diff);
|
|
1210
1398
|
})
|
|
1211
1399
|
);
|
|
1400
|
+
server.tool(
|
|
1401
|
+
"generate_payload_schema",
|
|
1402
|
+
"Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from captured request bodies. Several requests of one event teach which fields are optional or nullable, so filter with event (or pass requestIds) and write the result into the codebase instead of hand-typing the payload.",
|
|
1403
|
+
payloadSchemaShape,
|
|
1404
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
|
|
1405
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
|
|
1406
|
+
return payloadSchemaResult(requests, args);
|
|
1407
|
+
})
|
|
1408
|
+
);
|
|
1212
1409
|
server.tool(
|
|
1213
1410
|
"extract_from_request",
|
|
1214
1411
|
"Extract specific JSON fields from a captured request body.",
|
|
@@ -1566,9 +1763,79 @@ function registerTools(server, client) {
|
|
|
1566
1763
|
return jsonContent({ ...result, address: client.emails.address(slug, tag) });
|
|
1567
1764
|
})
|
|
1568
1765
|
);
|
|
1766
|
+
server.tool(
|
|
1767
|
+
"list_destinations",
|
|
1768
|
+
"List where an endpoint's captures go: its destinations (up to 10) with their URL, kinds, filter, format, templates and timing, and with summary: true their last 24 hours. Owner only.",
|
|
1769
|
+
{
|
|
1770
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1771
|
+
summary: z2.boolean().default(true).describe("Add each destination's last 24 hours")
|
|
1772
|
+
},
|
|
1773
|
+
withErrorHandling(async ({ slug, summary }) => {
|
|
1774
|
+
return jsonContent(await client.destinations.list(slug, { summary }));
|
|
1775
|
+
})
|
|
1776
|
+
);
|
|
1777
|
+
server.tool(
|
|
1778
|
+
"add_destination",
|
|
1779
|
+
"Add a destination for an endpoint's captures: your server (as received or signed JSON), a Slack or Discord channel (chat, optionally your own messageTemplate, throttleSeconds 1 for a heads-up), or a JSON body you shape with bodyTemplate (for Zapier, n8n or your API). filter sends only matching captures; delaySeconds makes it a callback. Each destination gets its own signing secret. Check it with preview_destination before relying on it. Owner only.",
|
|
1780
|
+
{
|
|
1781
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1782
|
+
url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
|
|
1783
|
+
...destinationFields
|
|
1784
|
+
},
|
|
1785
|
+
withErrorHandling(async ({ slug, ...input }) => {
|
|
1786
|
+
return jsonContent(await client.destinations.create(slug, input));
|
|
1787
|
+
})
|
|
1788
|
+
);
|
|
1789
|
+
server.tool(
|
|
1790
|
+
"update_destination",
|
|
1791
|
+
"Change a destination: any of its fields; only the ones you pass change. Owner only.",
|
|
1792
|
+
{
|
|
1793
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1794
|
+
destinationId: z2.string().describe("The destination's id (from list_destinations)"),
|
|
1795
|
+
url: httpUrlSchema.optional().describe("A new URL"),
|
|
1796
|
+
...destinationFields
|
|
1797
|
+
},
|
|
1798
|
+
withErrorHandling(async ({ slug, destinationId, ...input }) => {
|
|
1799
|
+
return jsonContent(await client.destinations.update(slug, destinationId, input));
|
|
1800
|
+
})
|
|
1801
|
+
);
|
|
1802
|
+
server.tool(
|
|
1803
|
+
"remove_destination",
|
|
1804
|
+
"Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
|
|
1805
|
+
{
|
|
1806
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1807
|
+
destinationId: z2.string().describe("The destination's id")
|
|
1808
|
+
},
|
|
1809
|
+
withErrorHandling(async ({ slug, destinationId }) => {
|
|
1810
|
+
await client.destinations.delete(slug, destinationId);
|
|
1811
|
+
return textContent(`Destination ${destinationId} removed.`);
|
|
1812
|
+
})
|
|
1813
|
+
);
|
|
1814
|
+
server.tool(
|
|
1815
|
+
"preview_destination",
|
|
1816
|
+
"Dry run of a destination against a captured request: whether its filter lets the request through and the exact request it would send (method, URL, headers, body with placeholders filled in). Nothing is sent. Use destinationId for a saved destination, destination for unsaved fields (on top of the saved ones, or with a url on their own), or both.",
|
|
1817
|
+
{
|
|
1818
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1819
|
+
destinationId: z2.string().optional().describe("A saved destination"),
|
|
1820
|
+
destination: z2.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
|
|
1821
|
+
requestId: z2.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
|
|
1822
|
+
},
|
|
1823
|
+
withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
|
|
1824
|
+
if (!destinationId && !destination?.url) {
|
|
1825
|
+
throw new Error("Pass destinationId, or a destination with a url");
|
|
1826
|
+
}
|
|
1827
|
+
return jsonContent(
|
|
1828
|
+
await client.destinations.preview(slug, {
|
|
1829
|
+
...destinationId ? { destinationId } : {},
|
|
1830
|
+
...destination ? { destination } : {},
|
|
1831
|
+
...requestId ? { requestId } : {}
|
|
1832
|
+
})
|
|
1833
|
+
);
|
|
1834
|
+
})
|
|
1835
|
+
);
|
|
1569
1836
|
server.tool(
|
|
1570
1837
|
"configure_forwarding",
|
|
1571
|
-
"Change an endpoint's forwarding: the URL, on or off, which kinds
|
|
1838
|
+
"Change an endpoint's primary forwarding destination (the one the endpoint's forward* fields stand for): the URL, on or off, which kinds, the format, your own headers, and the advanced options. Only the arguments you pass change. Owner only. For several destinations, filters, templates, throttling or delays, use add_destination and update_destination.",
|
|
1572
1839
|
{
|
|
1573
1840
|
slug: z2.string().describe("The endpoint slug"),
|
|
1574
1841
|
url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
|
|
@@ -1616,23 +1883,32 @@ function registerTools(server, client) {
|
|
|
1616
1883
|
"Read (or with rotate: true, replace) the whsec_ secret forwarded deliveries are signed with. Owner only. Treat the value as a credential: put it in the handler's environment, not in code.",
|
|
1617
1884
|
{
|
|
1618
1885
|
slug: z2.string().describe("The endpoint slug"),
|
|
1886
|
+
destinationId: z2.string().optional().describe("A destination (default: the endpoint's primary destination)"),
|
|
1619
1887
|
rotate: z2.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
|
|
1620
1888
|
},
|
|
1621
|
-
withErrorHandling(async ({ slug, rotate }) => {
|
|
1622
|
-
const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1889
|
+
withErrorHandling(async ({ slug, destinationId, rotate }) => {
|
|
1890
|
+
const secret = destinationId ? rotate ? await client.destinations.rotateSecret(slug, destinationId) : await client.destinations.secret(slug, destinationId) : rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1623
1891
|
return jsonContent({ secret, rotated: rotate });
|
|
1624
1892
|
})
|
|
1625
1893
|
);
|
|
1626
1894
|
server.tool(
|
|
1627
1895
|
"test_forwarding",
|
|
1628
|
-
"Send the endpoint's newest captured request of a kind (or a sample) to
|
|
1896
|
+
"Send the endpoint's newest captured request of a kind (or a sample, or requestId) to a destination once, exactly as a delivery would go out, and report the answer. Works whether it is on or off, and whatever its filter says; nothing is retried or logged.",
|
|
1629
1897
|
{
|
|
1630
1898
|
slug: z2.string().describe("The endpoint slug"),
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
)
|
|
1899
|
+
destinationId: z2.string().optional().describe("A destination (default: the endpoint's primary destination)"),
|
|
1900
|
+
kind: z2.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
|
|
1901
|
+
requestId: z2.string().optional().describe("With destinationId: a captured request to send")
|
|
1634
1902
|
},
|
|
1635
|
-
withErrorHandling(async ({ slug, kind }) => {
|
|
1903
|
+
withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
|
|
1904
|
+
if (destinationId) {
|
|
1905
|
+
return jsonContent(
|
|
1906
|
+
await client.destinations.test(slug, destinationId, {
|
|
1907
|
+
...kind ? { kind } : {},
|
|
1908
|
+
...requestId ? { requestId } : {}
|
|
1909
|
+
})
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1636
1912
|
return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
|
|
1637
1913
|
})
|
|
1638
1914
|
);
|
|
@@ -1643,17 +1919,25 @@ function registerTools(server, client) {
|
|
|
1643
1919
|
endpointSlug: z2.string().optional().describe("List the endpoint's delivery log"),
|
|
1644
1920
|
requestId: z2.string().optional().describe("List every delivery and try of this request"),
|
|
1645
1921
|
limit: z2.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
|
|
1646
|
-
status: z2.enum(["all", "pending", "failed"]).default("all").describe(
|
|
1922
|
+
status: z2.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
|
|
1923
|
+
"With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
|
|
1924
|
+
),
|
|
1925
|
+
destinationId: z2.string().optional().describe("With endpointSlug: only this destination"),
|
|
1647
1926
|
before: z2.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
|
|
1648
1927
|
},
|
|
1649
|
-
withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
|
|
1928
|
+
withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
|
|
1650
1929
|
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1651
1930
|
throw new Error("Pass exactly one of endpointSlug or requestId");
|
|
1652
1931
|
}
|
|
1653
1932
|
if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
|
|
1654
1933
|
const [summary, deliveries] = await Promise.all([
|
|
1655
1934
|
client.forwarding.summary(endpointSlug),
|
|
1656
|
-
client.forwarding.deliveries(endpointSlug, {
|
|
1935
|
+
client.forwarding.deliveries(endpointSlug, {
|
|
1936
|
+
limit,
|
|
1937
|
+
status,
|
|
1938
|
+
before,
|
|
1939
|
+
...destinationId ? { destinationId } : {}
|
|
1940
|
+
})
|
|
1657
1941
|
]);
|
|
1658
1942
|
return jsonContent({
|
|
1659
1943
|
summary,
|
|
@@ -1664,17 +1948,19 @@ function registerTools(server, client) {
|
|
|
1664
1948
|
);
|
|
1665
1949
|
server.tool(
|
|
1666
1950
|
"redeliver",
|
|
1667
|
-
"Forward again with the
|
|
1951
|
+
"Forward again with the current settings: one captured request or email (requestId; to every destination that takes it, or with destinationId to that one whatever its filter says), or every request whose latest delivery failed (endpointSlug, optionally only destinationId, oldest first, up to 1,000 at a time per destination). Owner only.",
|
|
1668
1952
|
{
|
|
1669
1953
|
requestId: z2.string().optional().describe("The request or email to send again"),
|
|
1670
|
-
endpointSlug: z2.string().optional().describe("Send every failed delivery of this endpoint again")
|
|
1954
|
+
endpointSlug: z2.string().optional().describe("Send every failed delivery of this endpoint again"),
|
|
1955
|
+
destinationId: z2.string().optional().describe("Only this destination")
|
|
1671
1956
|
},
|
|
1672
|
-
withErrorHandling(async ({ requestId, endpointSlug }) => {
|
|
1957
|
+
withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
|
|
1673
1958
|
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1674
1959
|
throw new Error("Pass exactly one of requestId or endpointSlug");
|
|
1675
1960
|
}
|
|
1961
|
+
const options = destinationId ? { destinationId } : {};
|
|
1676
1962
|
return jsonContent(
|
|
1677
|
-
requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
|
|
1963
|
+
requestId ? await client.forwarding.redeliver(requestId, options) : await client.forwarding.redeliverFailed(endpointSlug, options)
|
|
1678
1964
|
);
|
|
1679
1965
|
})
|
|
1680
1966
|
);
|
|
@@ -1899,6 +2185,18 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1899
2185
|
})
|
|
1900
2186
|
)
|
|
1901
2187
|
);
|
|
2188
|
+
registered.push(
|
|
2189
|
+
server.tool(
|
|
2190
|
+
"generate_payload_schema",
|
|
2191
|
+
"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.",
|
|
2192
|
+
payloadSchemaShape,
|
|
2193
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args }) => {
|
|
2194
|
+
const sandbox = await session.requireSandbox();
|
|
2195
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => sandbox.requests.get(id))) : await sandbox.requests.list(endpointSlug, { limit });
|
|
2196
|
+
return payloadSchemaResult(requests, args);
|
|
2197
|
+
})
|
|
2198
|
+
)
|
|
2199
|
+
);
|
|
1902
2200
|
registered.push(
|
|
1903
2201
|
server.tool(
|
|
1904
2202
|
"connect_account",
|
|
@@ -2057,7 +2355,7 @@ function registerAgentRegistrationTools(server) {
|
|
|
2057
2355
|
}
|
|
2058
2356
|
|
|
2059
2357
|
// src/index.ts
|
|
2060
|
-
var VERSION = true ? "1.
|
|
2358
|
+
var VERSION = true ? "1.14.0" : "0.0.0-dev";
|
|
2061
2359
|
function createServer(options = {}) {
|
|
2062
2360
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2063
2361
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webhooks-cc/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"mcpName": "io.github.kroqdotdev/webhooks-cc",
|
|
4
5
|
"description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"module": "dist/index.mjs",
|
|
@@ -31,20 +32,20 @@
|
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/kroqdotdev/webhooks-cc",
|
|
35
36
|
"directory": "packages/mcp"
|
|
36
37
|
},
|
|
37
38
|
"homepage": "https://webhooks.cc",
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
-
"zod": "^4.6.
|
|
41
|
-
"@webhooks-cc/sdk": "1.
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
41
|
+
"zod": "^4.6.7",
|
|
42
|
+
"@webhooks-cc/sdk": "1.18.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^26.6.
|
|
45
|
-
"tsup": "^
|
|
45
|
+
"@types/node": "^26.6.5",
|
|
46
|
+
"tsup": "^9.0.0",
|
|
46
47
|
"typescript": "^6.0.3",
|
|
47
|
-
"vitest": "^5.0.
|
|
48
|
+
"vitest": "^5.0.5"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsup",
|