@webhooks-cc/mcp 0.3.1 → 1.0.1
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 +126 -38
- package/dist/bin/mcp.js +818 -106
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +822 -106
- package/dist/index.mjs +828 -105
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -21,102 +21,590 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
createServer: () => createServer,
|
|
24
|
+
registerPrompts: () => registerPrompts,
|
|
25
|
+
registerResources: () => registerResources,
|
|
24
26
|
registerTools: () => registerTools
|
|
25
27
|
});
|
|
26
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
30
|
+
var import_sdk2 = require("@webhooks-cc/sdk");
|
|
31
|
+
|
|
32
|
+
// src/prompts.ts
|
|
33
|
+
var import_zod = require("zod");
|
|
34
|
+
function registerPrompts(server) {
|
|
35
|
+
server.registerPrompt(
|
|
36
|
+
"debug_webhook_delivery",
|
|
37
|
+
{
|
|
38
|
+
title: "Debug Webhook Delivery",
|
|
39
|
+
description: "Guide an agent through diagnosing why webhook delivery is failing or missing.",
|
|
40
|
+
argsSchema: {
|
|
41
|
+
provider: import_zod.z.string().optional().describe("Webhook provider, if known"),
|
|
42
|
+
endpointSlug: import_zod.z.string().optional().describe("Hosted endpoint slug, if known"),
|
|
43
|
+
targetUrl: import_zod.z.string().optional().describe("Your app's receiving URL, if known")
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
async ({ provider, endpointSlug, targetUrl }) => {
|
|
47
|
+
const scope = [
|
|
48
|
+
provider ? `Provider: ${provider}.` : null,
|
|
49
|
+
endpointSlug ? `Endpoint slug: ${endpointSlug}.` : null,
|
|
50
|
+
targetUrl ? `Target URL: ${targetUrl}.` : null
|
|
51
|
+
].filter(Boolean).join(" ");
|
|
52
|
+
return {
|
|
53
|
+
description: "Diagnose a missing or broken webhook delivery.",
|
|
54
|
+
messages: [
|
|
55
|
+
{
|
|
56
|
+
role: "user",
|
|
57
|
+
content: {
|
|
58
|
+
type: "text",
|
|
59
|
+
text: [
|
|
60
|
+
scope,
|
|
61
|
+
"Diagnose webhook delivery step by step.",
|
|
62
|
+
"Use list_endpoints or get_endpoint to confirm the endpoint and URL.",
|
|
63
|
+
"Use list_requests, wait_for_request, or wait_for_requests to check whether anything arrived.",
|
|
64
|
+
"If the provider is known, use preview_webhook or send_webhook to reproduce the webhook with realistic signing.",
|
|
65
|
+
"Use verify_signature when a secret is available.",
|
|
66
|
+
"Use compare_requests to compare retries or changed payloads.",
|
|
67
|
+
"Conclude with the most likely cause and the next concrete fix."
|
|
68
|
+
].filter(Boolean).join(" ")
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
server.registerPrompt(
|
|
76
|
+
"setup_provider_testing",
|
|
77
|
+
{
|
|
78
|
+
title: "Setup Provider Testing",
|
|
79
|
+
description: "Guide an agent through setting up webhook testing for a provider.",
|
|
80
|
+
argsSchema: {
|
|
81
|
+
provider: import_zod.z.string().describe("Webhook provider to test"),
|
|
82
|
+
targetUrl: import_zod.z.string().optional().describe("Optional local or remote target URL")
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async ({ provider, targetUrl }) => {
|
|
86
|
+
return {
|
|
87
|
+
description: `Set up webhook testing for ${provider}.`,
|
|
88
|
+
messages: [
|
|
89
|
+
{
|
|
90
|
+
role: "user",
|
|
91
|
+
content: {
|
|
92
|
+
type: "text",
|
|
93
|
+
text: [
|
|
94
|
+
`Set up webhook testing for ${provider}.`,
|
|
95
|
+
"Use list_provider_templates to inspect supported templates and signing details first.",
|
|
96
|
+
"Create an endpoint with create_endpoint, preferably ephemeral.",
|
|
97
|
+
"If a target URL is provided, use preview_webhook before send_to so the request shape is visible.",
|
|
98
|
+
targetUrl ? `Target URL: ${targetUrl}.` : null,
|
|
99
|
+
"Send a realistic provider webhook, wait for capture, and verify the signature if a secret is available.",
|
|
100
|
+
"Return the endpoint URL, the exact tools used, and the next step for the developer."
|
|
101
|
+
].filter(Boolean).join(" ")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
server.registerPrompt(
|
|
109
|
+
"compare_webhook_attempts",
|
|
110
|
+
{
|
|
111
|
+
title: "Compare Webhook Attempts",
|
|
112
|
+
description: "Guide an agent through comparing two webhook deliveries or retries.",
|
|
113
|
+
argsSchema: {
|
|
114
|
+
endpointSlug: import_zod.z.string().optional().describe("Endpoint slug to inspect for recent attempts"),
|
|
115
|
+
leftRequestId: import_zod.z.string().optional().describe("First request ID, if already known"),
|
|
116
|
+
rightRequestId: import_zod.z.string().optional().describe("Second request ID, if already known")
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async ({ endpointSlug, leftRequestId, rightRequestId }) => {
|
|
120
|
+
return {
|
|
121
|
+
description: "Compare two webhook deliveries and explain the difference.",
|
|
122
|
+
messages: [
|
|
123
|
+
{
|
|
124
|
+
role: "user",
|
|
125
|
+
content: {
|
|
126
|
+
type: "text",
|
|
127
|
+
text: [
|
|
128
|
+
endpointSlug ? `Endpoint slug: ${endpointSlug}.` : null,
|
|
129
|
+
leftRequestId && rightRequestId ? `Compare request ${leftRequestId} against ${rightRequestId}.` : "Find the most relevant two webhook attempts first.",
|
|
130
|
+
"Use compare_requests for the structured diff.",
|
|
131
|
+
"If request IDs are not provided, use list_requests or the endpoint recent resource to find the last two attempts.",
|
|
132
|
+
"Explain what changed in the body, headers, or timing, and whether the difference looks expected, retried, or broken."
|
|
133
|
+
].filter(Boolean).join(" ")
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/resources.ts
|
|
27
143
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
28
|
-
var
|
|
144
|
+
var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
|
|
145
|
+
var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
|
|
146
|
+
var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
|
|
147
|
+
var MAX_ENDPOINT_RESOURCE_ITEMS = 25;
|
|
148
|
+
var RECENT_REQUEST_LIMIT = 10;
|
|
149
|
+
function jsonResource(uri, value) {
|
|
150
|
+
return {
|
|
151
|
+
contents: [
|
|
152
|
+
{
|
|
153
|
+
uri,
|
|
154
|
+
mimeType: "application/json",
|
|
155
|
+
text: JSON.stringify(value, null, 2)
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function summarizeRequest(request) {
|
|
161
|
+
return {
|
|
162
|
+
id: request.id,
|
|
163
|
+
method: request.method,
|
|
164
|
+
path: request.path,
|
|
165
|
+
receivedAt: request.receivedAt,
|
|
166
|
+
contentType: request.contentType ?? null
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function variableToString(value, name) {
|
|
170
|
+
if (typeof value === "string" && value.length > 0) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`Missing resource variable "${name}"`);
|
|
174
|
+
}
|
|
175
|
+
async function listEndpointSummaries(client) {
|
|
176
|
+
const endpoints = (await client.endpoints.list()).slice().sort((left, right) => right.createdAt - left.createdAt);
|
|
177
|
+
const truncated = endpoints.length > MAX_ENDPOINT_RESOURCE_ITEMS;
|
|
178
|
+
const visible = endpoints.slice(0, MAX_ENDPOINT_RESOURCE_ITEMS);
|
|
179
|
+
const summaries = await Promise.all(
|
|
180
|
+
visible.map(async (endpoint) => {
|
|
181
|
+
const recent = await client.requests.list(endpoint.slug, { limit: 1 });
|
|
182
|
+
return {
|
|
183
|
+
...endpoint,
|
|
184
|
+
lastRequest: recent[0] ? summarizeRequest(recent[0]) : null
|
|
185
|
+
};
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
return {
|
|
189
|
+
endpoints: summaries,
|
|
190
|
+
total: endpoints.length,
|
|
191
|
+
truncated
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function registerResources(server, client) {
|
|
195
|
+
server.registerResource(
|
|
196
|
+
"endpoints-overview",
|
|
197
|
+
ENDPOINTS_RESOURCE_URI,
|
|
198
|
+
{
|
|
199
|
+
title: "Endpoints Overview",
|
|
200
|
+
description: "All endpoints with a summary of recent activity.",
|
|
201
|
+
mimeType: "application/json"
|
|
202
|
+
},
|
|
203
|
+
async () => {
|
|
204
|
+
return jsonResource(ENDPOINTS_RESOURCE_URI, await listEndpointSummaries(client));
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
server.registerResource(
|
|
208
|
+
"endpoint-recent-requests",
|
|
209
|
+
new import_mcp.ResourceTemplate(ENDPOINT_RECENT_TEMPLATE_URI, {
|
|
210
|
+
list: async () => {
|
|
211
|
+
const endpoints = await client.endpoints.list();
|
|
212
|
+
return {
|
|
213
|
+
resources: endpoints.map((endpoint) => ({
|
|
214
|
+
uri: `webhooks://endpoint/${endpoint.slug}/recent`,
|
|
215
|
+
name: `${endpoint.slug} recent requests`,
|
|
216
|
+
description: `Recent requests for endpoint ${endpoint.slug}`,
|
|
217
|
+
mimeType: "application/json"
|
|
218
|
+
}))
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
complete: {
|
|
222
|
+
slug: async (value) => {
|
|
223
|
+
const endpoints = await client.endpoints.list();
|
|
224
|
+
return endpoints.map((endpoint) => endpoint.slug).filter((slug) => slug.startsWith(value)).slice(0, 20);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}),
|
|
228
|
+
{
|
|
229
|
+
title: "Endpoint Recent Requests",
|
|
230
|
+
description: "Last 10 captured requests for an endpoint.",
|
|
231
|
+
mimeType: "application/json"
|
|
232
|
+
},
|
|
233
|
+
async (uri, variables) => {
|
|
234
|
+
const slug = variableToString(variables.slug, "slug");
|
|
235
|
+
const [endpoint, requests] = await Promise.all([
|
|
236
|
+
client.endpoints.get(slug),
|
|
237
|
+
client.requests.list(slug, { limit: RECENT_REQUEST_LIMIT })
|
|
238
|
+
]);
|
|
239
|
+
return jsonResource(uri.toString(), {
|
|
240
|
+
endpoint,
|
|
241
|
+
requests
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
server.registerResource(
|
|
246
|
+
"request-details",
|
|
247
|
+
new import_mcp.ResourceTemplate(REQUEST_TEMPLATE_URI, {
|
|
248
|
+
list: void 0,
|
|
249
|
+
complete: {}
|
|
250
|
+
}),
|
|
251
|
+
{
|
|
252
|
+
title: "Request Details",
|
|
253
|
+
description: "Full details for a captured request by ID.",
|
|
254
|
+
mimeType: "application/json"
|
|
255
|
+
},
|
|
256
|
+
async (uri, variables) => {
|
|
257
|
+
const id = variableToString(variables.id, "id");
|
|
258
|
+
const request = await client.requests.get(id);
|
|
259
|
+
return jsonResource(uri.toString(), request);
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
29
263
|
|
|
30
264
|
// src/tools.ts
|
|
31
|
-
var
|
|
265
|
+
var import_zod2 = require("zod");
|
|
266
|
+
var import_sdk = require("@webhooks-cc/sdk");
|
|
32
267
|
var MAX_BODY_SIZE = 32768;
|
|
268
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
269
|
+
var TEMPLATE_PROVIDER_VALUES = [
|
|
270
|
+
"stripe",
|
|
271
|
+
"github",
|
|
272
|
+
"shopify",
|
|
273
|
+
"twilio",
|
|
274
|
+
"slack",
|
|
275
|
+
"paddle",
|
|
276
|
+
"linear",
|
|
277
|
+
"standard-webhooks"
|
|
278
|
+
];
|
|
279
|
+
var VERIFY_PROVIDER_VALUES = [
|
|
280
|
+
...TEMPLATE_PROVIDER_VALUES,
|
|
281
|
+
"discord"
|
|
282
|
+
];
|
|
283
|
+
var TIME_SEPARATOR = " \u2014 ";
|
|
284
|
+
var httpUrlSchema = import_zod2.z.string().url().refine(
|
|
285
|
+
(value) => {
|
|
286
|
+
try {
|
|
287
|
+
const protocol = new URL(value).protocol;
|
|
288
|
+
return protocol === "http:" || protocol === "https:";
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{ message: "Only http and https URLs are supported" }
|
|
294
|
+
);
|
|
295
|
+
var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
|
|
296
|
+
var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
|
|
297
|
+
var mockResponseSchema = import_zod2.z.object({
|
|
298
|
+
status: import_zod2.z.number().int().min(100).max(599).describe("HTTP status code (100-599)"),
|
|
299
|
+
body: import_zod2.z.string().default("").describe("Response body string (default: empty)"),
|
|
300
|
+
headers: import_zod2.z.record(import_zod2.z.string()).default({}).describe("Response headers (default: none)"),
|
|
301
|
+
delay: import_zod2.z.number().int().min(0).max(3e4).optional().describe("Response delay in milliseconds (0-30000, default: none)")
|
|
302
|
+
});
|
|
33
303
|
function textContent(text) {
|
|
34
304
|
return { content: [{ type: "text", text }] };
|
|
35
305
|
}
|
|
306
|
+
function serializeJson(value, limit = MAX_BODY_SIZE) {
|
|
307
|
+
const full = JSON.stringify(value, null, 2);
|
|
308
|
+
if (full.length <= limit) {
|
|
309
|
+
return full;
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
let low = 0;
|
|
313
|
+
let high = value.length;
|
|
314
|
+
let best = JSON.stringify(
|
|
315
|
+
{ items: [], truncated: true, total: value.length, returned: 0 },
|
|
316
|
+
null,
|
|
317
|
+
2
|
|
318
|
+
);
|
|
319
|
+
while (low <= high) {
|
|
320
|
+
const mid = Math.floor((low + high) / 2);
|
|
321
|
+
const candidate = JSON.stringify(
|
|
322
|
+
{
|
|
323
|
+
items: value.slice(0, mid),
|
|
324
|
+
truncated: true,
|
|
325
|
+
total: value.length,
|
|
326
|
+
returned: mid
|
|
327
|
+
},
|
|
328
|
+
null,
|
|
329
|
+
2
|
|
330
|
+
);
|
|
331
|
+
if (candidate.length <= limit) {
|
|
332
|
+
best = candidate;
|
|
333
|
+
low = mid + 1;
|
|
334
|
+
} else {
|
|
335
|
+
high = mid - 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return best;
|
|
339
|
+
}
|
|
340
|
+
return full.slice(0, limit) + `
|
|
341
|
+
... [truncated, ${full.length} chars total]`;
|
|
342
|
+
}
|
|
343
|
+
function jsonContent(value) {
|
|
344
|
+
return textContent(serializeJson(value));
|
|
345
|
+
}
|
|
36
346
|
async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
|
|
37
347
|
const text = await response.text();
|
|
38
348
|
if (text.length <= limit) return text;
|
|
39
349
|
return text.slice(0, limit) + `
|
|
40
350
|
... [truncated, ${text.length} chars total]`;
|
|
41
351
|
}
|
|
352
|
+
function sleep(ms) {
|
|
353
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
354
|
+
}
|
|
355
|
+
function splitHint(message) {
|
|
356
|
+
const separatorIndex = message.indexOf(TIME_SEPARATOR);
|
|
357
|
+
if (separatorIndex === -1) {
|
|
358
|
+
return { message, hint: null };
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
message: message.slice(0, separatorIndex),
|
|
362
|
+
hint: message.slice(separatorIndex + TIME_SEPARATOR.length) || null
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function serializeError(error) {
|
|
366
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
367
|
+
const { message, hint } = splitHint(rawMessage);
|
|
368
|
+
const payload = {
|
|
369
|
+
error: true,
|
|
370
|
+
code: "validation_error",
|
|
371
|
+
message,
|
|
372
|
+
hint,
|
|
373
|
+
retryAfter: null
|
|
374
|
+
};
|
|
375
|
+
if (error instanceof import_sdk.UnauthorizedError) {
|
|
376
|
+
payload.code = "unauthorized";
|
|
377
|
+
} else if (error instanceof import_sdk.NotFoundError) {
|
|
378
|
+
payload.code = "not_found";
|
|
379
|
+
} else if (error instanceof import_sdk.RateLimitError) {
|
|
380
|
+
payload.code = "rate_limited";
|
|
381
|
+
payload.retryAfter = error.retryAfter ?? null;
|
|
382
|
+
} else if (error instanceof import_sdk.TimeoutError) {
|
|
383
|
+
payload.code = "timeout";
|
|
384
|
+
} else if (error instanceof import_sdk.WebhooksCCError) {
|
|
385
|
+
payload.code = error.statusCode >= 500 ? "server_error" : "validation_error";
|
|
386
|
+
}
|
|
387
|
+
return JSON.stringify(payload, null, 2);
|
|
388
|
+
}
|
|
42
389
|
function withErrorHandling(handler) {
|
|
43
390
|
return async (args) => {
|
|
44
391
|
try {
|
|
45
392
|
return await handler(args);
|
|
46
393
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
return { ...textContent(`Error: ${message}`), isError: true };
|
|
394
|
+
return { ...textContent(serializeError(error)), isError: true };
|
|
49
395
|
}
|
|
50
396
|
};
|
|
51
397
|
}
|
|
398
|
+
function filterRequestsByMethod(requests, method) {
|
|
399
|
+
if (!method) {
|
|
400
|
+
return requests;
|
|
401
|
+
}
|
|
402
|
+
const target = method.toUpperCase();
|
|
403
|
+
return requests.filter((request) => request.method.toUpperCase() === target);
|
|
404
|
+
}
|
|
405
|
+
async function waitForMultipleRequests(client, endpointSlug, options) {
|
|
406
|
+
const timeoutMs = typeof options.timeout === "number" ? options.timeout : options.timeout ? Number.isNaN(Number(options.timeout)) ? parseDurationLike(options.timeout) : Number(options.timeout) : 3e4;
|
|
407
|
+
const pollIntervalMs = typeof options.pollInterval === "number" ? options.pollInterval : options.pollInterval ? Number.isNaN(Number(options.pollInterval)) ? parseDurationLike(options.pollInterval) : Number(options.pollInterval) : 500;
|
|
408
|
+
const startedAt = Date.now();
|
|
409
|
+
let since = startedAt;
|
|
410
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
411
|
+
const requests = [];
|
|
412
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
413
|
+
const checkTime = Date.now();
|
|
414
|
+
const page = await client.requests.list(endpointSlug, {
|
|
415
|
+
since,
|
|
416
|
+
limit: Math.max(100, options.count * 5)
|
|
417
|
+
});
|
|
418
|
+
since = checkTime;
|
|
419
|
+
const filtered = filterRequestsByMethod(page, options.method).slice().sort((left, right) => left.receivedAt - right.receivedAt);
|
|
420
|
+
for (const request of filtered) {
|
|
421
|
+
if (seenIds.has(request.id)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
seenIds.add(request.id);
|
|
425
|
+
requests.push(request);
|
|
426
|
+
if (requests.length >= options.count) {
|
|
427
|
+
return {
|
|
428
|
+
requests,
|
|
429
|
+
complete: true,
|
|
430
|
+
timedOut: false,
|
|
431
|
+
expectedCount: options.count
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await sleep(Math.max(10, pollIntervalMs));
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
requests,
|
|
439
|
+
complete: requests.length >= options.count,
|
|
440
|
+
timedOut: true,
|
|
441
|
+
expectedCount: options.count
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function parseDurationLike(value) {
|
|
445
|
+
const trimmed = value.trim();
|
|
446
|
+
if (trimmed.length === 0) {
|
|
447
|
+
throw new Error("Duration value cannot be empty");
|
|
448
|
+
}
|
|
449
|
+
const numeric = Number(trimmed);
|
|
450
|
+
if (!Number.isNaN(numeric)) {
|
|
451
|
+
return numeric;
|
|
452
|
+
}
|
|
453
|
+
const match = trimmed.match(/^(\d+)\s*(ms|s|m|h|d)$/i);
|
|
454
|
+
if (!match) {
|
|
455
|
+
throw new Error(`Invalid duration: "${value}"`);
|
|
456
|
+
}
|
|
457
|
+
const amount = Number(match[1]);
|
|
458
|
+
const unit = match[2].toLowerCase();
|
|
459
|
+
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
460
|
+
return amount * multiplier;
|
|
461
|
+
}
|
|
462
|
+
function ensureVerifyArgs(args) {
|
|
463
|
+
if (args.provider === "discord") {
|
|
464
|
+
const publicKey = args.publicKey?.trim();
|
|
465
|
+
if (!publicKey) {
|
|
466
|
+
throw new Error('verify_signature for provider "discord" requires publicKey');
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
provider: "discord",
|
|
470
|
+
publicKey
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const secret = args.secret?.trim();
|
|
474
|
+
if (!secret) {
|
|
475
|
+
throw new Error(`verify_signature for provider "${args.provider}" requires secret`);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
provider: args.provider,
|
|
479
|
+
secret,
|
|
480
|
+
...args.url ? { url: args.url } : {}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async function summarizeResponse(response) {
|
|
484
|
+
return {
|
|
485
|
+
status: response.status,
|
|
486
|
+
statusText: response.statusText,
|
|
487
|
+
body: await readBodyTruncated(response)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
52
490
|
function registerTools(server, client) {
|
|
53
491
|
server.tool(
|
|
54
492
|
"create_endpoint",
|
|
55
|
-
"Create a
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
493
|
+
"Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
|
|
494
|
+
{
|
|
495
|
+
name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
|
|
496
|
+
ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
|
|
497
|
+
expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"'),
|
|
498
|
+
mockResponse: mockResponseSchema.optional().describe("Optional mock response to return when the endpoint receives a request")
|
|
499
|
+
},
|
|
500
|
+
withErrorHandling(async ({ name, ephemeral, expiresIn, mockResponse }) => {
|
|
501
|
+
const endpoint = await client.endpoints.create({ name, ephemeral, expiresIn, mockResponse });
|
|
502
|
+
return jsonContent(endpoint);
|
|
60
503
|
})
|
|
61
504
|
);
|
|
62
505
|
server.tool(
|
|
63
506
|
"list_endpoints",
|
|
64
|
-
"List all webhook endpoints for the authenticated user.
|
|
507
|
+
"List all webhook endpoints for the authenticated user.",
|
|
65
508
|
{},
|
|
66
509
|
withErrorHandling(async () => {
|
|
67
510
|
const endpoints = await client.endpoints.list();
|
|
68
|
-
return
|
|
511
|
+
return jsonContent(endpoints);
|
|
69
512
|
})
|
|
70
513
|
);
|
|
71
514
|
server.tool(
|
|
72
515
|
"get_endpoint",
|
|
73
|
-
"Get details for a specific webhook endpoint by
|
|
74
|
-
{ slug:
|
|
516
|
+
"Get details for a specific webhook endpoint by slug.",
|
|
517
|
+
{ slug: import_zod2.z.string().describe("The endpoint slug") },
|
|
75
518
|
withErrorHandling(async ({ slug }) => {
|
|
76
519
|
const endpoint = await client.endpoints.get(slug);
|
|
77
|
-
return
|
|
520
|
+
return jsonContent(endpoint);
|
|
78
521
|
})
|
|
79
522
|
);
|
|
80
523
|
server.tool(
|
|
81
524
|
"update_endpoint",
|
|
82
|
-
"Update an endpoint
|
|
525
|
+
"Update an endpoint name or mock response configuration.",
|
|
83
526
|
{
|
|
84
|
-
slug:
|
|
85
|
-
name:
|
|
86
|
-
mockResponse:
|
|
87
|
-
status: import_zod.z.number().min(100).max(599).describe("HTTP status code (100-599)"),
|
|
88
|
-
body: import_zod.z.string().default("").describe("Response body string (default: empty)"),
|
|
89
|
-
headers: import_zod.z.record(import_zod.z.string()).default({}).describe("Response headers (default: none)")
|
|
90
|
-
}).nullable().optional().describe("Mock response config, or null to clear it")
|
|
527
|
+
slug: import_zod2.z.string().describe("The endpoint slug to update"),
|
|
528
|
+
name: import_zod2.z.string().optional().describe("New display name"),
|
|
529
|
+
mockResponse: mockResponseSchema.nullable().optional().describe("Mock response config, or null to clear it")
|
|
91
530
|
},
|
|
92
531
|
withErrorHandling(async ({ slug, name, mockResponse }) => {
|
|
93
532
|
const endpoint = await client.endpoints.update(slug, { name, mockResponse });
|
|
94
|
-
return
|
|
533
|
+
return jsonContent(endpoint);
|
|
95
534
|
})
|
|
96
535
|
);
|
|
97
536
|
server.tool(
|
|
98
537
|
"delete_endpoint",
|
|
99
538
|
"Delete a webhook endpoint and all its captured requests.",
|
|
100
|
-
{ slug:
|
|
539
|
+
{ slug: import_zod2.z.string().describe("The endpoint slug to delete") },
|
|
101
540
|
withErrorHandling(async ({ slug }) => {
|
|
102
541
|
await client.endpoints.delete(slug);
|
|
103
542
|
return textContent(`Endpoint "${slug}" deleted.`);
|
|
104
543
|
})
|
|
105
544
|
);
|
|
545
|
+
server.tool(
|
|
546
|
+
"create_endpoints",
|
|
547
|
+
"Create multiple webhook endpoints in one call.",
|
|
548
|
+
{
|
|
549
|
+
count: import_zod2.z.number().int().min(1).max(20).describe("Number of endpoints to create"),
|
|
550
|
+
namePrefix: import_zod2.z.string().optional().describe("Optional prefix for endpoint names"),
|
|
551
|
+
ephemeral: import_zod2.z.boolean().optional().describe("Create temporary endpoints that auto-expire"),
|
|
552
|
+
expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"')
|
|
553
|
+
},
|
|
554
|
+
withErrorHandling(async ({ count, namePrefix, ephemeral, expiresIn }) => {
|
|
555
|
+
const endpoints = await Promise.all(
|
|
556
|
+
Array.from(
|
|
557
|
+
{ length: count },
|
|
558
|
+
(_, index) => client.endpoints.create({
|
|
559
|
+
name: namePrefix ? `${namePrefix}-${index + 1}` : void 0,
|
|
560
|
+
ephemeral,
|
|
561
|
+
expiresIn
|
|
562
|
+
})
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
return jsonContent({ endpoints });
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
server.tool(
|
|
569
|
+
"delete_endpoints",
|
|
570
|
+
"Delete multiple webhook endpoints in one call.",
|
|
571
|
+
{
|
|
572
|
+
slugs: import_zod2.z.array(import_zod2.z.string()).min(1).max(100).describe("Endpoint slugs to delete")
|
|
573
|
+
},
|
|
574
|
+
withErrorHandling(async ({ slugs }) => {
|
|
575
|
+
const settled = await Promise.allSettled(
|
|
576
|
+
slugs.map(async (slug) => {
|
|
577
|
+
await client.endpoints.delete(slug);
|
|
578
|
+
return slug;
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
return jsonContent({
|
|
582
|
+
deleted: settled.filter(
|
|
583
|
+
(result) => result.status === "fulfilled"
|
|
584
|
+
).map((result) => result.value),
|
|
585
|
+
failed: settled.flatMap(
|
|
586
|
+
(result, index) => result.status === "rejected" ? [
|
|
587
|
+
{
|
|
588
|
+
slug: slugs[index],
|
|
589
|
+
message: result.reason instanceof Error ? result.reason.message : String(result.reason)
|
|
590
|
+
}
|
|
591
|
+
] : []
|
|
592
|
+
)
|
|
593
|
+
});
|
|
594
|
+
})
|
|
595
|
+
);
|
|
106
596
|
server.tool(
|
|
107
597
|
"send_webhook",
|
|
108
|
-
"Send a test webhook to
|
|
598
|
+
"Send a test webhook to a hosted endpoint. Supports provider templates and signing.",
|
|
109
599
|
{
|
|
110
|
-
slug:
|
|
111
|
-
method:
|
|
112
|
-
headers:
|
|
113
|
-
body:
|
|
114
|
-
provider:
|
|
115
|
-
template:
|
|
116
|
-
event:
|
|
117
|
-
secret:
|
|
118
|
-
"Shared secret for provider signature generation (required when provider is set)"
|
|
119
|
-
)
|
|
600
|
+
slug: import_zod2.z.string().describe("The endpoint slug to send to"),
|
|
601
|
+
method: methodSchema,
|
|
602
|
+
headers: import_zod2.z.record(import_zod2.z.string()).optional().describe("HTTP headers to include"),
|
|
603
|
+
body: import_zod2.z.unknown().optional().describe("Request body"),
|
|
604
|
+
provider: import_zod2.z.enum(TEMPLATE_PROVIDER_VALUES).optional().describe("Optional provider template to send with signed headers"),
|
|
605
|
+
template: import_zod2.z.string().optional().describe("Provider-specific template preset"),
|
|
606
|
+
event: import_zod2.z.string().optional().describe("Provider event or topic name"),
|
|
607
|
+
secret: import_zod2.z.string().optional().describe("Signing secret. Required when provider is set.")
|
|
120
608
|
},
|
|
121
609
|
withErrorHandling(
|
|
122
610
|
async ({ slug, method, headers, body, provider, template, event, secret }) => {
|
|
@@ -139,104 +627,204 @@ function registerTools(server, client) {
|
|
|
139
627
|
response = await client.endpoints.send(slug, { method, headers, body });
|
|
140
628
|
}
|
|
141
629
|
const responseBody = await readBodyTruncated(response);
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
);
|
|
630
|
+
return jsonContent({
|
|
631
|
+
status: response.status,
|
|
632
|
+
statusText: response.statusText,
|
|
633
|
+
body: responseBody
|
|
634
|
+
});
|
|
149
635
|
}
|
|
150
636
|
)
|
|
151
637
|
);
|
|
152
638
|
server.tool(
|
|
153
639
|
"list_requests",
|
|
154
|
-
"List captured
|
|
640
|
+
"List recent captured requests for an endpoint.",
|
|
155
641
|
{
|
|
156
|
-
endpointSlug:
|
|
157
|
-
limit:
|
|
158
|
-
since:
|
|
642
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
643
|
+
limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
|
|
644
|
+
since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms")
|
|
159
645
|
},
|
|
160
646
|
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
161
647
|
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
162
|
-
return
|
|
648
|
+
return jsonContent(requests);
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
server.tool(
|
|
652
|
+
"search_requests",
|
|
653
|
+
"Search captured webhook requests across endpoints using retained full-text search.",
|
|
654
|
+
{
|
|
655
|
+
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
656
|
+
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
657
|
+
q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
|
|
658
|
+
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
659
|
+
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
|
|
660
|
+
limit: import_zod2.z.number().int().min(1).max(200).default(50).describe("Max results to return"),
|
|
661
|
+
offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("Result offset"),
|
|
662
|
+
order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
|
|
663
|
+
},
|
|
664
|
+
withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
|
|
665
|
+
const results = await client.requests.search({
|
|
666
|
+
slug,
|
|
667
|
+
method,
|
|
668
|
+
q,
|
|
669
|
+
from,
|
|
670
|
+
to,
|
|
671
|
+
limit,
|
|
672
|
+
offset,
|
|
673
|
+
order
|
|
674
|
+
});
|
|
675
|
+
return jsonContent(results);
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
server.tool(
|
|
679
|
+
"count_requests",
|
|
680
|
+
"Count captured webhook requests that match the given filters.",
|
|
681
|
+
{
|
|
682
|
+
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
683
|
+
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
684
|
+
q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
|
|
685
|
+
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
686
|
+
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
|
|
687
|
+
},
|
|
688
|
+
withErrorHandling(async ({ slug, method, q, from, to }) => {
|
|
689
|
+
const count = await client.requests.count({ slug, method, q, from, to });
|
|
690
|
+
return jsonContent({ count });
|
|
163
691
|
})
|
|
164
692
|
);
|
|
165
693
|
server.tool(
|
|
166
694
|
"get_request",
|
|
167
|
-
"Get full details
|
|
168
|
-
{ requestId:
|
|
695
|
+
"Get full details for a specific captured request by ID.",
|
|
696
|
+
{ requestId: import_zod2.z.string().describe("The request ID") },
|
|
169
697
|
withErrorHandling(async ({ requestId }) => {
|
|
170
698
|
const request = await client.requests.get(requestId);
|
|
171
|
-
return
|
|
699
|
+
return jsonContent(request);
|
|
172
700
|
})
|
|
173
701
|
);
|
|
174
702
|
server.tool(
|
|
175
703
|
"wait_for_request",
|
|
176
|
-
"Wait for a
|
|
704
|
+
"Wait for a request to arrive at an endpoint.",
|
|
177
705
|
{
|
|
178
|
-
endpointSlug:
|
|
179
|
-
timeout:
|
|
180
|
-
pollInterval:
|
|
706
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug to monitor"),
|
|
707
|
+
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
708
|
+
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
|
|
181
709
|
},
|
|
182
710
|
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
183
711
|
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
184
|
-
return
|
|
712
|
+
return jsonContent(request);
|
|
713
|
+
})
|
|
714
|
+
);
|
|
715
|
+
server.tool(
|
|
716
|
+
"wait_for_requests",
|
|
717
|
+
"Wait for multiple requests to arrive at an endpoint.",
|
|
718
|
+
{
|
|
719
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug to monitor"),
|
|
720
|
+
count: import_zod2.z.number().int().min(1).max(20).describe("Number of requests to collect"),
|
|
721
|
+
timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
|
|
722
|
+
pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"'),
|
|
723
|
+
method: import_zod2.z.string().optional().describe("Only collect requests with this HTTP method")
|
|
724
|
+
},
|
|
725
|
+
withErrorHandling(async ({ endpointSlug, count, timeout, pollInterval, method }) => {
|
|
726
|
+
const result = await waitForMultipleRequests(client, endpointSlug, {
|
|
727
|
+
count,
|
|
728
|
+
timeout,
|
|
729
|
+
pollInterval,
|
|
730
|
+
method
|
|
731
|
+
});
|
|
732
|
+
return jsonContent(result);
|
|
185
733
|
})
|
|
186
734
|
);
|
|
187
735
|
server.tool(
|
|
188
736
|
"replay_request",
|
|
189
|
-
"Replay a previously captured
|
|
737
|
+
"Replay a previously captured request to a target URL.",
|
|
190
738
|
{
|
|
191
|
-
requestId:
|
|
192
|
-
targetUrl:
|
|
193
|
-
(u) => {
|
|
194
|
-
try {
|
|
195
|
-
const p = new URL(u).protocol;
|
|
196
|
-
return p === "http:" || p === "https:";
|
|
197
|
-
} catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
{ message: "Only http and https URLs are supported" }
|
|
202
|
-
).describe("The URL to send the replayed request to (http or https only)")
|
|
739
|
+
requestId: import_zod2.z.string().describe("The captured request ID"),
|
|
740
|
+
targetUrl: httpUrlSchema.describe("The URL to replay the request to")
|
|
203
741
|
},
|
|
204
742
|
withErrorHandling(async ({ requestId, targetUrl }) => {
|
|
205
743
|
const response = await client.requests.replay(requestId, targetUrl);
|
|
206
744
|
const responseBody = await readBodyTruncated(response);
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
745
|
+
return jsonContent({
|
|
746
|
+
status: response.status,
|
|
747
|
+
statusText: response.statusText,
|
|
748
|
+
body: responseBody
|
|
749
|
+
});
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
server.tool(
|
|
753
|
+
"compare_requests",
|
|
754
|
+
"Compare two captured requests and show the structured differences.",
|
|
755
|
+
{
|
|
756
|
+
leftRequestId: import_zod2.z.string().describe("The first request ID"),
|
|
757
|
+
rightRequestId: import_zod2.z.string().describe("The second request ID"),
|
|
758
|
+
ignoreHeaders: import_zod2.z.array(import_zod2.z.string()).optional().describe("Headers to ignore during comparison")
|
|
759
|
+
},
|
|
760
|
+
withErrorHandling(async ({ leftRequestId, rightRequestId, ignoreHeaders }) => {
|
|
761
|
+
const [leftRequest, rightRequest] = await Promise.all([
|
|
762
|
+
client.requests.get(leftRequestId),
|
|
763
|
+
client.requests.get(rightRequestId)
|
|
764
|
+
]);
|
|
765
|
+
const diff = (0, import_sdk.diffRequests)(leftRequest, rightRequest, { ignoreHeaders });
|
|
766
|
+
return jsonContent(diff);
|
|
767
|
+
})
|
|
768
|
+
);
|
|
769
|
+
server.tool(
|
|
770
|
+
"extract_from_request",
|
|
771
|
+
"Extract specific JSON fields from a captured request body.",
|
|
772
|
+
{
|
|
773
|
+
requestId: import_zod2.z.string().describe("The request ID"),
|
|
774
|
+
jsonPaths: import_zod2.z.array(import_zod2.z.string()).min(1).max(50).describe("Dot-notation JSON paths to extract")
|
|
775
|
+
},
|
|
776
|
+
withErrorHandling(async ({ requestId, jsonPaths }) => {
|
|
777
|
+
const request = await client.requests.get(requestId);
|
|
778
|
+
const extracted = Object.fromEntries(
|
|
779
|
+
jsonPaths.map((path) => [path, (0, import_sdk.extractJsonField)(request, path) ?? null])
|
|
213
780
|
);
|
|
781
|
+
return jsonContent(extracted);
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
server.tool(
|
|
785
|
+
"verify_signature",
|
|
786
|
+
"Verify the webhook signature on a captured request.",
|
|
787
|
+
{
|
|
788
|
+
requestId: import_zod2.z.string().describe("The captured request ID"),
|
|
789
|
+
provider: import_zod2.z.enum(VERIFY_PROVIDER_VALUES).describe("Provider whose signature scheme should be verified"),
|
|
790
|
+
secret: import_zod2.z.string().optional().describe("Shared signing secret. Required for non-Discord providers."),
|
|
791
|
+
publicKey: import_zod2.z.string().optional().describe("Discord application public key. Required for provider=discord."),
|
|
792
|
+
url: httpUrlSchema.optional().describe("Original signed URL. Required for Twilio verification.")
|
|
793
|
+
},
|
|
794
|
+
withErrorHandling(async ({ requestId, provider, secret, publicKey, url }) => {
|
|
795
|
+
const request = await client.requests.get(requestId);
|
|
796
|
+
const verificationOptions = ensureVerifyArgs({ provider, secret, publicKey, url });
|
|
797
|
+
const result = await (0, import_sdk.verifySignature)(request, verificationOptions);
|
|
798
|
+
return jsonContent({
|
|
799
|
+
valid: result.valid,
|
|
800
|
+
details: result.valid ? "Signature is valid." : "Signature did not match."
|
|
801
|
+
});
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
server.tool(
|
|
805
|
+
"clear_requests",
|
|
806
|
+
"Delete captured requests for an endpoint without deleting the endpoint itself.",
|
|
807
|
+
{
|
|
808
|
+
slug: import_zod2.z.string().describe("The endpoint slug to clear"),
|
|
809
|
+
before: durationOrTimestampSchema.optional().describe('Only clear requests older than this timestamp or duration like "1h"')
|
|
810
|
+
},
|
|
811
|
+
withErrorHandling(async ({ slug, before }) => {
|
|
812
|
+
await client.requests.clear(slug, { before });
|
|
813
|
+
return jsonContent({ slug, cleared: true, before: before ?? null });
|
|
214
814
|
})
|
|
215
815
|
);
|
|
216
816
|
server.tool(
|
|
217
817
|
"send_to",
|
|
218
|
-
"Send a webhook directly to any URL with optional provider signing.
|
|
818
|
+
"Send a webhook directly to any URL with optional provider signing.",
|
|
219
819
|
{
|
|
220
|
-
url:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
},
|
|
229
|
-
{ message: "Only http and https URLs are supported" }
|
|
230
|
-
).describe("Target URL to send the webhook to"),
|
|
231
|
-
method: import_zod.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).default("POST").describe("HTTP method (default: POST)"),
|
|
232
|
-
headers: import_zod.z.record(import_zod.z.string()).optional().describe("HTTP headers to include"),
|
|
233
|
-
body: import_zod.z.unknown().optional().describe("Request body (will be JSON-serialized)"),
|
|
234
|
-
provider: import_zod.z.enum(["stripe", "github", "shopify", "twilio", "standard-webhooks"]).optional().describe("Optional provider template for signing"),
|
|
235
|
-
template: import_zod.z.string().optional().describe("Provider-specific template preset (not used for standard-webhooks)"),
|
|
236
|
-
event: import_zod.z.string().optional().describe("Provider event/topic name"),
|
|
237
|
-
secret: import_zod.z.string().optional().describe(
|
|
238
|
-
"Shared secret for provider signature generation (required when provider is set)"
|
|
239
|
-
)
|
|
820
|
+
url: httpUrlSchema.describe("Target URL"),
|
|
821
|
+
method: methodSchema,
|
|
822
|
+
headers: import_zod2.z.record(import_zod2.z.string()).optional().describe("HTTP headers to include"),
|
|
823
|
+
body: import_zod2.z.unknown().optional().describe("Request body"),
|
|
824
|
+
provider: import_zod2.z.enum(TEMPLATE_PROVIDER_VALUES).optional().describe("Optional provider template for signing"),
|
|
825
|
+
template: import_zod2.z.string().optional().describe("Provider-specific template preset"),
|
|
826
|
+
event: import_zod2.z.string().optional().describe("Provider event or topic name"),
|
|
827
|
+
secret: import_zod2.z.string().optional().describe("Signing secret. Required when provider is set.")
|
|
240
828
|
},
|
|
241
829
|
withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
|
|
242
830
|
const response = await client.sendTo(url, {
|
|
@@ -249,47 +837,175 @@ function registerTools(server, client) {
|
|
|
249
837
|
secret
|
|
250
838
|
});
|
|
251
839
|
const responseBody = await readBodyTruncated(response);
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
840
|
+
return jsonContent({
|
|
841
|
+
status: response.status,
|
|
842
|
+
statusText: response.statusText,
|
|
843
|
+
body: responseBody
|
|
844
|
+
});
|
|
845
|
+
})
|
|
846
|
+
);
|
|
847
|
+
server.tool(
|
|
848
|
+
"preview_webhook",
|
|
849
|
+
"Preview a webhook request without sending it. Returns the exact URL, method, headers, and body.",
|
|
850
|
+
{
|
|
851
|
+
url: httpUrlSchema.describe("Target URL"),
|
|
852
|
+
method: methodSchema,
|
|
853
|
+
headers: import_zod2.z.record(import_zod2.z.string()).optional().describe("HTTP headers to include"),
|
|
854
|
+
body: import_zod2.z.unknown().optional().describe("Request body"),
|
|
855
|
+
provider: import_zod2.z.enum(TEMPLATE_PROVIDER_VALUES).optional().describe("Optional provider template for signing"),
|
|
856
|
+
template: import_zod2.z.string().optional().describe("Provider-specific template preset"),
|
|
857
|
+
event: import_zod2.z.string().optional().describe("Provider event or topic name"),
|
|
858
|
+
secret: import_zod2.z.string().optional().describe("Signing secret. Required when provider is set.")
|
|
859
|
+
},
|
|
860
|
+
withErrorHandling(async ({ url, method, headers, body, provider, template, event, secret }) => {
|
|
861
|
+
const preview = await client.buildRequest(url, {
|
|
862
|
+
method,
|
|
863
|
+
headers,
|
|
864
|
+
body,
|
|
865
|
+
provider,
|
|
866
|
+
template,
|
|
867
|
+
event,
|
|
868
|
+
secret
|
|
869
|
+
});
|
|
870
|
+
return jsonContent(preview);
|
|
871
|
+
})
|
|
872
|
+
);
|
|
873
|
+
server.tool(
|
|
874
|
+
"list_provider_templates",
|
|
875
|
+
"List supported webhook providers, templates, and signing metadata.",
|
|
876
|
+
{
|
|
877
|
+
provider: import_zod2.z.enum(TEMPLATE_PROVIDER_VALUES).optional().describe("Filter to a single provider")
|
|
878
|
+
},
|
|
879
|
+
withErrorHandling(async ({ provider }) => {
|
|
880
|
+
if (provider) {
|
|
881
|
+
return jsonContent([client.templates.get(provider)]);
|
|
882
|
+
}
|
|
883
|
+
return jsonContent(
|
|
884
|
+
client.templates.listProviders().map((name) => client.templates.get(name))
|
|
258
885
|
);
|
|
259
886
|
})
|
|
260
887
|
);
|
|
888
|
+
server.tool(
|
|
889
|
+
"get_usage",
|
|
890
|
+
"Check current request usage, remaining quota, plan, and period end.",
|
|
891
|
+
{},
|
|
892
|
+
withErrorHandling(async () => {
|
|
893
|
+
const usage = await client.usage();
|
|
894
|
+
return jsonContent({
|
|
895
|
+
...usage,
|
|
896
|
+
periodEnd: usage.periodEnd ? new Date(usage.periodEnd).toISOString() : null
|
|
897
|
+
});
|
|
898
|
+
})
|
|
899
|
+
);
|
|
900
|
+
server.tool(
|
|
901
|
+
"test_webhook_flow",
|
|
902
|
+
"Run a full webhook test flow: create endpoint, optionally mock, send, wait, verify, replay, and clean up.",
|
|
903
|
+
{
|
|
904
|
+
provider: import_zod2.z.enum(TEMPLATE_PROVIDER_VALUES).optional().describe("Optional provider template to use when sending the webhook"),
|
|
905
|
+
event: import_zod2.z.string().optional().describe("Optional provider event or topic name"),
|
|
906
|
+
secret: import_zod2.z.string().optional().describe(
|
|
907
|
+
"Signing secret. Required when provider is set or signature verification is enabled."
|
|
908
|
+
),
|
|
909
|
+
mockStatus: import_zod2.z.number().int().min(100).max(599).optional().describe("Optional mock response status to configure before sending"),
|
|
910
|
+
targetUrl: httpUrlSchema.optional().describe("Optional URL to replay the captured request to after capture"),
|
|
911
|
+
verifySignature: import_zod2.z.boolean().default(false).describe("Verify the captured request signature after capture"),
|
|
912
|
+
cleanup: import_zod2.z.boolean().default(true).describe("Delete the created endpoint after the flow completes")
|
|
913
|
+
},
|
|
914
|
+
withErrorHandling(
|
|
915
|
+
async ({
|
|
916
|
+
provider,
|
|
917
|
+
event,
|
|
918
|
+
secret,
|
|
919
|
+
mockStatus,
|
|
920
|
+
targetUrl,
|
|
921
|
+
verifySignature: shouldVerify,
|
|
922
|
+
cleanup
|
|
923
|
+
}) => {
|
|
924
|
+
const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s" });
|
|
925
|
+
if (mockStatus !== void 0) {
|
|
926
|
+
flow.setMock({
|
|
927
|
+
status: mockStatus,
|
|
928
|
+
body: "",
|
|
929
|
+
headers: {}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (provider) {
|
|
933
|
+
const templateSecret = secret?.trim();
|
|
934
|
+
if (!templateSecret) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
"test_webhook_flow with provider templates requires a non-empty secret"
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
flow.sendTemplate({
|
|
940
|
+
provider,
|
|
941
|
+
event,
|
|
942
|
+
secret: templateSecret
|
|
943
|
+
});
|
|
944
|
+
if (shouldVerify) {
|
|
945
|
+
flow.verifySignature({
|
|
946
|
+
provider,
|
|
947
|
+
secret: templateSecret
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
951
|
+
if (shouldVerify) {
|
|
952
|
+
throw new Error("test_webhook_flow cannot verify signatures without a provider");
|
|
953
|
+
}
|
|
954
|
+
flow.send();
|
|
955
|
+
}
|
|
956
|
+
if (targetUrl) {
|
|
957
|
+
flow.replayTo(targetUrl);
|
|
958
|
+
}
|
|
959
|
+
if (cleanup) {
|
|
960
|
+
flow.cleanup();
|
|
961
|
+
}
|
|
962
|
+
const result = await flow.run();
|
|
963
|
+
return jsonContent({
|
|
964
|
+
endpoint: result.endpoint,
|
|
965
|
+
request: result.request ?? null,
|
|
966
|
+
verification: result.verification ?? null,
|
|
967
|
+
replayResponse: result.replayResponse ? await summarizeResponse(result.replayResponse) : null,
|
|
968
|
+
cleanedUp: result.cleanedUp
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
)
|
|
972
|
+
);
|
|
261
973
|
server.tool(
|
|
262
974
|
"describe",
|
|
263
|
-
"Describe all available SDK operations,
|
|
975
|
+
"Describe all available SDK operations, parameters, and types.",
|
|
264
976
|
{},
|
|
265
977
|
withErrorHandling(async () => {
|
|
266
978
|
const description = client.describe();
|
|
267
|
-
return
|
|
979
|
+
return jsonContent(description);
|
|
268
980
|
})
|
|
269
981
|
);
|
|
270
982
|
}
|
|
271
983
|
|
|
272
984
|
// src/index.ts
|
|
273
|
-
var VERSION = true ? "0.
|
|
985
|
+
var VERSION = true ? "1.0.1" : "0.0.0-dev";
|
|
274
986
|
function createServer(options = {}) {
|
|
275
987
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
276
988
|
if (!apiKey) {
|
|
277
989
|
throw new Error("Missing API key. Set WHK_API_KEY environment variable or pass apiKey option.");
|
|
278
990
|
}
|
|
279
|
-
const client = new
|
|
991
|
+
const client = new import_sdk2.WebhooksCC({
|
|
280
992
|
apiKey,
|
|
281
993
|
webhookUrl: options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
|
|
282
994
|
baseUrl: options.baseUrl ?? process.env.WHK_BASE_URL
|
|
283
995
|
});
|
|
284
|
-
const server = new
|
|
996
|
+
const server = new import_mcp2.McpServer({
|
|
285
997
|
name: "webhooks-cc",
|
|
286
998
|
version: VERSION
|
|
287
999
|
});
|
|
288
1000
|
registerTools(server, client);
|
|
1001
|
+
registerPrompts(server);
|
|
1002
|
+
registerResources(server, client);
|
|
289
1003
|
return server;
|
|
290
1004
|
}
|
|
291
1005
|
// Annotate the CommonJS export names for ESM import in node:
|
|
292
1006
|
0 && (module.exports = {
|
|
293
1007
|
createServer,
|
|
1008
|
+
registerPrompts,
|
|
1009
|
+
registerResources,
|
|
294
1010
|
registerTools
|
|
295
1011
|
});
|