@webhooks-cc/mcp 1.15.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ For other agents, `npx skills add kroqdotdev/webhooks-cc` installs the same skil
34
34
  - **claude.ai and Claude Desktop**: Settings, Connectors, add a custom connector with the URL `https://webhooks.cc/mcp`
35
35
  - **ChatGPT**: add it as a custom connector (developer mode)
36
36
 
37
- A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has 44 tools: `send_to` and `replay_request` are left out and `targetUrl` is refused, because a hosted server cannot reach your machine, and waits are capped at 50 seconds. It needs an account (no sandbox), and serves tools only, without prompts or resources.
37
+ A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has 46 tools: `send_to` and `replay_request` are left out and `targetUrl` is refused, because a hosted server cannot reach your machine, and waits are capped at 50 seconds. It needs an account (no sandbox), and serves tools only, without prompts or resources.
38
38
 
39
39
  ### One-click
40
40
 
@@ -83,13 +83,14 @@ Leave out `env` to start without a key. Get your API key at [webhooks.cc/account
83
83
 
84
84
  ## What it exposes
85
85
 
86
- The server exposes `46` tools, `3` prompts, and `3` resource surfaces with an API key, the same `46` tools and the prompts with a saved connection, and `11` sandbox tools otherwise. The remote server exposes `44` tools.
86
+ The server exposes `48` tools, `3` prompts, and `3` resource surfaces with an API key, the same `48` tools and the prompts with a saved connection, and `11` sandbox tools otherwise. The remote server exposes `46` tools.
87
87
 
88
88
  ### Endpoint tools
89
89
 
90
90
  - `create_endpoint`
91
91
  - `list_endpoints`
92
92
  - `get_endpoint`
93
+ - `favorite_endpoint` (mark or unmark one of your favourites; `list_endpoints` and `get_endpoint` show `favorite`)
93
94
  - `update_endpoint`
94
95
  - `preview_response`
95
96
  - `delete_endpoint`
package/dist/bin/mcp.js CHANGED
@@ -191,6 +191,53 @@ function capLists(holder) {
191
191
  }
192
192
  }
193
193
  }
194
+ var MAX_PARSED_ITEMS = 20;
195
+ var MAX_PARSED_VALUE = 200;
196
+ var MAX_PARSED_SHEETS = 10;
197
+ function trimParsed(holder) {
198
+ const parsed = holder.parsed;
199
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
200
+ const next = { ...parsed };
201
+ if (Array.isArray(next.lineItems)) {
202
+ if (next.lineItems.length > MAX_PARSED_ITEMS) {
203
+ next.lineItemsTotal = next.lineItems.length;
204
+ }
205
+ next.lineItems = next.lineItems.slice(0, MAX_PARSED_ITEMS).map(
206
+ (row) => Object.fromEntries(
207
+ Object.entries(row).map(([key, value]) => [
208
+ key,
209
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
210
+ ])
211
+ )
212
+ );
213
+ }
214
+ if (next.fields && typeof next.fields === "object") {
215
+ next.fields = Object.fromEntries(
216
+ Object.entries(next.fields).map(([key, value]) => [
217
+ key,
218
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
219
+ ])
220
+ );
221
+ }
222
+ if (Array.isArray(next.attachments)) {
223
+ next.attachments = next.attachments.map((attachment) => {
224
+ const { sheets, text, ...rest } = attachment;
225
+ return {
226
+ ...rest,
227
+ ...Array.isArray(sheets) ? {
228
+ sheets: sheets.slice(0, MAX_PARSED_SHEETS).map((sheet) => ({
229
+ name: sheet.name,
230
+ rowCount: sheet.rowCount
231
+ })),
232
+ ...sheets.length > MAX_PARSED_SHEETS ? { sheetsTotal: sheets.length } : {}
233
+ } : {},
234
+ ...typeof text === "string" ? { textLength: text.length } : {}
235
+ };
236
+ });
237
+ }
238
+ next.trimmed = "Line items, sheet rows and attachment text were cut to fit the output.";
239
+ holder.parsed = next;
240
+ }
194
241
  function keepOnly(holder, keep) {
195
242
  for (const key of Object.keys(holder)) {
196
243
  if (!keep.includes(key)) delete holder[key];
@@ -256,6 +303,7 @@ function compactRequest(request) {
256
303
  const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
257
304
  const budget = MAX_OUTPUT - 128;
258
305
  shrinkToFit(root, budget, [
306
+ () => trimParsed(compact),
259
307
  () => omitHeaders(compact),
260
308
  () => trimSpam(email),
261
309
  () => capLists(email),
@@ -397,6 +445,10 @@ function registerResources(server, client) {
397
445
  var import_zod3 = require("zod");
398
446
  var import_sdk2 = require("@webhooks-cc/sdk");
399
447
 
448
+ // src/version.ts
449
+ var VERSION = true ? "1.17.0" : "0.0.0-dev";
450
+ var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
451
+
400
452
  // src/tools.ts
401
453
  var import_zod2 = require("zod");
402
454
  var import_sdk = require("@webhooks-cc/sdk");
@@ -439,6 +491,34 @@ function conditionSchema(use) {
439
491
  });
440
492
  }
441
493
  var ruleConditionSchema = conditionSchema("response");
494
+ var emailFieldSchema = import_zod2.z.object({
495
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
496
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
497
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
498
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
499
+ });
500
+ var layoutSchema = import_zod2.z.object({
501
+ id: import_zod2.z.string().describe("Stable id: 1 to 40 letters, digits, - or _"),
502
+ name: import_zod2.z.string().max(100),
503
+ enabled: import_zod2.z.boolean().optional().describe("Off: kept but never matched (default on)"),
504
+ match: import_zod2.z.object({
505
+ logic: import_zod2.z.enum(["and", "or"]).optional(),
506
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
507
+ }).optional().describe("When it applies, e.g. the sender or subject; no conditions: every email"),
508
+ fields: import_zod2.z.array(emailFieldSchema).max(20).optional().describe("Named values, as emailFields"),
509
+ lineItems: import_zod2.z.object({
510
+ from: import_zod2.z.enum(["text", "html", "attachment"]),
511
+ pattern: import_zod2.z.string().optional().describe(
512
+ "text/html: RE2 with named groups found again and again, one row each, e.g. (?P<sku>[A-Z]+-\\d+)\\s+x(?P<qty>\\d+)"
513
+ ),
514
+ start: import_zod2.z.string().optional().describe("text/html: search only after this (RE2)"),
515
+ end: import_zod2.z.string().optional().describe("text/html: and before this (RE2)"),
516
+ attachment: import_zod2.z.string().optional().describe("attachment: file name with * and ? (default: the first spreadsheet or CSV)"),
517
+ sheet: import_zod2.z.string().optional().describe("attachment: sheet name (default: the first)"),
518
+ columns: import_zod2.z.array(import_zod2.z.object({ name: import_zod2.z.string(), column: import_zod2.z.string() })).max(30).optional().describe("attachment: { name, column: header text or letter }; default: every column")
519
+ }).nullable().optional().describe("Rows such as an order's products"),
520
+ parseAttachments: import_zod2.z.boolean().optional().describe("Parse CSV, XLSX, ODS, PDF text and text files into the JSON; uses parsed pages")
521
+ });
442
522
  var destinationFields = {
443
523
  name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
444
524
  enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
@@ -505,6 +585,9 @@ var mockResponseSchema = import_zod2.z.object({
505
585
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
506
586
  )
507
587
  });
588
+ var endpointSiteSchema = import_zod2.z.enum(["webhooks", "mailhooks", "both"]).describe(
589
+ "Which site's dashboard lists the endpoint: webhooks (webhooks.cc), mailhooks (mailhooks.cc) or both (the default when creating). Only the listing: its HTTP URL and email address work either way. Owner only to change."
590
+ );
508
591
  var createEndpointShape = {
509
592
  name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
510
593
  ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
@@ -516,6 +599,7 @@ var createEndpointShape = {
516
599
  notificationUrl: import_zod2.z.string().url().optional().describe(
517
600
  "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
518
601
  ),
602
+ site: endpointSiteSchema.optional(),
519
603
  signingProvider: import_zod2.z.string().optional().describe(
520
604
  "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
521
605
  ),
@@ -858,6 +942,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
858
942
  "subject",
859
943
  "from",
860
944
  "fields",
945
+ "parsed",
861
946
  "replyText",
862
947
  "size",
863
948
  "test",
@@ -894,6 +979,7 @@ function emailDetail(client, email, view, includeHtml) {
894
979
  ...includeHtml && html !== null ? { html } : {}
895
980
  };
896
981
  shrinkToFit(detail, MAX_BODY_SIZE, [
982
+ () => trimParsed(detail),
897
983
  () => omitHeaders(detail),
898
984
  () => trimSpam(detail),
899
985
  () => capExtracts(detail),
@@ -987,6 +1073,7 @@ function registerTools(target, client, options = {}) {
987
1073
  mockResponse,
988
1074
  responseRules,
989
1075
  notificationUrl,
1076
+ site,
990
1077
  signingProvider,
991
1078
  signingSecret,
992
1079
  signingHeader
@@ -1008,7 +1095,8 @@ function registerTools(target, client, options = {}) {
1008
1095
  expiresIn,
1009
1096
  mockResponse,
1010
1097
  responseRules,
1011
- notificationUrl
1098
+ notificationUrl,
1099
+ site
1012
1100
  });
1013
1101
  if (signingProvider && signingSecret) {
1014
1102
  try {
@@ -1093,6 +1181,18 @@ function registerTools(target, client, options = {}) {
1093
1181
  return jsonContent(endpoint);
1094
1182
  })
1095
1183
  );
1184
+ server.tool(
1185
+ "favorite_endpoint",
1186
+ "Mark an endpoint as one of your favourites, or unmark it with favorite: false. A favourite is your own choice, so endpoints shared with you through teams can be marked too; list_endpoints and get_endpoint show it as favorite. Idempotent.",
1187
+ {
1188
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1189
+ favorite: import_zod2.z.boolean().optional().describe("true marks it as a favourite (the default), false unmarks it")
1190
+ },
1191
+ withErrorHandling(async ({ slug, favorite }) => {
1192
+ const result = favorite === false ? await client.endpoints.unfavorite(slug) : await client.endpoints.favorite(slug);
1193
+ return jsonContent({ slug, favorite: result.favorite });
1194
+ })
1195
+ );
1096
1196
  server.tool(
1097
1197
  "update_endpoint",
1098
1198
  "Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
@@ -1110,15 +1210,12 @@ function registerTools(target, client, options = {}) {
1110
1210
  "Custom signature header name (only for generic-hmac provider), or null to clear"
1111
1211
  ),
1112
1212
  showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
1113
- emailFields: import_zod2.z.array(
1114
- import_zod2.z.object({
1115
- name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
1116
- from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
1117
- header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
1118
- pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
1119
- })
1120
- ).max(20).nullable().optional().describe(
1213
+ site: endpointSiteSchema.optional(),
1214
+ emailFields: import_zod2.z.array(emailFieldSchema).max(20).nullable().optional().describe(
1121
1215
  "Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
1216
+ ),
1217
+ layouts: import_zod2.z.array(layoutSchema).max(50).nullable().optional().describe(
1218
+ "How the endpoint reads each sender's emails: the first layout whose match holds applies its fields, line items and attachment parsing; the result is `parsed` in the email's JSON, and deliveries wait for it. The plan uses the first 2 (Business 50). Replaces the list; null or [] removes them. Try them with preview_layouts first. Owner only."
1122
1219
  )
1123
1220
  },
1124
1221
  withErrorHandling(
@@ -1132,7 +1229,9 @@ function registerTools(target, client, options = {}) {
1132
1229
  signingSecret,
1133
1230
  signingHeader,
1134
1231
  showEmailExtracts,
1135
- emailFields
1232
+ site,
1233
+ emailFields,
1234
+ layouts
1136
1235
  }) => {
1137
1236
  const endpoint = await client.endpoints.update(slug, {
1138
1237
  name,
@@ -1143,12 +1242,29 @@ function registerTools(target, client, options = {}) {
1143
1242
  signingSecret,
1144
1243
  signingHeader,
1145
1244
  showEmailExtracts,
1146
- emailFields
1245
+ site,
1246
+ emailFields,
1247
+ layouts
1147
1248
  });
1148
1249
  return jsonContent(endpoint);
1149
1250
  }
1150
1251
  )
1151
1252
  );
1253
+ server.tool(
1254
+ "preview_layouts",
1255
+ "Dry run of an endpoint's layouts on a captured email: which layout matches, the fields and line items it finds, and what earlier parses of its attachments held. Nothing is saved and no pages are used. Pass layouts to try them before saving them with update_endpoint.",
1256
+ {
1257
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1258
+ requestId: import_zod2.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1259
+ layouts: import_zod2.z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1260
+ },
1261
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1262
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1263
+ const holder = { ...preview };
1264
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1265
+ return jsonContent(holder);
1266
+ })
1267
+ );
1152
1268
  server.tool(
1153
1269
  "preview_response",
1154
1270
  "Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
@@ -2049,7 +2165,8 @@ var AgentSession = class {
2049
2165
  baseUrl: this.baseUrl,
2050
2166
  webhookUrl: this.webhookUrl,
2051
2167
  clientName: this.clientName,
2052
- credentialsFile: this.credentialsFile
2168
+ credentialsFile: this.credentialsFile,
2169
+ userAgent: MCP_USER_AGENT
2053
2170
  };
2054
2171
  }
2055
2172
  /**
@@ -2142,6 +2259,7 @@ function registerSandboxTools(server, options = {}) {
2142
2259
  baseUrl,
2143
2260
  webhookUrl,
2144
2261
  credentialsFile: options.credentialsFile,
2262
+ userAgent: MCP_USER_AGENT,
2145
2263
  // The human disconnected the agent: back to the sandbox, unless a
2146
2264
  // newer connection is already on display.
2147
2265
  onRevoked: () => queueMicrotask(() => {
@@ -2303,7 +2421,8 @@ function sandboxToolSet(server, session, showAccount) {
2303
2421
  webhookUrl: session.webhookUrl,
2304
2422
  email: normalized,
2305
2423
  clientName: session.clientName,
2306
- credentialsFile: session.credentialsFile
2424
+ credentialsFile: session.credentialsFile,
2425
+ userAgent: MCP_USER_AGENT
2307
2426
  }));
2308
2427
  session.connect = { via: "connect", pending, email: normalized };
2309
2428
  attempt = pending;
@@ -2409,7 +2528,6 @@ function sandboxToolSet(server, session, showAccount) {
2409
2528
  }
2410
2529
 
2411
2530
  // src/index.ts
2412
- var VERSION = true ? "1.15.1" : "0.0.0-dev";
2413
2531
  function createServer(options = {}) {
2414
2532
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2415
2533
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -2424,7 +2542,7 @@ function createServer(options = {}) {
2424
2542
  registerPrompts(server);
2425
2543
  return server;
2426
2544
  }
2427
- const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl });
2545
+ const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl, userAgent: MCP_USER_AGENT });
2428
2546
  registerTools(server, client);
2429
2547
  registerPrompts(server);
2430
2548
  registerResources(server, client);
@@ -100,6 +100,53 @@ function capLists(holder) {
100
100
  }
101
101
  }
102
102
  }
103
+ var MAX_PARSED_ITEMS = 20;
104
+ var MAX_PARSED_VALUE = 200;
105
+ var MAX_PARSED_SHEETS = 10;
106
+ function trimParsed(holder) {
107
+ const parsed = holder.parsed;
108
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
109
+ const next = { ...parsed };
110
+ if (Array.isArray(next.lineItems)) {
111
+ if (next.lineItems.length > MAX_PARSED_ITEMS) {
112
+ next.lineItemsTotal = next.lineItems.length;
113
+ }
114
+ next.lineItems = next.lineItems.slice(0, MAX_PARSED_ITEMS).map(
115
+ (row) => Object.fromEntries(
116
+ Object.entries(row).map(([key, value]) => [
117
+ key,
118
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
119
+ ])
120
+ )
121
+ );
122
+ }
123
+ if (next.fields && typeof next.fields === "object") {
124
+ next.fields = Object.fromEntries(
125
+ Object.entries(next.fields).map(([key, value]) => [
126
+ key,
127
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
128
+ ])
129
+ );
130
+ }
131
+ if (Array.isArray(next.attachments)) {
132
+ next.attachments = next.attachments.map((attachment) => {
133
+ const { sheets, text, ...rest } = attachment;
134
+ return {
135
+ ...rest,
136
+ ...Array.isArray(sheets) ? {
137
+ sheets: sheets.slice(0, MAX_PARSED_SHEETS).map((sheet) => ({
138
+ name: sheet.name,
139
+ rowCount: sheet.rowCount
140
+ })),
141
+ ...sheets.length > MAX_PARSED_SHEETS ? { sheetsTotal: sheets.length } : {}
142
+ } : {},
143
+ ...typeof text === "string" ? { textLength: text.length } : {}
144
+ };
145
+ });
146
+ }
147
+ next.trimmed = "Line items, sheet rows and attachment text were cut to fit the output.";
148
+ holder.parsed = next;
149
+ }
103
150
  function keepOnly(holder, keep) {
104
151
  for (const key of Object.keys(holder)) {
105
152
  if (!keep.includes(key)) delete holder[key];
@@ -165,6 +212,7 @@ function compactRequest(request) {
165
212
  const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
166
213
  const budget = MAX_OUTPUT - 128;
167
214
  shrinkToFit(root, budget, [
215
+ () => trimParsed(compact),
168
216
  () => omitHeaders(compact),
169
217
  () => trimSpam(email),
170
218
  () => capLists(email),
@@ -221,6 +269,34 @@ function conditionSchema(use) {
221
269
  });
222
270
  }
223
271
  var ruleConditionSchema = conditionSchema("response");
272
+ var emailFieldSchema = z.object({
273
+ name: z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
274
+ from: z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
275
+ header: z.string().optional().describe('The header name, for from "header"'),
276
+ pattern: z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
277
+ });
278
+ var layoutSchema = z.object({
279
+ id: z.string().describe("Stable id: 1 to 40 letters, digits, - or _"),
280
+ name: z.string().max(100),
281
+ enabled: z.boolean().optional().describe("Off: kept but never matched (default on)"),
282
+ match: z.object({
283
+ logic: z.enum(["and", "or"]).optional(),
284
+ conditions: z.array(conditionSchema("filter")).max(10)
285
+ }).optional().describe("When it applies, e.g. the sender or subject; no conditions: every email"),
286
+ fields: z.array(emailFieldSchema).max(20).optional().describe("Named values, as emailFields"),
287
+ lineItems: z.object({
288
+ from: z.enum(["text", "html", "attachment"]),
289
+ pattern: z.string().optional().describe(
290
+ "text/html: RE2 with named groups found again and again, one row each, e.g. (?P<sku>[A-Z]+-\\d+)\\s+x(?P<qty>\\d+)"
291
+ ),
292
+ start: z.string().optional().describe("text/html: search only after this (RE2)"),
293
+ end: z.string().optional().describe("text/html: and before this (RE2)"),
294
+ attachment: z.string().optional().describe("attachment: file name with * and ? (default: the first spreadsheet or CSV)"),
295
+ sheet: z.string().optional().describe("attachment: sheet name (default: the first)"),
296
+ columns: z.array(z.object({ name: z.string(), column: z.string() })).max(30).optional().describe("attachment: { name, column: header text or letter }; default: every column")
297
+ }).nullable().optional().describe("Rows such as an order's products"),
298
+ parseAttachments: z.boolean().optional().describe("Parse CSV, XLSX, ODS, PDF text and text files into the JSON; uses parsed pages")
299
+ });
224
300
  var destinationFields = {
225
301
  name: z.string().max(100).nullable().optional().describe("A name for the list and the log"),
226
302
  enabled: z.boolean().optional().describe("Send to it (default true)"),
@@ -287,6 +363,9 @@ var mockResponseSchema = z.object({
287
363
  `Response delay in milliseconds (${MOCK_RESPONSE_DELAY_MIN}-${MOCK_RESPONSE_DELAY_MAX}, default: none)`
288
364
  )
289
365
  });
366
+ var endpointSiteSchema = z.enum(["webhooks", "mailhooks", "both"]).describe(
367
+ "Which site's dashboard lists the endpoint: webhooks (webhooks.cc), mailhooks (mailhooks.cc) or both (the default when creating). Only the listing: its HTTP URL and email address work either way. Owner only to change."
368
+ );
290
369
  var createEndpointShape = {
291
370
  name: z.string().optional().describe("Display name for the endpoint"),
292
371
  ephemeral: z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
@@ -298,6 +377,7 @@ var createEndpointShape = {
298
377
  notificationUrl: z.string().url().optional().describe(
299
378
  "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
300
379
  ),
380
+ site: endpointSiteSchema.optional(),
301
381
  signingProvider: z.string().optional().describe(
302
382
  "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
303
383
  ),
@@ -640,6 +720,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
640
720
  "subject",
641
721
  "from",
642
722
  "fields",
723
+ "parsed",
643
724
  "replyText",
644
725
  "size",
645
726
  "test",
@@ -676,6 +757,7 @@ function emailDetail(client, email, view, includeHtml) {
676
757
  ...includeHtml && html !== null ? { html } : {}
677
758
  };
678
759
  shrinkToFit(detail, MAX_BODY_SIZE, [
760
+ () => trimParsed(detail),
679
761
  () => omitHeaders(detail),
680
762
  () => trimSpam(detail),
681
763
  () => capExtracts(detail),
@@ -769,6 +851,7 @@ function registerTools(target, client, options = {}) {
769
851
  mockResponse,
770
852
  responseRules,
771
853
  notificationUrl,
854
+ site,
772
855
  signingProvider,
773
856
  signingSecret,
774
857
  signingHeader
@@ -790,7 +873,8 @@ function registerTools(target, client, options = {}) {
790
873
  expiresIn,
791
874
  mockResponse,
792
875
  responseRules,
793
- notificationUrl
876
+ notificationUrl,
877
+ site
794
878
  });
795
879
  if (signingProvider && signingSecret) {
796
880
  try {
@@ -875,6 +959,18 @@ function registerTools(target, client, options = {}) {
875
959
  return jsonContent(endpoint);
876
960
  })
877
961
  );
962
+ server.tool(
963
+ "favorite_endpoint",
964
+ "Mark an endpoint as one of your favourites, or unmark it with favorite: false. A favourite is your own choice, so endpoints shared with you through teams can be marked too; list_endpoints and get_endpoint show it as favorite. Idempotent.",
965
+ {
966
+ slug: z.string().describe("The endpoint slug"),
967
+ favorite: z.boolean().optional().describe("true marks it as a favourite (the default), false unmarks it")
968
+ },
969
+ withErrorHandling(async ({ slug, favorite }) => {
970
+ const result = favorite === false ? await client.endpoints.unfavorite(slug) : await client.endpoints.favorite(slug);
971
+ return jsonContent({ slug, favorite: result.favorite });
972
+ })
973
+ );
878
974
  server.tool(
879
975
  "update_endpoint",
880
976
  "Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
@@ -892,15 +988,12 @@ function registerTools(target, client, options = {}) {
892
988
  "Custom signature header name (only for generic-hmac provider), or null to clear"
893
989
  ),
894
990
  showEmailExtracts: z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
895
- emailFields: z.array(
896
- z.object({
897
- name: z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
898
- from: z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
899
- header: z.string().optional().describe('The header name, for from "header"'),
900
- pattern: z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
901
- })
902
- ).max(20).nullable().optional().describe(
991
+ site: endpointSiteSchema.optional(),
992
+ emailFields: z.array(emailFieldSchema).max(20).nullable().optional().describe(
903
993
  "Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
994
+ ),
995
+ layouts: z.array(layoutSchema).max(50).nullable().optional().describe(
996
+ "How the endpoint reads each sender's emails: the first layout whose match holds applies its fields, line items and attachment parsing; the result is `parsed` in the email's JSON, and deliveries wait for it. The plan uses the first 2 (Business 50). Replaces the list; null or [] removes them. Try them with preview_layouts first. Owner only."
904
997
  )
905
998
  },
906
999
  withErrorHandling(
@@ -914,7 +1007,9 @@ function registerTools(target, client, options = {}) {
914
1007
  signingSecret,
915
1008
  signingHeader,
916
1009
  showEmailExtracts,
917
- emailFields
1010
+ site,
1011
+ emailFields,
1012
+ layouts
918
1013
  }) => {
919
1014
  const endpoint = await client.endpoints.update(slug, {
920
1015
  name,
@@ -925,12 +1020,29 @@ function registerTools(target, client, options = {}) {
925
1020
  signingSecret,
926
1021
  signingHeader,
927
1022
  showEmailExtracts,
928
- emailFields
1023
+ site,
1024
+ emailFields,
1025
+ layouts
929
1026
  });
930
1027
  return jsonContent(endpoint);
931
1028
  }
932
1029
  )
933
1030
  );
1031
+ server.tool(
1032
+ "preview_layouts",
1033
+ "Dry run of an endpoint's layouts on a captured email: which layout matches, the fields and line items it finds, and what earlier parses of its attachments held. Nothing is saved and no pages are used. Pass layouts to try them before saving them with update_endpoint.",
1034
+ {
1035
+ slug: z.string().describe("The endpoint slug"),
1036
+ requestId: z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1037
+ layouts: z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1038
+ },
1039
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1040
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1041
+ const holder = { ...preview };
1042
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1043
+ return jsonContent(holder);
1044
+ })
1045
+ );
934
1046
  server.tool(
935
1047
  "preview_response",
936
1048
  "Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
package/dist/index.js CHANGED
@@ -215,6 +215,53 @@ function capLists(holder) {
215
215
  }
216
216
  }
217
217
  }
218
+ var MAX_PARSED_ITEMS = 20;
219
+ var MAX_PARSED_VALUE = 200;
220
+ var MAX_PARSED_SHEETS = 10;
221
+ function trimParsed(holder) {
222
+ const parsed = holder.parsed;
223
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
224
+ const next = { ...parsed };
225
+ if (Array.isArray(next.lineItems)) {
226
+ if (next.lineItems.length > MAX_PARSED_ITEMS) {
227
+ next.lineItemsTotal = next.lineItems.length;
228
+ }
229
+ next.lineItems = next.lineItems.slice(0, MAX_PARSED_ITEMS).map(
230
+ (row) => Object.fromEntries(
231
+ Object.entries(row).map(([key, value]) => [
232
+ key,
233
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
234
+ ])
235
+ )
236
+ );
237
+ }
238
+ if (next.fields && typeof next.fields === "object") {
239
+ next.fields = Object.fromEntries(
240
+ Object.entries(next.fields).map(([key, value]) => [
241
+ key,
242
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
243
+ ])
244
+ );
245
+ }
246
+ if (Array.isArray(next.attachments)) {
247
+ next.attachments = next.attachments.map((attachment) => {
248
+ const { sheets, text, ...rest } = attachment;
249
+ return {
250
+ ...rest,
251
+ ...Array.isArray(sheets) ? {
252
+ sheets: sheets.slice(0, MAX_PARSED_SHEETS).map((sheet) => ({
253
+ name: sheet.name,
254
+ rowCount: sheet.rowCount
255
+ })),
256
+ ...sheets.length > MAX_PARSED_SHEETS ? { sheetsTotal: sheets.length } : {}
257
+ } : {},
258
+ ...typeof text === "string" ? { textLength: text.length } : {}
259
+ };
260
+ });
261
+ }
262
+ next.trimmed = "Line items, sheet rows and attachment text were cut to fit the output.";
263
+ holder.parsed = next;
264
+ }
218
265
  function keepOnly(holder, keep) {
219
266
  for (const key of Object.keys(holder)) {
220
267
  if (!keep.includes(key)) delete holder[key];
@@ -280,6 +327,7 @@ function compactRequest(request) {
280
327
  const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
281
328
  const budget = MAX_OUTPUT - 128;
282
329
  shrinkToFit(root, budget, [
330
+ () => trimParsed(compact),
283
331
  () => omitHeaders(compact),
284
332
  () => trimSpam(email),
285
333
  () => capLists(email),
@@ -421,6 +469,10 @@ function registerResources(server, client) {
421
469
  var import_zod3 = require("zod");
422
470
  var import_sdk2 = require("@webhooks-cc/sdk");
423
471
 
472
+ // src/version.ts
473
+ var VERSION = true ? "1.17.0" : "0.0.0-dev";
474
+ var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
475
+
424
476
  // src/tools.ts
425
477
  var import_zod2 = require("zod");
426
478
  var import_sdk = require("@webhooks-cc/sdk");
@@ -463,6 +515,34 @@ function conditionSchema(use) {
463
515
  });
464
516
  }
465
517
  var ruleConditionSchema = conditionSchema("response");
518
+ var emailFieldSchema = import_zod2.z.object({
519
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
520
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
521
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
522
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
523
+ });
524
+ var layoutSchema = import_zod2.z.object({
525
+ id: import_zod2.z.string().describe("Stable id: 1 to 40 letters, digits, - or _"),
526
+ name: import_zod2.z.string().max(100),
527
+ enabled: import_zod2.z.boolean().optional().describe("Off: kept but never matched (default on)"),
528
+ match: import_zod2.z.object({
529
+ logic: import_zod2.z.enum(["and", "or"]).optional(),
530
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
531
+ }).optional().describe("When it applies, e.g. the sender or subject; no conditions: every email"),
532
+ fields: import_zod2.z.array(emailFieldSchema).max(20).optional().describe("Named values, as emailFields"),
533
+ lineItems: import_zod2.z.object({
534
+ from: import_zod2.z.enum(["text", "html", "attachment"]),
535
+ pattern: import_zod2.z.string().optional().describe(
536
+ "text/html: RE2 with named groups found again and again, one row each, e.g. (?P<sku>[A-Z]+-\\d+)\\s+x(?P<qty>\\d+)"
537
+ ),
538
+ start: import_zod2.z.string().optional().describe("text/html: search only after this (RE2)"),
539
+ end: import_zod2.z.string().optional().describe("text/html: and before this (RE2)"),
540
+ attachment: import_zod2.z.string().optional().describe("attachment: file name with * and ? (default: the first spreadsheet or CSV)"),
541
+ sheet: import_zod2.z.string().optional().describe("attachment: sheet name (default: the first)"),
542
+ columns: import_zod2.z.array(import_zod2.z.object({ name: import_zod2.z.string(), column: import_zod2.z.string() })).max(30).optional().describe("attachment: { name, column: header text or letter }; default: every column")
543
+ }).nullable().optional().describe("Rows such as an order's products"),
544
+ parseAttachments: import_zod2.z.boolean().optional().describe("Parse CSV, XLSX, ODS, PDF text and text files into the JSON; uses parsed pages")
545
+ });
466
546
  var destinationFields = {
467
547
  name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
468
548
  enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
@@ -529,6 +609,9 @@ var mockResponseSchema = import_zod2.z.object({
529
609
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
530
610
  )
531
611
  });
612
+ var endpointSiteSchema = import_zod2.z.enum(["webhooks", "mailhooks", "both"]).describe(
613
+ "Which site's dashboard lists the endpoint: webhooks (webhooks.cc), mailhooks (mailhooks.cc) or both (the default when creating). Only the listing: its HTTP URL and email address work either way. Owner only to change."
614
+ );
532
615
  var createEndpointShape = {
533
616
  name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
534
617
  ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
@@ -540,6 +623,7 @@ var createEndpointShape = {
540
623
  notificationUrl: import_zod2.z.string().url().optional().describe(
541
624
  "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
542
625
  ),
626
+ site: endpointSiteSchema.optional(),
543
627
  signingProvider: import_zod2.z.string().optional().describe(
544
628
  "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
545
629
  ),
@@ -882,6 +966,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
882
966
  "subject",
883
967
  "from",
884
968
  "fields",
969
+ "parsed",
885
970
  "replyText",
886
971
  "size",
887
972
  "test",
@@ -918,6 +1003,7 @@ function emailDetail(client, email, view, includeHtml) {
918
1003
  ...includeHtml && html !== null ? { html } : {}
919
1004
  };
920
1005
  shrinkToFit(detail, MAX_BODY_SIZE, [
1006
+ () => trimParsed(detail),
921
1007
  () => omitHeaders(detail),
922
1008
  () => trimSpam(detail),
923
1009
  () => capExtracts(detail),
@@ -1011,6 +1097,7 @@ function registerTools(target, client, options = {}) {
1011
1097
  mockResponse,
1012
1098
  responseRules,
1013
1099
  notificationUrl,
1100
+ site,
1014
1101
  signingProvider,
1015
1102
  signingSecret,
1016
1103
  signingHeader
@@ -1032,7 +1119,8 @@ function registerTools(target, client, options = {}) {
1032
1119
  expiresIn,
1033
1120
  mockResponse,
1034
1121
  responseRules,
1035
- notificationUrl
1122
+ notificationUrl,
1123
+ site
1036
1124
  });
1037
1125
  if (signingProvider && signingSecret) {
1038
1126
  try {
@@ -1117,6 +1205,18 @@ function registerTools(target, client, options = {}) {
1117
1205
  return jsonContent(endpoint);
1118
1206
  })
1119
1207
  );
1208
+ server.tool(
1209
+ "favorite_endpoint",
1210
+ "Mark an endpoint as one of your favourites, or unmark it with favorite: false. A favourite is your own choice, so endpoints shared with you through teams can be marked too; list_endpoints and get_endpoint show it as favorite. Idempotent.",
1211
+ {
1212
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1213
+ favorite: import_zod2.z.boolean().optional().describe("true marks it as a favourite (the default), false unmarks it")
1214
+ },
1215
+ withErrorHandling(async ({ slug, favorite }) => {
1216
+ const result = favorite === false ? await client.endpoints.unfavorite(slug) : await client.endpoints.favorite(slug);
1217
+ return jsonContent({ slug, favorite: result.favorite });
1218
+ })
1219
+ );
1120
1220
  server.tool(
1121
1221
  "update_endpoint",
1122
1222
  "Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
@@ -1134,15 +1234,12 @@ function registerTools(target, client, options = {}) {
1134
1234
  "Custom signature header name (only for generic-hmac provider), or null to clear"
1135
1235
  ),
1136
1236
  showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
1137
- emailFields: import_zod2.z.array(
1138
- import_zod2.z.object({
1139
- name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
1140
- from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
1141
- header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
1142
- pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
1143
- })
1144
- ).max(20).nullable().optional().describe(
1237
+ site: endpointSiteSchema.optional(),
1238
+ emailFields: import_zod2.z.array(emailFieldSchema).max(20).nullable().optional().describe(
1145
1239
  "Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
1240
+ ),
1241
+ layouts: import_zod2.z.array(layoutSchema).max(50).nullable().optional().describe(
1242
+ "How the endpoint reads each sender's emails: the first layout whose match holds applies its fields, line items and attachment parsing; the result is `parsed` in the email's JSON, and deliveries wait for it. The plan uses the first 2 (Business 50). Replaces the list; null or [] removes them. Try them with preview_layouts first. Owner only."
1146
1243
  )
1147
1244
  },
1148
1245
  withErrorHandling(
@@ -1156,7 +1253,9 @@ function registerTools(target, client, options = {}) {
1156
1253
  signingSecret,
1157
1254
  signingHeader,
1158
1255
  showEmailExtracts,
1159
- emailFields
1256
+ site,
1257
+ emailFields,
1258
+ layouts
1160
1259
  }) => {
1161
1260
  const endpoint = await client.endpoints.update(slug, {
1162
1261
  name,
@@ -1167,12 +1266,29 @@ function registerTools(target, client, options = {}) {
1167
1266
  signingSecret,
1168
1267
  signingHeader,
1169
1268
  showEmailExtracts,
1170
- emailFields
1269
+ site,
1270
+ emailFields,
1271
+ layouts
1171
1272
  });
1172
1273
  return jsonContent(endpoint);
1173
1274
  }
1174
1275
  )
1175
1276
  );
1277
+ server.tool(
1278
+ "preview_layouts",
1279
+ "Dry run of an endpoint's layouts on a captured email: which layout matches, the fields and line items it finds, and what earlier parses of its attachments held. Nothing is saved and no pages are used. Pass layouts to try them before saving them with update_endpoint.",
1280
+ {
1281
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1282
+ requestId: import_zod2.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1283
+ layouts: import_zod2.z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1284
+ },
1285
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1286
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1287
+ const holder = { ...preview };
1288
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1289
+ return jsonContent(holder);
1290
+ })
1291
+ );
1176
1292
  server.tool(
1177
1293
  "preview_response",
1178
1294
  "Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
@@ -2073,7 +2189,8 @@ var AgentSession = class {
2073
2189
  baseUrl: this.baseUrl,
2074
2190
  webhookUrl: this.webhookUrl,
2075
2191
  clientName: this.clientName,
2076
- credentialsFile: this.credentialsFile
2192
+ credentialsFile: this.credentialsFile,
2193
+ userAgent: MCP_USER_AGENT
2077
2194
  };
2078
2195
  }
2079
2196
  /**
@@ -2166,6 +2283,7 @@ function registerSandboxTools(server, options = {}) {
2166
2283
  baseUrl,
2167
2284
  webhookUrl,
2168
2285
  credentialsFile: options.credentialsFile,
2286
+ userAgent: MCP_USER_AGENT,
2169
2287
  // The human disconnected the agent: back to the sandbox, unless a
2170
2288
  // newer connection is already on display.
2171
2289
  onRevoked: () => queueMicrotask(() => {
@@ -2327,7 +2445,8 @@ function sandboxToolSet(server, session, showAccount) {
2327
2445
  webhookUrl: session.webhookUrl,
2328
2446
  email: normalized,
2329
2447
  clientName: session.clientName,
2330
- credentialsFile: session.credentialsFile
2448
+ credentialsFile: session.credentialsFile,
2449
+ userAgent: MCP_USER_AGENT
2331
2450
  }));
2332
2451
  session.connect = { via: "connect", pending, email: normalized };
2333
2452
  attempt = pending;
@@ -2436,7 +2555,6 @@ function registerAgentRegistrationTools(server) {
2436
2555
  }
2437
2556
 
2438
2557
  // src/index.ts
2439
- var VERSION = true ? "1.15.1" : "0.0.0-dev";
2440
2558
  function createServer(options = {}) {
2441
2559
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2442
2560
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -2451,7 +2569,7 @@ function createServer(options = {}) {
2451
2569
  registerPrompts(server);
2452
2570
  return server;
2453
2571
  }
2454
- const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl });
2572
+ const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl, userAgent: MCP_USER_AGENT });
2455
2573
  registerTools(server, client);
2456
2574
  registerPrompts(server);
2457
2575
  registerResources(server, client);
package/dist/index.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  registerTools,
14
14
  waitForRequestShape,
15
15
  withErrorHandling
16
- } from "./chunk-RHQD2GRD.mjs";
16
+ } from "./chunk-ZAPNMGCE.mjs";
17
17
 
18
18
  // src/index.ts
19
19
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -260,6 +260,12 @@ import {
260
260
  SandboxClient,
261
261
  WebhooksCC
262
262
  } from "@webhooks-cc/sdk";
263
+
264
+ // src/version.ts
265
+ var VERSION = true ? "1.17.0" : "0.0.0-dev";
266
+ var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
267
+
268
+ // src/sandbox-tools.ts
263
269
  var SANDBOX = {
264
270
  maxEndpoints: 3,
265
271
  requestsPerEndpoint: 25,
@@ -291,7 +297,8 @@ var AgentSession = class {
291
297
  baseUrl: this.baseUrl,
292
298
  webhookUrl: this.webhookUrl,
293
299
  clientName: this.clientName,
294
- credentialsFile: this.credentialsFile
300
+ credentialsFile: this.credentialsFile,
301
+ userAgent: MCP_USER_AGENT
295
302
  };
296
303
  }
297
304
  /**
@@ -384,6 +391,7 @@ function registerSandboxTools(server, options = {}) {
384
391
  baseUrl,
385
392
  webhookUrl,
386
393
  credentialsFile: options.credentialsFile,
394
+ userAgent: MCP_USER_AGENT,
387
395
  // The human disconnected the agent: back to the sandbox, unless a
388
396
  // newer connection is already on display.
389
397
  onRevoked: () => queueMicrotask(() => {
@@ -545,7 +553,8 @@ function sandboxToolSet(server, session, showAccount) {
545
553
  webhookUrl: session.webhookUrl,
546
554
  email: normalized,
547
555
  clientName: session.clientName,
548
- credentialsFile: session.credentialsFile
556
+ credentialsFile: session.credentialsFile,
557
+ userAgent: MCP_USER_AGENT
549
558
  }));
550
559
  session.connect = { via: "connect", pending, email: normalized };
551
560
  attempt = pending;
@@ -654,7 +663,6 @@ function registerAgentRegistrationTools(server) {
654
663
  }
655
664
 
656
665
  // src/index.ts
657
- var VERSION = true ? "1.15.1" : "0.0.0-dev";
658
666
  function createServer(options = {}) {
659
667
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
660
668
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -669,7 +677,7 @@ function createServer(options = {}) {
669
677
  registerPrompts(server);
670
678
  return server;
671
679
  }
672
- const client = new WebhooksCC2({ apiKey, webhookUrl, baseUrl });
680
+ const client = new WebhooksCC2({ apiKey, webhookUrl, baseUrl, userAgent: MCP_USER_AGENT });
673
681
  registerTools(server, client);
674
682
  registerPrompts(server);
675
683
  registerResources(server, client);
package/dist/tools.d.mts CHANGED
@@ -64,6 +64,11 @@ declare const createEndpointShape: {
64
64
  }, z.core.$strip>>;
65
65
  }, z.core.$strip>>>;
66
66
  notificationUrl: z.ZodOptional<z.ZodString>;
67
+ site: z.ZodOptional<z.ZodEnum<{
68
+ webhooks: "webhooks";
69
+ mailhooks: "mailhooks";
70
+ both: "both";
71
+ }>>;
67
72
  signingProvider: z.ZodOptional<z.ZodString>;
68
73
  signingSecret: z.ZodOptional<z.ZodString>;
69
74
  signingHeader: z.ZodOptional<z.ZodString>;
package/dist/tools.d.ts CHANGED
@@ -64,6 +64,11 @@ declare const createEndpointShape: {
64
64
  }, z.core.$strip>>;
65
65
  }, z.core.$strip>>>;
66
66
  notificationUrl: z.ZodOptional<z.ZodString>;
67
+ site: z.ZodOptional<z.ZodEnum<{
68
+ webhooks: "webhooks";
69
+ mailhooks: "mailhooks";
70
+ both: "both";
71
+ }>>;
67
72
  signingProvider: z.ZodOptional<z.ZodString>;
68
73
  signingSecret: z.ZodOptional<z.ZodString>;
69
74
  signingHeader: z.ZodOptional<z.ZodString>;
package/dist/tools.js CHANGED
@@ -109,6 +109,53 @@ function capLists(holder) {
109
109
  }
110
110
  }
111
111
  }
112
+ var MAX_PARSED_ITEMS = 20;
113
+ var MAX_PARSED_VALUE = 200;
114
+ var MAX_PARSED_SHEETS = 10;
115
+ function trimParsed(holder) {
116
+ const parsed = holder.parsed;
117
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
118
+ const next = { ...parsed };
119
+ if (Array.isArray(next.lineItems)) {
120
+ if (next.lineItems.length > MAX_PARSED_ITEMS) {
121
+ next.lineItemsTotal = next.lineItems.length;
122
+ }
123
+ next.lineItems = next.lineItems.slice(0, MAX_PARSED_ITEMS).map(
124
+ (row) => Object.fromEntries(
125
+ Object.entries(row).map(([key, value]) => [
126
+ key,
127
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
128
+ ])
129
+ )
130
+ );
131
+ }
132
+ if (next.fields && typeof next.fields === "object") {
133
+ next.fields = Object.fromEntries(
134
+ Object.entries(next.fields).map(([key, value]) => [
135
+ key,
136
+ typeof value === "string" ? sliceText(value, MAX_PARSED_VALUE) : value
137
+ ])
138
+ );
139
+ }
140
+ if (Array.isArray(next.attachments)) {
141
+ next.attachments = next.attachments.map((attachment) => {
142
+ const { sheets, text, ...rest } = attachment;
143
+ return {
144
+ ...rest,
145
+ ...Array.isArray(sheets) ? {
146
+ sheets: sheets.slice(0, MAX_PARSED_SHEETS).map((sheet) => ({
147
+ name: sheet.name,
148
+ rowCount: sheet.rowCount
149
+ })),
150
+ ...sheets.length > MAX_PARSED_SHEETS ? { sheetsTotal: sheets.length } : {}
151
+ } : {},
152
+ ...typeof text === "string" ? { textLength: text.length } : {}
153
+ };
154
+ });
155
+ }
156
+ next.trimmed = "Line items, sheet rows and attachment text were cut to fit the output.";
157
+ holder.parsed = next;
158
+ }
112
159
  function keepOnly(holder, keep) {
113
160
  for (const key of Object.keys(holder)) {
114
161
  if (!keep.includes(key)) delete holder[key];
@@ -174,6 +221,7 @@ function compactRequest(request) {
174
221
  const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
175
222
  const budget = MAX_OUTPUT - 128;
176
223
  shrinkToFit(root, budget, [
224
+ () => trimParsed(compact),
177
225
  () => omitHeaders(compact),
178
226
  () => trimSpam(email),
179
227
  () => capLists(email),
@@ -230,6 +278,34 @@ function conditionSchema(use) {
230
278
  });
231
279
  }
232
280
  var ruleConditionSchema = conditionSchema("response");
281
+ var emailFieldSchema = import_zod.z.object({
282
+ name: import_zod.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
283
+ from: import_zod.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
284
+ header: import_zod.z.string().optional().describe('The header name, for from "header"'),
285
+ pattern: import_zod.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
286
+ });
287
+ var layoutSchema = import_zod.z.object({
288
+ id: import_zod.z.string().describe("Stable id: 1 to 40 letters, digits, - or _"),
289
+ name: import_zod.z.string().max(100),
290
+ enabled: import_zod.z.boolean().optional().describe("Off: kept but never matched (default on)"),
291
+ match: import_zod.z.object({
292
+ logic: import_zod.z.enum(["and", "or"]).optional(),
293
+ conditions: import_zod.z.array(conditionSchema("filter")).max(10)
294
+ }).optional().describe("When it applies, e.g. the sender or subject; no conditions: every email"),
295
+ fields: import_zod.z.array(emailFieldSchema).max(20).optional().describe("Named values, as emailFields"),
296
+ lineItems: import_zod.z.object({
297
+ from: import_zod.z.enum(["text", "html", "attachment"]),
298
+ pattern: import_zod.z.string().optional().describe(
299
+ "text/html: RE2 with named groups found again and again, one row each, e.g. (?P<sku>[A-Z]+-\\d+)\\s+x(?P<qty>\\d+)"
300
+ ),
301
+ start: import_zod.z.string().optional().describe("text/html: search only after this (RE2)"),
302
+ end: import_zod.z.string().optional().describe("text/html: and before this (RE2)"),
303
+ attachment: import_zod.z.string().optional().describe("attachment: file name with * and ? (default: the first spreadsheet or CSV)"),
304
+ sheet: import_zod.z.string().optional().describe("attachment: sheet name (default: the first)"),
305
+ columns: import_zod.z.array(import_zod.z.object({ name: import_zod.z.string(), column: import_zod.z.string() })).max(30).optional().describe("attachment: { name, column: header text or letter }; default: every column")
306
+ }).nullable().optional().describe("Rows such as an order's products"),
307
+ parseAttachments: import_zod.z.boolean().optional().describe("Parse CSV, XLSX, ODS, PDF text and text files into the JSON; uses parsed pages")
308
+ });
233
309
  var destinationFields = {
234
310
  name: import_zod.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
235
311
  enabled: import_zod.z.boolean().optional().describe("Send to it (default true)"),
@@ -296,6 +372,9 @@ var mockResponseSchema = import_zod.z.object({
296
372
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
297
373
  )
298
374
  });
375
+ var endpointSiteSchema = import_zod.z.enum(["webhooks", "mailhooks", "both"]).describe(
376
+ "Which site's dashboard lists the endpoint: webhooks (webhooks.cc), mailhooks (mailhooks.cc) or both (the default when creating). Only the listing: its HTTP URL and email address work either way. Owner only to change."
377
+ );
299
378
  var createEndpointShape = {
300
379
  name: import_zod.z.string().optional().describe("Display name for the endpoint"),
301
380
  ephemeral: import_zod.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
@@ -307,6 +386,7 @@ var createEndpointShape = {
307
386
  notificationUrl: import_zod.z.string().url().optional().describe(
308
387
  "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
309
388
  ),
389
+ site: endpointSiteSchema.optional(),
310
390
  signingProvider: import_zod.z.string().optional().describe(
311
391
  "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
312
392
  ),
@@ -649,6 +729,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
649
729
  "subject",
650
730
  "from",
651
731
  "fields",
732
+ "parsed",
652
733
  "replyText",
653
734
  "size",
654
735
  "test",
@@ -685,6 +766,7 @@ function emailDetail(client, email, view, includeHtml) {
685
766
  ...includeHtml && html !== null ? { html } : {}
686
767
  };
687
768
  shrinkToFit(detail, MAX_BODY_SIZE, [
769
+ () => trimParsed(detail),
688
770
  () => omitHeaders(detail),
689
771
  () => trimSpam(detail),
690
772
  () => capExtracts(detail),
@@ -778,6 +860,7 @@ function registerTools(target, client, options = {}) {
778
860
  mockResponse,
779
861
  responseRules,
780
862
  notificationUrl,
863
+ site,
781
864
  signingProvider,
782
865
  signingSecret,
783
866
  signingHeader
@@ -799,7 +882,8 @@ function registerTools(target, client, options = {}) {
799
882
  expiresIn,
800
883
  mockResponse,
801
884
  responseRules,
802
- notificationUrl
885
+ notificationUrl,
886
+ site
803
887
  });
804
888
  if (signingProvider && signingSecret) {
805
889
  try {
@@ -884,6 +968,18 @@ function registerTools(target, client, options = {}) {
884
968
  return jsonContent(endpoint);
885
969
  })
886
970
  );
971
+ server.tool(
972
+ "favorite_endpoint",
973
+ "Mark an endpoint as one of your favourites, or unmark it with favorite: false. A favourite is your own choice, so endpoints shared with you through teams can be marked too; list_endpoints and get_endpoint show it as favorite. Idempotent.",
974
+ {
975
+ slug: import_zod.z.string().describe("The endpoint slug"),
976
+ favorite: import_zod.z.boolean().optional().describe("true marks it as a favourite (the default), false unmarks it")
977
+ },
978
+ withErrorHandling(async ({ slug, favorite }) => {
979
+ const result = favorite === false ? await client.endpoints.unfavorite(slug) : await client.endpoints.favorite(slug);
980
+ return jsonContent({ slug, favorite: result.favorite });
981
+ })
982
+ );
887
983
  server.tool(
888
984
  "update_endpoint",
889
985
  "Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
@@ -901,15 +997,12 @@ function registerTools(target, client, options = {}) {
901
997
  "Custom signature header name (only for generic-hmac provider), or null to clear"
902
998
  ),
903
999
  showEmailExtracts: import_zod.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only."),
904
- emailFields: import_zod.z.array(
905
- import_zod.z.object({
906
- name: import_zod.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
907
- from: import_zod.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
908
- header: import_zod.z.string().optional().describe('The header name, for from "header"'),
909
- pattern: import_zod.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
910
- })
911
- ).max(20).nullable().optional().describe(
1000
+ site: endpointSiteSchema.optional(),
1001
+ emailFields: import_zod.z.array(emailFieldSchema).max(20).nullable().optional().describe(
912
1002
  "Named fields picked out of every email: shown with it, `fields` in its JSON, read by forwarding templates and filters (email_field). Replaces the list; null or [] removes them. Owner only."
1003
+ ),
1004
+ layouts: import_zod.z.array(layoutSchema).max(50).nullable().optional().describe(
1005
+ "How the endpoint reads each sender's emails: the first layout whose match holds applies its fields, line items and attachment parsing; the result is `parsed` in the email's JSON, and deliveries wait for it. The plan uses the first 2 (Business 50). Replaces the list; null or [] removes them. Try them with preview_layouts first. Owner only."
913
1006
  )
914
1007
  },
915
1008
  withErrorHandling(
@@ -923,7 +1016,9 @@ function registerTools(target, client, options = {}) {
923
1016
  signingSecret,
924
1017
  signingHeader,
925
1018
  showEmailExtracts,
926
- emailFields
1019
+ site,
1020
+ emailFields,
1021
+ layouts
927
1022
  }) => {
928
1023
  const endpoint = await client.endpoints.update(slug, {
929
1024
  name,
@@ -934,12 +1029,29 @@ function registerTools(target, client, options = {}) {
934
1029
  signingSecret,
935
1030
  signingHeader,
936
1031
  showEmailExtracts,
937
- emailFields
1032
+ site,
1033
+ emailFields,
1034
+ layouts
938
1035
  });
939
1036
  return jsonContent(endpoint);
940
1037
  }
941
1038
  )
942
1039
  );
1040
+ server.tool(
1041
+ "preview_layouts",
1042
+ "Dry run of an endpoint's layouts on a captured email: which layout matches, the fields and line items it finds, and what earlier parses of its attachments held. Nothing is saved and no pages are used. Pass layouts to try them before saving them with update_endpoint.",
1043
+ {
1044
+ slug: import_zod.z.string().describe("The endpoint slug"),
1045
+ requestId: import_zod.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1046
+ layouts: import_zod.z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1047
+ },
1048
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1049
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1050
+ const holder = { ...preview };
1051
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1052
+ return jsonContent(holder);
1053
+ })
1054
+ );
943
1055
  server.tool(
944
1056
  "preview_response",
945
1057
  "Dry run of an endpoint's responses: which response rule answers a captured request, and the response with its {{ placeholders }} filled in, exactly as the receiver would send it. Nothing is sent. Pass mockResponse or responseRules to try settings before saving them with update_endpoint.",
package/dist/tools.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  textContent,
15
15
  waitForRequestShape,
16
16
  withErrorHandling
17
- } from "./chunk-RHQD2GRD.mjs";
17
+ } from "./chunk-ZAPNMGCE.mjs";
18
18
  export {
19
19
  REMOTE_MAX_WAIT_MS,
20
20
  REMOTE_MIN_POLL_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webhooks-cc/mcp",
3
- "version": "1.15.1",
3
+ "version": "1.17.0",
4
4
  "mcpName": "io.github.kroqdotdev/webhooks-cc",
5
5
  "description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
6
6
  "main": "dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@modelcontextprotocol/sdk": "^1.32.1",
46
46
  "zod": "^4.6.7",
47
- "@webhooks-cc/sdk": "1.19.0"
47
+ "@webhooks-cc/sdk": "1.21.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^26.6.5",