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