@webhooks-cc/mcp 1.15.1 → 1.16.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 45 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,7 +83,7 @@ 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 `47` tools, `3` prompts, and `3` resource surfaces with an API key, the same `47` tools and the prompts with a saved connection, and `11` sandbox tools otherwise. The remote server exposes `45` tools.
87
87
 
88
88
  ### Endpoint tools
89
89
 
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),
@@ -439,6 +487,34 @@ function conditionSchema(use) {
439
487
  });
440
488
  }
441
489
  var ruleConditionSchema = conditionSchema("response");
490
+ var emailFieldSchema = import_zod2.z.object({
491
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
492
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
493
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
494
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
495
+ });
496
+ var layoutSchema = import_zod2.z.object({
497
+ id: import_zod2.z.string().describe("Stable id: 1 to 40 letters, digits, - or _"),
498
+ name: import_zod2.z.string().max(100),
499
+ enabled: import_zod2.z.boolean().optional().describe("Off: kept but never matched (default on)"),
500
+ match: import_zod2.z.object({
501
+ logic: import_zod2.z.enum(["and", "or"]).optional(),
502
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
503
+ }).optional().describe("When it applies, e.g. the sender or subject; no conditions: every email"),
504
+ fields: import_zod2.z.array(emailFieldSchema).max(20).optional().describe("Named values, as emailFields"),
505
+ lineItems: import_zod2.z.object({
506
+ from: import_zod2.z.enum(["text", "html", "attachment"]),
507
+ pattern: import_zod2.z.string().optional().describe(
508
+ "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+)"
509
+ ),
510
+ start: import_zod2.z.string().optional().describe("text/html: search only after this (RE2)"),
511
+ end: import_zod2.z.string().optional().describe("text/html: and before this (RE2)"),
512
+ attachment: import_zod2.z.string().optional().describe("attachment: file name with * and ? (default: the first spreadsheet or CSV)"),
513
+ sheet: import_zod2.z.string().optional().describe("attachment: sheet name (default: the first)"),
514
+ 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")
515
+ }).nullable().optional().describe("Rows such as an order's products"),
516
+ parseAttachments: import_zod2.z.boolean().optional().describe("Parse CSV, XLSX, ODS, PDF text and text files into the JSON; uses parsed pages")
517
+ });
442
518
  var destinationFields = {
443
519
  name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
444
520
  enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
@@ -505,6 +581,9 @@ var mockResponseSchema = import_zod2.z.object({
505
581
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
506
582
  )
507
583
  });
584
+ var endpointSiteSchema = import_zod2.z.enum(["webhooks", "mailhooks", "both"]).describe(
585
+ "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."
586
+ );
508
587
  var createEndpointShape = {
509
588
  name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
510
589
  ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
@@ -516,6 +595,7 @@ var createEndpointShape = {
516
595
  notificationUrl: import_zod2.z.string().url().optional().describe(
517
596
  "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
518
597
  ),
598
+ site: endpointSiteSchema.optional(),
519
599
  signingProvider: import_zod2.z.string().optional().describe(
520
600
  "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
521
601
  ),
@@ -858,6 +938,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
858
938
  "subject",
859
939
  "from",
860
940
  "fields",
941
+ "parsed",
861
942
  "replyText",
862
943
  "size",
863
944
  "test",
@@ -894,6 +975,7 @@ function emailDetail(client, email, view, includeHtml) {
894
975
  ...includeHtml && html !== null ? { html } : {}
895
976
  };
896
977
  shrinkToFit(detail, MAX_BODY_SIZE, [
978
+ () => trimParsed(detail),
897
979
  () => omitHeaders(detail),
898
980
  () => trimSpam(detail),
899
981
  () => capExtracts(detail),
@@ -987,6 +1069,7 @@ function registerTools(target, client, options = {}) {
987
1069
  mockResponse,
988
1070
  responseRules,
989
1071
  notificationUrl,
1072
+ site,
990
1073
  signingProvider,
991
1074
  signingSecret,
992
1075
  signingHeader
@@ -1008,7 +1091,8 @@ function registerTools(target, client, options = {}) {
1008
1091
  expiresIn,
1009
1092
  mockResponse,
1010
1093
  responseRules,
1011
- notificationUrl
1094
+ notificationUrl,
1095
+ site
1012
1096
  });
1013
1097
  if (signingProvider && signingSecret) {
1014
1098
  try {
@@ -1110,15 +1194,12 @@ function registerTools(target, client, options = {}) {
1110
1194
  "Custom signature header name (only for generic-hmac provider), or null to clear"
1111
1195
  ),
1112
1196
  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(
1197
+ site: endpointSiteSchema.optional(),
1198
+ emailFields: import_zod2.z.array(emailFieldSchema).max(20).nullable().optional().describe(
1121
1199
  "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."
1200
+ ),
1201
+ layouts: import_zod2.z.array(layoutSchema).max(50).nullable().optional().describe(
1202
+ "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
1203
  )
1123
1204
  },
1124
1205
  withErrorHandling(
@@ -1132,7 +1213,9 @@ function registerTools(target, client, options = {}) {
1132
1213
  signingSecret,
1133
1214
  signingHeader,
1134
1215
  showEmailExtracts,
1135
- emailFields
1216
+ site,
1217
+ emailFields,
1218
+ layouts
1136
1219
  }) => {
1137
1220
  const endpoint = await client.endpoints.update(slug, {
1138
1221
  name,
@@ -1143,12 +1226,29 @@ function registerTools(target, client, options = {}) {
1143
1226
  signingSecret,
1144
1227
  signingHeader,
1145
1228
  showEmailExtracts,
1146
- emailFields
1229
+ site,
1230
+ emailFields,
1231
+ layouts
1147
1232
  });
1148
1233
  return jsonContent(endpoint);
1149
1234
  }
1150
1235
  )
1151
1236
  );
1237
+ server.tool(
1238
+ "preview_layouts",
1239
+ "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.",
1240
+ {
1241
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1242
+ requestId: import_zod2.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1243
+ layouts: import_zod2.z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1244
+ },
1245
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1246
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1247
+ const holder = { ...preview };
1248
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1249
+ return jsonContent(holder);
1250
+ })
1251
+ );
1152
1252
  server.tool(
1153
1253
  "preview_response",
1154
1254
  "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.",
@@ -2409,7 +2509,7 @@ function sandboxToolSet(server, session, showAccount) {
2409
2509
  }
2410
2510
 
2411
2511
  // src/index.ts
2412
- var VERSION = true ? "1.15.1" : "0.0.0-dev";
2512
+ var VERSION = true ? "1.16.0" : "0.0.0-dev";
2413
2513
  function createServer(options = {}) {
2414
2514
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2415
2515
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -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 {
@@ -892,15 +976,12 @@ function registerTools(target, client, options = {}) {
892
976
  "Custom signature header name (only for generic-hmac provider), or null to clear"
893
977
  ),
894
978
  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(
979
+ site: endpointSiteSchema.optional(),
980
+ emailFields: z.array(emailFieldSchema).max(20).nullable().optional().describe(
903
981
  "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."
982
+ ),
983
+ layouts: z.array(layoutSchema).max(50).nullable().optional().describe(
984
+ "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
985
  )
905
986
  },
906
987
  withErrorHandling(
@@ -914,7 +995,9 @@ function registerTools(target, client, options = {}) {
914
995
  signingSecret,
915
996
  signingHeader,
916
997
  showEmailExtracts,
917
- emailFields
998
+ site,
999
+ emailFields,
1000
+ layouts
918
1001
  }) => {
919
1002
  const endpoint = await client.endpoints.update(slug, {
920
1003
  name,
@@ -925,12 +1008,29 @@ function registerTools(target, client, options = {}) {
925
1008
  signingSecret,
926
1009
  signingHeader,
927
1010
  showEmailExtracts,
928
- emailFields
1011
+ site,
1012
+ emailFields,
1013
+ layouts
929
1014
  });
930
1015
  return jsonContent(endpoint);
931
1016
  }
932
1017
  )
933
1018
  );
1019
+ server.tool(
1020
+ "preview_layouts",
1021
+ "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.",
1022
+ {
1023
+ slug: z.string().describe("The endpoint slug"),
1024
+ requestId: z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1025
+ layouts: z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1026
+ },
1027
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1028
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1029
+ const holder = { ...preview };
1030
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1031
+ return jsonContent(holder);
1032
+ })
1033
+ );
934
1034
  server.tool(
935
1035
  "preview_response",
936
1036
  "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),
@@ -463,6 +511,34 @@ function conditionSchema(use) {
463
511
  });
464
512
  }
465
513
  var ruleConditionSchema = conditionSchema("response");
514
+ var emailFieldSchema = import_zod2.z.object({
515
+ name: import_zod2.z.string().describe("Letters, digits and _, starting with a letter: {{ fields.name }}"),
516
+ from: import_zod2.z.enum(["subject", "text", "html", "from", "to", "header"]).describe("What it reads; html is the HTML as text"),
517
+ header: import_zod2.z.string().optional().describe('The header name, for from "header"'),
518
+ pattern: import_zod2.z.string().describe("RE2 regular expression; the first group is the value (no lookarounds)")
519
+ });
520
+ var layoutSchema = import_zod2.z.object({
521
+ id: import_zod2.z.string().describe("Stable id: 1 to 40 letters, digits, - or _"),
522
+ name: import_zod2.z.string().max(100),
523
+ enabled: import_zod2.z.boolean().optional().describe("Off: kept but never matched (default on)"),
524
+ match: import_zod2.z.object({
525
+ logic: import_zod2.z.enum(["and", "or"]).optional(),
526
+ conditions: import_zod2.z.array(conditionSchema("filter")).max(10)
527
+ }).optional().describe("When it applies, e.g. the sender or subject; no conditions: every email"),
528
+ fields: import_zod2.z.array(emailFieldSchema).max(20).optional().describe("Named values, as emailFields"),
529
+ lineItems: import_zod2.z.object({
530
+ from: import_zod2.z.enum(["text", "html", "attachment"]),
531
+ pattern: import_zod2.z.string().optional().describe(
532
+ "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+)"
533
+ ),
534
+ start: import_zod2.z.string().optional().describe("text/html: search only after this (RE2)"),
535
+ end: import_zod2.z.string().optional().describe("text/html: and before this (RE2)"),
536
+ attachment: import_zod2.z.string().optional().describe("attachment: file name with * and ? (default: the first spreadsheet or CSV)"),
537
+ sheet: import_zod2.z.string().optional().describe("attachment: sheet name (default: the first)"),
538
+ 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")
539
+ }).nullable().optional().describe("Rows such as an order's products"),
540
+ parseAttachments: import_zod2.z.boolean().optional().describe("Parse CSV, XLSX, ODS, PDF text and text files into the JSON; uses parsed pages")
541
+ });
466
542
  var destinationFields = {
467
543
  name: import_zod2.z.string().max(100).nullable().optional().describe("A name for the list and the log"),
468
544
  enabled: import_zod2.z.boolean().optional().describe("Send to it (default true)"),
@@ -529,6 +605,9 @@ var mockResponseSchema = import_zod2.z.object({
529
605
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
530
606
  )
531
607
  });
608
+ var endpointSiteSchema = import_zod2.z.enum(["webhooks", "mailhooks", "both"]).describe(
609
+ "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."
610
+ );
532
611
  var createEndpointShape = {
533
612
  name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
534
613
  ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
@@ -540,6 +619,7 @@ var createEndpointShape = {
540
619
  notificationUrl: import_zod2.z.string().url().optional().describe(
541
620
  "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
542
621
  ),
622
+ site: endpointSiteSchema.optional(),
543
623
  signingProvider: import_zod2.z.string().optional().describe(
544
624
  "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
545
625
  ),
@@ -882,6 +962,7 @@ var ESSENTIAL_DETAIL_FIELDS = [
882
962
  "subject",
883
963
  "from",
884
964
  "fields",
965
+ "parsed",
885
966
  "replyText",
886
967
  "size",
887
968
  "test",
@@ -918,6 +999,7 @@ function emailDetail(client, email, view, includeHtml) {
918
999
  ...includeHtml && html !== null ? { html } : {}
919
1000
  };
920
1001
  shrinkToFit(detail, MAX_BODY_SIZE, [
1002
+ () => trimParsed(detail),
921
1003
  () => omitHeaders(detail),
922
1004
  () => trimSpam(detail),
923
1005
  () => capExtracts(detail),
@@ -1011,6 +1093,7 @@ function registerTools(target, client, options = {}) {
1011
1093
  mockResponse,
1012
1094
  responseRules,
1013
1095
  notificationUrl,
1096
+ site,
1014
1097
  signingProvider,
1015
1098
  signingSecret,
1016
1099
  signingHeader
@@ -1032,7 +1115,8 @@ function registerTools(target, client, options = {}) {
1032
1115
  expiresIn,
1033
1116
  mockResponse,
1034
1117
  responseRules,
1035
- notificationUrl
1118
+ notificationUrl,
1119
+ site
1036
1120
  });
1037
1121
  if (signingProvider && signingSecret) {
1038
1122
  try {
@@ -1134,15 +1218,12 @@ function registerTools(target, client, options = {}) {
1134
1218
  "Custom signature header name (only for generic-hmac provider), or null to clear"
1135
1219
  ),
1136
1220
  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(
1221
+ site: endpointSiteSchema.optional(),
1222
+ emailFields: import_zod2.z.array(emailFieldSchema).max(20).nullable().optional().describe(
1145
1223
  "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."
1224
+ ),
1225
+ layouts: import_zod2.z.array(layoutSchema).max(50).nullable().optional().describe(
1226
+ "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
1227
  )
1147
1228
  },
1148
1229
  withErrorHandling(
@@ -1156,7 +1237,9 @@ function registerTools(target, client, options = {}) {
1156
1237
  signingSecret,
1157
1238
  signingHeader,
1158
1239
  showEmailExtracts,
1159
- emailFields
1240
+ site,
1241
+ emailFields,
1242
+ layouts
1160
1243
  }) => {
1161
1244
  const endpoint = await client.endpoints.update(slug, {
1162
1245
  name,
@@ -1167,12 +1250,29 @@ function registerTools(target, client, options = {}) {
1167
1250
  signingSecret,
1168
1251
  signingHeader,
1169
1252
  showEmailExtracts,
1170
- emailFields
1253
+ site,
1254
+ emailFields,
1255
+ layouts
1171
1256
  });
1172
1257
  return jsonContent(endpoint);
1173
1258
  }
1174
1259
  )
1175
1260
  );
1261
+ server.tool(
1262
+ "preview_layouts",
1263
+ "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.",
1264
+ {
1265
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1266
+ requestId: import_zod2.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1267
+ layouts: import_zod2.z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1268
+ },
1269
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1270
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1271
+ const holder = { ...preview };
1272
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1273
+ return jsonContent(holder);
1274
+ })
1275
+ );
1176
1276
  server.tool(
1177
1277
  "preview_response",
1178
1278
  "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.",
@@ -2436,7 +2536,7 @@ function registerAgentRegistrationTools(server) {
2436
2536
  }
2437
2537
 
2438
2538
  // src/index.ts
2439
- var VERSION = true ? "1.15.1" : "0.0.0-dev";
2539
+ var VERSION = true ? "1.16.0" : "0.0.0-dev";
2440
2540
  function createServer(options = {}) {
2441
2541
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2442
2542
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
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-4LEPLCW5.mjs";
17
17
 
18
18
  // src/index.ts
19
19
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -654,7 +654,7 @@ function registerAgentRegistrationTools(server) {
654
654
  }
655
655
 
656
656
  // src/index.ts
657
- var VERSION = true ? "1.15.1" : "0.0.0-dev";
657
+ var VERSION = true ? "1.16.0" : "0.0.0-dev";
658
658
  function createServer(options = {}) {
659
659
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
660
660
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
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 {
@@ -901,15 +985,12 @@ function registerTools(target, client, options = {}) {
901
985
  "Custom signature header name (only for generic-hmac provider), or null to clear"
902
986
  ),
903
987
  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(
988
+ site: endpointSiteSchema.optional(),
989
+ emailFields: import_zod.z.array(emailFieldSchema).max(20).nullable().optional().describe(
912
990
  "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."
991
+ ),
992
+ layouts: import_zod.z.array(layoutSchema).max(50).nullable().optional().describe(
993
+ "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
994
  )
914
995
  },
915
996
  withErrorHandling(
@@ -923,7 +1004,9 @@ function registerTools(target, client, options = {}) {
923
1004
  signingSecret,
924
1005
  signingHeader,
925
1006
  showEmailExtracts,
926
- emailFields
1007
+ site,
1008
+ emailFields,
1009
+ layouts
927
1010
  }) => {
928
1011
  const endpoint = await client.endpoints.update(slug, {
929
1012
  name,
@@ -934,12 +1017,29 @@ function registerTools(target, client, options = {}) {
934
1017
  signingSecret,
935
1018
  signingHeader,
936
1019
  showEmailExtracts,
937
- emailFields
1020
+ site,
1021
+ emailFields,
1022
+ layouts
938
1023
  });
939
1024
  return jsonContent(endpoint);
940
1025
  }
941
1026
  )
942
1027
  );
1028
+ server.tool(
1029
+ "preview_layouts",
1030
+ "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.",
1031
+ {
1032
+ slug: import_zod.z.string().describe("The endpoint slug"),
1033
+ requestId: import_zod.z.string().optional().describe("A captured email on the endpoint (default: the newest)"),
1034
+ layouts: import_zod.z.array(layoutSchema).max(50).optional().describe("Unsaved layouts to try")
1035
+ },
1036
+ withErrorHandling(async ({ slug, requestId, layouts }) => {
1037
+ const preview = await client.endpoints.previewLayouts(slug, { requestId, layouts });
1038
+ const holder = { ...preview };
1039
+ shrinkToFit(holder, MAX_BODY_SIZE, [() => trimParsed(holder)]);
1040
+ return jsonContent(holder);
1041
+ })
1042
+ );
943
1043
  server.tool(
944
1044
  "preview_response",
945
1045
  "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-4LEPLCW5.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.16.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.20.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^26.6.5",