@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/README.md
CHANGED
|
@@ -11,6 +11,16 @@ The server reads `WHK_API_KEY` from the environment. It also supports:
|
|
|
11
11
|
- `WHK_BASE_URL` for a custom API base URL
|
|
12
12
|
- `WHK_WEBHOOK_URL` for a custom receiver base URL
|
|
13
13
|
|
|
14
|
+
### Claude Code plugin
|
|
15
|
+
|
|
16
|
+
The plugin installs the server together with a webhook testing skill that tells Claude when to use it:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
/plugin install webhooks-cc --marketplace kroqdotdev/webhooks-cc
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For other agents, `npx skills add kroqdotdev/webhooks-cc` installs the same skill. See [Agent skill](https://webhooks.cc/docs/mcp/skills).
|
|
23
|
+
|
|
14
24
|
### One-click
|
|
15
25
|
|
|
16
26
|
- **Cursor**: [Add to Cursor](https://cursor.com/en/install-mcp?name=webhooks-cc&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3ZWJob29rcy1jYy9tY3AiXSwiZW52Ijp7IldIS19BUElfS0VZIjoid2hjY18uLi4ifX0=)
|
|
@@ -54,11 +64,11 @@ npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
|
|
|
54
64
|
}
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
Get your API key at [webhooks.cc/account](https://webhooks.cc/account).
|
|
67
|
+
Get your API key at [webhooks.cc/account/api-keys](https://webhooks.cc/account/api-keys).
|
|
58
68
|
|
|
59
69
|
## What it exposes
|
|
60
70
|
|
|
61
|
-
The server exposes `
|
|
71
|
+
The server exposes `46` tools, `3` prompts, and `3` resource surfaces with an API key, and `11` sandbox tools without one.
|
|
62
72
|
|
|
63
73
|
### Endpoint tools
|
|
64
74
|
|
|
@@ -66,6 +76,7 @@ The server exposes `38` tools, `3` prompts, and `3` resource surfaces with an AP
|
|
|
66
76
|
- `list_endpoints`
|
|
67
77
|
- `get_endpoint`
|
|
68
78
|
- `update_endpoint`
|
|
79
|
+
- `preview_response`
|
|
69
80
|
- `delete_endpoint`
|
|
70
81
|
- `create_endpoints`
|
|
71
82
|
- `delete_endpoints`
|
|
@@ -80,6 +91,7 @@ The server exposes `38` tools, `3` prompts, and `3` resource surfaces with an AP
|
|
|
80
91
|
- `wait_for_requests`
|
|
81
92
|
- `replay_request`
|
|
82
93
|
- `compare_requests`
|
|
94
|
+
- `generate_payload_schema` (TypeScript, Zod, Pydantic, or JSON Schema from captured bodies)
|
|
83
95
|
- `extract_from_request`
|
|
84
96
|
- `verify_signature`
|
|
85
97
|
- `clear_requests`
|
|
@@ -92,19 +104,24 @@ Every endpoint on an account receives email at `{slug}@mailhooks.cc` (and `{slug
|
|
|
92
104
|
- `get_email`
|
|
93
105
|
- `wait_for_email`
|
|
94
106
|
- `send_test_email`
|
|
107
|
+
- `render_email` (an email in Postmark, SendGrid, Mailgun or CloudMailin shape, optionally posted to your local inbound handler)
|
|
95
108
|
|
|
96
109
|
The request tools and resources return emails too (filter with `kind`), without the raw message or the HTML part and with the text cut at 8,000 characters; `get_email` returns the HTML.
|
|
97
110
|
|
|
98
111
|
### Forwarding tools
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
Send captured HTTP requests and emails to up to 10 destinations (your servers, Slack or Discord
|
|
114
|
+
channels, Zapier or n8n), each with its own kinds, conditions, format (as received, signed JSON,
|
|
115
|
+
a JSON body you shape with placeholders, or a chat message), throttle, delay and signing secret,
|
|
116
|
+
retried and logged (owner only):
|
|
102
117
|
|
|
103
|
-
- `
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
107
|
-
- `
|
|
118
|
+
- `list_destinations`, `add_destination`, `update_destination`, `remove_destination`
|
|
119
|
+
- `preview_destination` (dry run: does the filter pass, and the exact request it would send)
|
|
120
|
+
- `configure_forwarding` (the primary destination: URL, on/off, kinds, format, your headers, path, retries, order, sent time field)
|
|
121
|
+
- `get_forwarding_secret` (returns a destination's `whsec_` secret; with `rotate: true`, replaces it)
|
|
122
|
+
- `test_forwarding` (sends one request to a destination and reports the answer)
|
|
123
|
+
- `list_deliveries` (the delivery log with its counts, by destination, or every try of one request)
|
|
124
|
+
- `redeliver` (one request, to one destination or all, or every failed delivery of an endpoint)
|
|
108
125
|
|
|
109
126
|
### Send and test tools
|
|
110
127
|
|
|
@@ -132,7 +149,7 @@ Endpoints shared with you already appear in `list_endpoints` (with `fromTeam`) a
|
|
|
132
149
|
|
|
133
150
|
Started without `WHK_API_KEY`, the server registers sandbox tools instead ([auth.md](https://webhooks.cc/auth.md)):
|
|
134
151
|
|
|
135
|
-
- `create_endpoint`, `list_endpoints`, `get_endpoint`, `delete_endpoint`, `list_requests`, `get_request`, `wait_for_request`: the same names and arguments as the full tools, in a sandbox of up to 3 endpoints, 25 captured requests each and 100 in all, for 24 hours. The first `create_endpoint` registers it (a few seconds of proof of work).
|
|
152
|
+
- `create_endpoint`, `list_endpoints`, `get_endpoint`, `delete_endpoint`, `list_requests`, `get_request`, `wait_for_request`, `generate_payload_schema`: the same names and arguments as the full tools, in a sandbox of up to 3 endpoints, 25 captured requests each and 100 in all, for 24 hours. The first `create_endpoint` registers it (a few seconds of proof of work).
|
|
136
153
|
- `connect_account`: returns a link and a 6-digit code for a human to connect the agent to their account.
|
|
137
154
|
- `wait_for_connection`: waits for the human, then replaces the sandbox tools with the full tools for that account (`notifications/tools/list_changed`).
|
|
138
155
|
- `about_sandbox`: the limits and how to connect, without a network call.
|
package/dist/bin/mcp.js
CHANGED
|
@@ -76,6 +76,7 @@ function registerPrompts(server) {
|
|
|
76
76
|
"If a target URL is provided, use preview_webhook before send_to so the request shape is visible.",
|
|
77
77
|
targetUrl ? `Target URL: ${targetUrl}.` : null,
|
|
78
78
|
"Send a realistic provider webhook, wait for capture, and verify the signature if a secret is available.",
|
|
79
|
+
"Once real deliveries are captured, use generate_payload_schema to write the payload types for the handler.",
|
|
79
80
|
"Return the endpoint URL, the exact tools used, and the next step for the developer."
|
|
80
81
|
].filter(Boolean).join(" ")
|
|
81
82
|
}
|
|
@@ -417,28 +418,75 @@ var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HT
|
|
|
417
418
|
var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
|
|
418
419
|
var EMAIL_SCAN_LIMIT = 100;
|
|
419
420
|
var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
|
|
420
|
-
var
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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)"
|
|
425
452
|
),
|
|
426
|
-
|
|
427
|
-
|
|
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"
|
|
428
465
|
),
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
(
|
|
436
|
-
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
};
|
|
442
490
|
var responseRuleSchema = import_zod2.z.object({
|
|
443
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)`),
|
|
444
492
|
enabled: import_zod2.z.boolean().optional().default(true).describe("Whether this rule is active"),
|
|
@@ -449,8 +497,10 @@ var responseRuleSchema = import_zod2.z.object({
|
|
|
449
497
|
var responseRulesSchema = import_zod2.z.array(responseRuleSchema).max(50).describe("Ordered conditional response rules (first match wins, max 50)");
|
|
450
498
|
var mockResponseSchema = import_zod2.z.object({
|
|
451
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})`),
|
|
452
|
-
body: import_zod2.z.string().default("").describe(
|
|
453
|
-
|
|
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"),
|
|
454
504
|
delay: import_zod2.z.number().int().min(import_sdk.MOCK_RESPONSE_DELAY_MIN).max(import_sdk.MOCK_RESPONSE_DELAY_MAX).optional().describe(
|
|
455
505
|
`Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
|
|
456
506
|
)
|
|
@@ -493,6 +543,67 @@ var waitForRequestShape = {
|
|
|
493
543
|
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
494
544
|
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
|
|
495
545
|
};
|
|
546
|
+
var payloadSchemaShape = {
|
|
547
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
548
|
+
format: import_zod2.z.enum(import_sdk.PAYLOAD_SCHEMA_FORMATS).default("typescript").describe(
|
|
549
|
+
'Output: "typescript" interfaces, "zod" schemas, "pydantic" models, or "json-schema"'
|
|
550
|
+
),
|
|
551
|
+
name: import_zod2.z.string().max(100).optional().describe("Root type name (default: from the detected event, else WebhookPayload)"),
|
|
552
|
+
event: import_zod2.z.string().optional().describe(
|
|
553
|
+
'Only requests whose detected provider event matches, e.g. "invoice.paid" or "push". One event per schema gives the tightest types.'
|
|
554
|
+
),
|
|
555
|
+
method: import_zod2.z.string().optional().describe("Only requests with this HTTP method"),
|
|
556
|
+
path: import_zod2.z.string().optional().describe("Only requests with this path"),
|
|
557
|
+
requestIds: import_zod2.z.array(import_zod2.z.string()).min(1).max(50).optional().describe("Use exactly these requests instead of the latest ones"),
|
|
558
|
+
limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("How many of the latest requests to read before filtering")
|
|
559
|
+
};
|
|
560
|
+
function eventName(event) {
|
|
561
|
+
const words = event.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
562
|
+
const name = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
563
|
+
if (!name) return "WebhookPayload";
|
|
564
|
+
return /^[0-9]/.test(name) ? `Webhook${name}Event` : `${name}Event`;
|
|
565
|
+
}
|
|
566
|
+
function payloadSchemaResult(requests, args2) {
|
|
567
|
+
const method = args2.method?.toUpperCase();
|
|
568
|
+
const matching = requests.filter(
|
|
569
|
+
(request) => request.kind !== "email" && (!method || request.method.toUpperCase() === method) && (!args2.path || request.path === args2.path) && (!args2.event || (0, import_sdk.detectWebhookInfo)(request)?.event === args2.event)
|
|
570
|
+
);
|
|
571
|
+
if (matching.length === 0) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`None of the ${requests.length} requests read matched. Capture a request first, or loosen event, method or path.`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const events = [
|
|
577
|
+
...new Set(
|
|
578
|
+
matching.map((request) => (0, import_sdk.detectWebhookInfo)(request)?.event).filter((e) => !!e)
|
|
579
|
+
)
|
|
580
|
+
];
|
|
581
|
+
const name = args2.name ?? (events.length === 1 ? eventName(events[0]) : void 0);
|
|
582
|
+
const result = (0, import_sdk.generateRequestSchema)(matching, { format: args2.format, name });
|
|
583
|
+
if (result.samples === 0) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`None of the ${matching.length} matching requests has a JSON or form body to learn from.`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const output = {
|
|
589
|
+
format: result.format,
|
|
590
|
+
name: result.name,
|
|
591
|
+
samples: result.samples,
|
|
592
|
+
skipped: result.skipped,
|
|
593
|
+
...events.length > 0 ? { events } : {},
|
|
594
|
+
...events.length > 1 ? {
|
|
595
|
+
warning: `The samples mix ${events.length} events, so fields of one event show up as optional. Pass event for one schema per event.`
|
|
596
|
+
} : {},
|
|
597
|
+
code: result.code
|
|
598
|
+
};
|
|
599
|
+
const size = JSON.stringify(output, null, 2).length;
|
|
600
|
+
if (size > MAX_OUTPUT) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`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.`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return jsonContent(output);
|
|
606
|
+
}
|
|
496
607
|
function textContent(text) {
|
|
497
608
|
return { content: [{ type: "text", text }] };
|
|
498
609
|
}
|
|
@@ -744,6 +855,8 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
744
855
|
"tag",
|
|
745
856
|
"subject",
|
|
746
857
|
"from",
|
|
858
|
+
"fields",
|
|
859
|
+
"replyText",
|
|
747
860
|
"size",
|
|
748
861
|
"test",
|
|
749
862
|
"spam",
|
|
@@ -753,6 +866,11 @@ var ESSENTIAL_DETAIL_FIELDS = [
|
|
|
753
866
|
"html",
|
|
754
867
|
"htmlTruncated"
|
|
755
868
|
];
|
|
869
|
+
function shrinkRendered(rendered, max) {
|
|
870
|
+
const body = typeof rendered.body === "string" ? rendered.body : "";
|
|
871
|
+
if (body.length <= max - 1024) return rendered;
|
|
872
|
+
return { ...rendered, body: body.slice(0, max - 1024), bodyTruncated: true };
|
|
873
|
+
}
|
|
756
874
|
function emailDetail(client, email, view, includeHtml) {
|
|
757
875
|
const { data } = client.emails.toJson(email, {
|
|
758
876
|
endpoint: view.endpoint,
|
|
@@ -915,7 +1033,7 @@ function registerTools(server, client) {
|
|
|
915
1033
|
);
|
|
916
1034
|
server.tool(
|
|
917
1035
|
"update_endpoint",
|
|
918
|
-
"Update an endpoint name, mock response, conditional response rules, signing configuration,
|
|
1036
|
+
"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.",
|
|
919
1037
|
{
|
|
920
1038
|
slug: import_zod2.z.string().describe("The endpoint slug to update"),
|
|
921
1039
|
name: import_zod2.z.string().optional().describe("New display name"),
|
|
@@ -929,7 +1047,17 @@ function registerTools(server, client) {
|
|
|
929
1047
|
signingHeader: import_zod2.z.string().nullable().optional().describe(
|
|
930
1048
|
"Custom signature header name (only for generic-hmac provider), or null to clear"
|
|
931
1049
|
),
|
|
932
|
-
showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
|
|
1050
|
+
showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
|
|
1051
|
+
emailFields: import_zod2.z.array(
|
|
1052
|
+
import_zod2.z.object({
|
|
1053
|
+
name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
|
|
1054
|
+
from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
|
|
1055
|
+
header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
|
|
1056
|
+
pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
|
|
1057
|
+
})
|
|
1058
|
+
).max(20).nullable().optional().describe(
|
|
1059
|
+
"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."
|
|
1060
|
+
)
|
|
933
1061
|
},
|
|
934
1062
|
withErrorHandling(
|
|
935
1063
|
async ({
|
|
@@ -941,7 +1069,8 @@ function registerTools(server, client) {
|
|
|
941
1069
|
signingProvider,
|
|
942
1070
|
signingSecret,
|
|
943
1071
|
signingHeader,
|
|
944
|
-
showEmailExtracts
|
|
1072
|
+
showEmailExtracts,
|
|
1073
|
+
emailFields
|
|
945
1074
|
}) => {
|
|
946
1075
|
const endpoint = await client.endpoints.update(slug, {
|
|
947
1076
|
name,
|
|
@@ -951,12 +1080,33 @@ function registerTools(server, client) {
|
|
|
951
1080
|
signingProvider,
|
|
952
1081
|
signingSecret,
|
|
953
1082
|
signingHeader,
|
|
954
|
-
showEmailExtracts
|
|
1083
|
+
showEmailExtracts,
|
|
1084
|
+
emailFields
|
|
955
1085
|
});
|
|
956
1086
|
return jsonContent(endpoint);
|
|
957
1087
|
}
|
|
958
1088
|
)
|
|
959
1089
|
);
|
|
1090
|
+
server.tool(
|
|
1091
|
+
"preview_response",
|
|
1092
|
+
"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.",
|
|
1093
|
+
{
|
|
1094
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1095
|
+
requestId: import_zod2.z.string().optional().describe(
|
|
1096
|
+
"A captured request on the endpoint (default: the newest HTTP request, or a sample)"
|
|
1097
|
+
),
|
|
1098
|
+
mockResponse: mockResponseSchema.nullable().optional().describe("Try this default response instead of the saved one (null: none)"),
|
|
1099
|
+
responseRules: responseRulesSchema.nullable().optional().describe("Try these rules instead of the saved ones (null: none)")
|
|
1100
|
+
},
|
|
1101
|
+
withErrorHandling(async ({ slug, requestId, mockResponse, responseRules }) => {
|
|
1102
|
+
const preview = await client.endpoints.previewResponse(slug, {
|
|
1103
|
+
...requestId ? { requestId } : {},
|
|
1104
|
+
...mockResponse !== void 0 ? { mockResponse } : {},
|
|
1105
|
+
...responseRules !== void 0 ? { responseRules } : {}
|
|
1106
|
+
});
|
|
1107
|
+
return jsonContent(preview);
|
|
1108
|
+
})
|
|
1109
|
+
);
|
|
960
1110
|
server.tool(
|
|
961
1111
|
"delete_endpoint",
|
|
962
1112
|
"Delete a webhook endpoint and all its captured requests.",
|
|
@@ -1172,6 +1322,39 @@ function registerTools(server, client) {
|
|
|
1172
1322
|
});
|
|
1173
1323
|
})
|
|
1174
1324
|
);
|
|
1325
|
+
server.tool(
|
|
1326
|
+
"render_email",
|
|
1327
|
+
"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.",
|
|
1328
|
+
{
|
|
1329
|
+
requestId: import_zod2.z.string().describe("The captured email's request ID"),
|
|
1330
|
+
format: import_zod2.z.enum(["postmark", "sendgrid", "mailgun", "cloudmailin", "json"]).describe("Whose shape; json is webhooks.cc's own email.received JSON (unsigned)"),
|
|
1331
|
+
mailgunSigningKey: import_zod2.z.string().optional().describe("For mailgun: the signing key your handler verifies with (a test key)"),
|
|
1332
|
+
targetUrl: httpUrlSchema.optional().describe("Post it here, e.g. http://localhost:3000/api/inbound/postmark")
|
|
1333
|
+
},
|
|
1334
|
+
withErrorHandling(async ({ requestId, format, mailgunSigningKey, targetUrl }) => {
|
|
1335
|
+
const rendered = await client.emails.render(requestId, {
|
|
1336
|
+
format,
|
|
1337
|
+
...mailgunSigningKey ? { mailgunSigningKey } : {}
|
|
1338
|
+
});
|
|
1339
|
+
if (!targetUrl) {
|
|
1340
|
+
return jsonContent(
|
|
1341
|
+
shrinkRendered(rendered, MAX_BODY_SIZE)
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
const response = await fetch(targetUrl, {
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
headers: { "content-type": rendered.headers["content-type"] },
|
|
1347
|
+
body: rendered.body,
|
|
1348
|
+
redirect: "manual"
|
|
1349
|
+
});
|
|
1350
|
+
return jsonContent({
|
|
1351
|
+
format,
|
|
1352
|
+
status: response.status,
|
|
1353
|
+
statusText: response.statusText,
|
|
1354
|
+
body: await readBodyTruncated(response)
|
|
1355
|
+
});
|
|
1356
|
+
})
|
|
1357
|
+
);
|
|
1175
1358
|
server.tool(
|
|
1176
1359
|
"compare_requests",
|
|
1177
1360
|
"Compare two captured requests and show the structured differences.",
|
|
@@ -1189,6 +1372,15 @@ function registerTools(server, client) {
|
|
|
1189
1372
|
return jsonContent(diff);
|
|
1190
1373
|
})
|
|
1191
1374
|
);
|
|
1375
|
+
server.tool(
|
|
1376
|
+
"generate_payload_schema",
|
|
1377
|
+
"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.",
|
|
1378
|
+
payloadSchemaShape,
|
|
1379
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args2 }) => {
|
|
1380
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => client.requests.get(id))) : await client.requests.list(endpointSlug, { limit, kind: "http" });
|
|
1381
|
+
return payloadSchemaResult(requests, args2);
|
|
1382
|
+
})
|
|
1383
|
+
);
|
|
1192
1384
|
server.tool(
|
|
1193
1385
|
"extract_from_request",
|
|
1194
1386
|
"Extract specific JSON fields from a captured request body.",
|
|
@@ -1546,9 +1738,79 @@ function registerTools(server, client) {
|
|
|
1546
1738
|
return jsonContent({ ...result, address: client.emails.address(slug, tag) });
|
|
1547
1739
|
})
|
|
1548
1740
|
);
|
|
1741
|
+
server.tool(
|
|
1742
|
+
"list_destinations",
|
|
1743
|
+
"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.",
|
|
1744
|
+
{
|
|
1745
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1746
|
+
summary: import_zod2.z.boolean().default(true).describe("Add each destination's last 24 hours")
|
|
1747
|
+
},
|
|
1748
|
+
withErrorHandling(async ({ slug, summary }) => {
|
|
1749
|
+
return jsonContent(await client.destinations.list(slug, { summary }));
|
|
1750
|
+
})
|
|
1751
|
+
);
|
|
1752
|
+
server.tool(
|
|
1753
|
+
"add_destination",
|
|
1754
|
+
"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.",
|
|
1755
|
+
{
|
|
1756
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1757
|
+
url: httpUrlSchema.describe("https URL on a public host name, or a Slack or Discord webhook"),
|
|
1758
|
+
...destinationFields
|
|
1759
|
+
},
|
|
1760
|
+
withErrorHandling(async ({ slug, ...input }) => {
|
|
1761
|
+
return jsonContent(await client.destinations.create(slug, input));
|
|
1762
|
+
})
|
|
1763
|
+
);
|
|
1764
|
+
server.tool(
|
|
1765
|
+
"update_destination",
|
|
1766
|
+
"Change a destination: any of its fields; only the ones you pass change. Owner only.",
|
|
1767
|
+
{
|
|
1768
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1769
|
+
destinationId: import_zod2.z.string().describe("The destination's id (from list_destinations)"),
|
|
1770
|
+
url: httpUrlSchema.optional().describe("A new URL"),
|
|
1771
|
+
...destinationFields
|
|
1772
|
+
},
|
|
1773
|
+
withErrorHandling(async ({ slug, destinationId, ...input }) => {
|
|
1774
|
+
return jsonContent(await client.destinations.update(slug, destinationId, input));
|
|
1775
|
+
})
|
|
1776
|
+
);
|
|
1777
|
+
server.tool(
|
|
1778
|
+
"remove_destination",
|
|
1779
|
+
"Remove a destination. Deliveries still waiting for it are marked failed; its log stays. Owner only.",
|
|
1780
|
+
{
|
|
1781
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1782
|
+
destinationId: import_zod2.z.string().describe("The destination's id")
|
|
1783
|
+
},
|
|
1784
|
+
withErrorHandling(async ({ slug, destinationId }) => {
|
|
1785
|
+
await client.destinations.delete(slug, destinationId);
|
|
1786
|
+
return textContent(`Destination ${destinationId} removed.`);
|
|
1787
|
+
})
|
|
1788
|
+
);
|
|
1789
|
+
server.tool(
|
|
1790
|
+
"preview_destination",
|
|
1791
|
+
"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.",
|
|
1792
|
+
{
|
|
1793
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1794
|
+
destinationId: import_zod2.z.string().optional().describe("A saved destination"),
|
|
1795
|
+
destination: import_zod2.z.object({ url: httpUrlSchema.optional(), ...destinationFields }).optional().describe("Unsaved fields to try"),
|
|
1796
|
+
requestId: import_zod2.z.string().optional().describe("A captured request (default: the newest of the kind it takes, or a sample)")
|
|
1797
|
+
},
|
|
1798
|
+
withErrorHandling(async ({ slug, destinationId, destination, requestId }) => {
|
|
1799
|
+
if (!destinationId && !destination?.url) {
|
|
1800
|
+
throw new Error("Pass destinationId, or a destination with a url");
|
|
1801
|
+
}
|
|
1802
|
+
return jsonContent(
|
|
1803
|
+
await client.destinations.preview(slug, {
|
|
1804
|
+
...destinationId ? { destinationId } : {},
|
|
1805
|
+
...destination ? { destination } : {},
|
|
1806
|
+
...requestId ? { requestId } : {}
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
})
|
|
1810
|
+
);
|
|
1549
1811
|
server.tool(
|
|
1550
1812
|
"configure_forwarding",
|
|
1551
|
-
"Change an endpoint's forwarding: the URL, on or off, which kinds
|
|
1813
|
+
"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.",
|
|
1552
1814
|
{
|
|
1553
1815
|
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1554
1816
|
url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
|
|
@@ -1596,23 +1858,32 @@ function registerTools(server, client) {
|
|
|
1596
1858
|
"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.",
|
|
1597
1859
|
{
|
|
1598
1860
|
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1861
|
+
destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
|
|
1599
1862
|
rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
|
|
1600
1863
|
},
|
|
1601
|
-
withErrorHandling(async ({ slug, rotate }) => {
|
|
1602
|
-
const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1864
|
+
withErrorHandling(async ({ slug, destinationId, rotate }) => {
|
|
1865
|
+
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);
|
|
1603
1866
|
return jsonContent({ secret, rotated: rotate });
|
|
1604
1867
|
})
|
|
1605
1868
|
);
|
|
1606
1869
|
server.tool(
|
|
1607
1870
|
"test_forwarding",
|
|
1608
|
-
"Send the endpoint's newest captured request of a kind (or a sample) to
|
|
1871
|
+
"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.",
|
|
1609
1872
|
{
|
|
1610
1873
|
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
)
|
|
1874
|
+
destinationId: import_zod2.z.string().optional().describe("A destination (default: the endpoint's primary destination)"),
|
|
1875
|
+
kind: import_zod2.z.enum(["http", "email"]).optional().describe("Which kind to send (default: http when the destination takes HTTP requests)"),
|
|
1876
|
+
requestId: import_zod2.z.string().optional().describe("With destinationId: a captured request to send")
|
|
1614
1877
|
},
|
|
1615
|
-
withErrorHandling(async ({ slug, kind }) => {
|
|
1878
|
+
withErrorHandling(async ({ slug, destinationId, kind, requestId }) => {
|
|
1879
|
+
if (destinationId) {
|
|
1880
|
+
return jsonContent(
|
|
1881
|
+
await client.destinations.test(slug, destinationId, {
|
|
1882
|
+
...kind ? { kind } : {},
|
|
1883
|
+
...requestId ? { requestId } : {}
|
|
1884
|
+
})
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1616
1887
|
return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
|
|
1617
1888
|
})
|
|
1618
1889
|
);
|
|
@@ -1623,17 +1894,25 @@ function registerTools(server, client) {
|
|
|
1623
1894
|
endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's delivery log"),
|
|
1624
1895
|
requestId: import_zod2.z.string().optional().describe("List every delivery and try of this request"),
|
|
1625
1896
|
limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
|
|
1626
|
-
status: import_zod2.z.enum(["all", "pending", "failed"]).default("all").describe(
|
|
1897
|
+
status: import_zod2.z.enum(["all", "pending", "failed", "skipped"]).default("all").describe(
|
|
1898
|
+
"With endpointSlug: all, pending (queued or retrying), failed or skipped (filtered out)"
|
|
1899
|
+
),
|
|
1900
|
+
destinationId: import_zod2.z.string().optional().describe("With endpointSlug: only this destination"),
|
|
1627
1901
|
before: import_zod2.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
|
|
1628
1902
|
},
|
|
1629
|
-
withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
|
|
1903
|
+
withErrorHandling(async ({ endpointSlug, requestId, limit, status, destinationId, before }) => {
|
|
1630
1904
|
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1631
1905
|
throw new Error("Pass exactly one of endpointSlug or requestId");
|
|
1632
1906
|
}
|
|
1633
1907
|
if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
|
|
1634
1908
|
const [summary, deliveries] = await Promise.all([
|
|
1635
1909
|
client.forwarding.summary(endpointSlug),
|
|
1636
|
-
client.forwarding.deliveries(endpointSlug, {
|
|
1910
|
+
client.forwarding.deliveries(endpointSlug, {
|
|
1911
|
+
limit,
|
|
1912
|
+
status,
|
|
1913
|
+
before,
|
|
1914
|
+
...destinationId ? { destinationId } : {}
|
|
1915
|
+
})
|
|
1637
1916
|
]);
|
|
1638
1917
|
return jsonContent({
|
|
1639
1918
|
summary,
|
|
@@ -1644,17 +1923,19 @@ function registerTools(server, client) {
|
|
|
1644
1923
|
);
|
|
1645
1924
|
server.tool(
|
|
1646
1925
|
"redeliver",
|
|
1647
|
-
"Forward again with the
|
|
1926
|
+
"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.",
|
|
1648
1927
|
{
|
|
1649
1928
|
requestId: import_zod2.z.string().optional().describe("The request or email to send again"),
|
|
1650
|
-
endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again")
|
|
1929
|
+
endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again"),
|
|
1930
|
+
destinationId: import_zod2.z.string().optional().describe("Only this destination")
|
|
1651
1931
|
},
|
|
1652
|
-
withErrorHandling(async ({ requestId, endpointSlug }) => {
|
|
1932
|
+
withErrorHandling(async ({ requestId, endpointSlug, destinationId }) => {
|
|
1653
1933
|
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1654
1934
|
throw new Error("Pass exactly one of requestId or endpointSlug");
|
|
1655
1935
|
}
|
|
1936
|
+
const options = destinationId ? { destinationId } : {};
|
|
1656
1937
|
return jsonContent(
|
|
1657
|
-
requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
|
|
1938
|
+
requestId ? await client.forwarding.redeliver(requestId, options) : await client.forwarding.redeliverFailed(endpointSlug, options)
|
|
1658
1939
|
);
|
|
1659
1940
|
})
|
|
1660
1941
|
);
|
|
@@ -1879,6 +2160,18 @@ function registerSandboxTools(server, options = {}) {
|
|
|
1879
2160
|
})
|
|
1880
2161
|
)
|
|
1881
2162
|
);
|
|
2163
|
+
registered.push(
|
|
2164
|
+
server.tool(
|
|
2165
|
+
"generate_payload_schema",
|
|
2166
|
+
"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.",
|
|
2167
|
+
payloadSchemaShape,
|
|
2168
|
+
withErrorHandling(async ({ endpointSlug, requestIds, limit, ...args2 }) => {
|
|
2169
|
+
const sandbox = await session.requireSandbox();
|
|
2170
|
+
const requests = requestIds ? await Promise.all(requestIds.map((id) => sandbox.requests.get(id))) : await sandbox.requests.list(endpointSlug, { limit });
|
|
2171
|
+
return payloadSchemaResult(requests, args2);
|
|
2172
|
+
})
|
|
2173
|
+
)
|
|
2174
|
+
);
|
|
1882
2175
|
registered.push(
|
|
1883
2176
|
server.tool(
|
|
1884
2177
|
"connect_account",
|
|
@@ -2034,7 +2327,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
2034
2327
|
}
|
|
2035
2328
|
|
|
2036
2329
|
// src/index.ts
|
|
2037
|
-
var VERSION = true ? "1.
|
|
2330
|
+
var VERSION = true ? "1.14.0" : "0.0.0-dev";
|
|
2038
2331
|
function createServer(options = {}) {
|
|
2039
2332
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2040
2333
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
|
@@ -2255,7 +2548,7 @@ function runSetup(args2) {
|
|
|
2255
2548
|
const apiKey = resolveApiKey(flags);
|
|
2256
2549
|
if (!apiKey) {
|
|
2257
2550
|
console.error("Error: API key required. Pass --api-key or set WHK_API_KEY env var.");
|
|
2258
|
-
console.error("Get your API key at https://webhooks.cc/account\n");
|
|
2551
|
+
console.error("Get your API key at https://webhooks.cc/account/api-keys\n");
|
|
2259
2552
|
process.exit(1);
|
|
2260
2553
|
}
|
|
2261
2554
|
validateApiKeyFormat(apiKey);
|
|
@@ -2310,7 +2603,7 @@ Setup:
|
|
|
2310
2603
|
npx @webhooks-cc/mcp setup windsurf --api-key whcc_...
|
|
2311
2604
|
npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
|
|
2312
2605
|
|
|
2313
|
-
Get your API key at https://webhooks.cc/account
|
|
2606
|
+
Get your API key at https://webhooks.cc/account/api-keys
|
|
2314
2607
|
`);
|
|
2315
2608
|
} else if (command && command !== "--") {
|
|
2316
2609
|
console.error(`Unknown command: "${command}". Run with --help for usage.`);
|