@webhooks-cc/mcp 1.9.0 → 1.10.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 +22 -1
- package/dist/bin/mcp.js +433 -20
- package/dist/index.js +433 -20
- package/dist/index.mjs +436 -21
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Get your API key at [webhooks.cc/account](https://webhooks.cc/account).
|
|
|
58
58
|
|
|
59
59
|
## What it exposes
|
|
60
60
|
|
|
61
|
-
The server exposes `
|
|
61
|
+
The server exposes `44` tools, `3` prompts, and `3` resource surfaces.
|
|
62
62
|
|
|
63
63
|
### Endpoint tools
|
|
64
64
|
|
|
@@ -84,6 +84,27 @@ The server exposes `35` tools, `3` prompts, and `3` resource surfaces.
|
|
|
84
84
|
- `verify_signature`
|
|
85
85
|
- `clear_requests`
|
|
86
86
|
|
|
87
|
+
### Email tools
|
|
88
|
+
|
|
89
|
+
Every endpoint on an account receives email at `{slug}@mailhooks.cc` (and `{slug}+{tag}@mailhooks.cc`). These tools read it, with the one-time code and main link already picked out, unless the endpoint's owner turned that off:
|
|
90
|
+
|
|
91
|
+
- `list_emails`
|
|
92
|
+
- `get_email`
|
|
93
|
+
- `wait_for_email`
|
|
94
|
+
- `send_test_email`
|
|
95
|
+
|
|
96
|
+
The request tools and resources return emails too (filter with `kind`), without the raw message or the HTML part and with the text cut at 8,000 characters; `get_email` returns the HTML.
|
|
97
|
+
|
|
98
|
+
### Forwarding tools
|
|
99
|
+
|
|
100
|
+
Forward every captured email to a URL as signed JSON (owner only):
|
|
101
|
+
|
|
102
|
+
- `configure_forwarding`
|
|
103
|
+
- `get_forwarding_secret` (returns the `whsec_` secret; with `rotate: true`, replaces it)
|
|
104
|
+
- `test_forwarding`
|
|
105
|
+
- `list_deliveries`
|
|
106
|
+
- `redeliver_email`
|
|
107
|
+
|
|
87
108
|
### Send and test tools
|
|
88
109
|
|
|
89
110
|
- `send_webhook`
|
package/dist/bin/mcp.js
CHANGED
|
@@ -120,6 +120,149 @@ function registerPrompts(server) {
|
|
|
120
120
|
|
|
121
121
|
// src/resources.ts
|
|
122
122
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
123
|
+
|
|
124
|
+
// src/compact.ts
|
|
125
|
+
var MAX_OUTPUT = 32768;
|
|
126
|
+
var MAX_EMAIL_TEXT = 8e3;
|
|
127
|
+
var MAX_EXTRACTS = 10;
|
|
128
|
+
var MAX_LINK_TEXT = 200;
|
|
129
|
+
var MAX_LINK_URL = 2e3;
|
|
130
|
+
var MAX_LIST_ITEMS = 5;
|
|
131
|
+
var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
|
|
132
|
+
var TEXT_FLOOR = 2e3;
|
|
133
|
+
function jsonSize(value) {
|
|
134
|
+
return JSON.stringify(value, null, 2).length;
|
|
135
|
+
}
|
|
136
|
+
function sliceText(text, end) {
|
|
137
|
+
if (end <= 0) return "";
|
|
138
|
+
if (end >= text.length) return text;
|
|
139
|
+
const last = text.charCodeAt(end - 1);
|
|
140
|
+
return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
|
|
141
|
+
}
|
|
142
|
+
function cutEmailText(text) {
|
|
143
|
+
if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
|
|
144
|
+
return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
|
|
145
|
+
}
|
|
146
|
+
function shrinkToFit(root, budget, steps) {
|
|
147
|
+
for (const step of steps) {
|
|
148
|
+
if (jsonSize(root) <= budget) return;
|
|
149
|
+
step();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function omitHeaders(holder) {
|
|
153
|
+
const headers = holder.headers;
|
|
154
|
+
if (headers === null || typeof headers !== "object") return;
|
|
155
|
+
const count = Object.keys(headers).length;
|
|
156
|
+
if (count === 0) return;
|
|
157
|
+
holder.headers = {};
|
|
158
|
+
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
159
|
+
}
|
|
160
|
+
function capExtracts(holder) {
|
|
161
|
+
if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
|
|
162
|
+
holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
|
|
163
|
+
holder.codesTruncated = true;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(holder.links)) {
|
|
166
|
+
if (holder.links.length > MAX_EXTRACTS) {
|
|
167
|
+
holder.links = holder.links.slice(0, MAX_EXTRACTS);
|
|
168
|
+
holder.linksTruncated = true;
|
|
169
|
+
}
|
|
170
|
+
holder.links = holder.links.map((link) => ({
|
|
171
|
+
...link,
|
|
172
|
+
...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
|
|
173
|
+
...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function capLists(holder) {
|
|
178
|
+
for (const key of EMAIL_LISTS) {
|
|
179
|
+
const list = holder[key];
|
|
180
|
+
if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
|
|
181
|
+
holder[key] = list.slice(0, MAX_LIST_ITEMS);
|
|
182
|
+
holder[`${key}Total`] = list.length;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function keepOnly(holder, keep) {
|
|
187
|
+
for (const key of Object.keys(holder)) {
|
|
188
|
+
if (!keep.includes(key)) delete holder[key];
|
|
189
|
+
}
|
|
190
|
+
holder.trimmed = "The email was too big for the output, so only these fields are shown.";
|
|
191
|
+
}
|
|
192
|
+
function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
|
|
193
|
+
const value = holder[key];
|
|
194
|
+
if (typeof value !== "string" || value.length <= floor) return;
|
|
195
|
+
const flagged = holder[flag] === true;
|
|
196
|
+
holder[key] = "";
|
|
197
|
+
holder[flag] = true;
|
|
198
|
+
const room = budget - jsonSize(root) + 2;
|
|
199
|
+
let low = 0;
|
|
200
|
+
let high = value.length;
|
|
201
|
+
while (low < high) {
|
|
202
|
+
const mid = Math.ceil((low + high) / 2);
|
|
203
|
+
if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
|
|
204
|
+
else high = mid - 1;
|
|
205
|
+
}
|
|
206
|
+
const kept = sliceText(value, Math.max(low, floor));
|
|
207
|
+
holder[key] = kept;
|
|
208
|
+
if (kept.length === value.length && !flagged) delete holder[flag];
|
|
209
|
+
}
|
|
210
|
+
function keepFirst(holder, key) {
|
|
211
|
+
const list = holder[key];
|
|
212
|
+
if (!Array.isArray(list) || list.length <= 1) return;
|
|
213
|
+
holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
|
|
214
|
+
holder[key] = list.slice(0, 1);
|
|
215
|
+
}
|
|
216
|
+
var ESSENTIAL_EMAIL_FIELDS = [
|
|
217
|
+
"subject",
|
|
218
|
+
"from",
|
|
219
|
+
"fromTotal",
|
|
220
|
+
"tag",
|
|
221
|
+
"date",
|
|
222
|
+
"text",
|
|
223
|
+
"textTruncated",
|
|
224
|
+
"htmlSize"
|
|
225
|
+
];
|
|
226
|
+
function compactRequest(request) {
|
|
227
|
+
if (request.kind !== "email") return request;
|
|
228
|
+
const compact = { ...request };
|
|
229
|
+
delete compact.body;
|
|
230
|
+
delete compact.bodyRaw;
|
|
231
|
+
compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
|
|
232
|
+
if (!request.email) return compact;
|
|
233
|
+
const { html, ...rest } = request.email;
|
|
234
|
+
const { text, cut } = cutEmailText(rest.text);
|
|
235
|
+
const restoreText = () => {
|
|
236
|
+
email.text = text;
|
|
237
|
+
if (cut) email.textTruncated = true;
|
|
238
|
+
else delete email.textTruncated;
|
|
239
|
+
};
|
|
240
|
+
const email = {
|
|
241
|
+
...rest,
|
|
242
|
+
text,
|
|
243
|
+
...cut ? { textTruncated: true } : {},
|
|
244
|
+
htmlSize: html?.length ?? 0
|
|
245
|
+
};
|
|
246
|
+
compact.email = email;
|
|
247
|
+
const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
|
|
248
|
+
const budget = MAX_OUTPUT - 128;
|
|
249
|
+
shrinkToFit(root, budget, [
|
|
250
|
+
() => omitHeaders(compact),
|
|
251
|
+
() => capLists(email),
|
|
252
|
+
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
253
|
+
// Down to the essentials there may be room for more of the text again.
|
|
254
|
+
() => {
|
|
255
|
+
keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
|
|
256
|
+
keepFirst(email, "from");
|
|
257
|
+
restoreText();
|
|
258
|
+
cutStringToFit(root, email, "text", "textTruncated", budget);
|
|
259
|
+
},
|
|
260
|
+
() => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
|
|
261
|
+
]);
|
|
262
|
+
return compact;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/resources.ts
|
|
123
266
|
var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
|
|
124
267
|
var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
|
|
125
268
|
var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
|
|
@@ -217,7 +360,7 @@ function registerResources(server, client) {
|
|
|
217
360
|
]);
|
|
218
361
|
return jsonResource(uri.toString(), {
|
|
219
362
|
endpoint,
|
|
220
|
-
requests
|
|
363
|
+
requests: requests.map(compactRequest)
|
|
221
364
|
});
|
|
222
365
|
}
|
|
223
366
|
);
|
|
@@ -235,7 +378,7 @@ function registerResources(server, client) {
|
|
|
235
378
|
async (uri, variables) => {
|
|
236
379
|
const id = variableToString(variables.id, "id");
|
|
237
380
|
const request = await client.requests.get(id);
|
|
238
|
-
return jsonResource(uri.toString(), request);
|
|
381
|
+
return jsonResource(uri.toString(), compactRequest(request));
|
|
239
382
|
}
|
|
240
383
|
);
|
|
241
384
|
}
|
|
@@ -243,7 +386,7 @@ function registerResources(server, client) {
|
|
|
243
386
|
// src/tools.ts
|
|
244
387
|
var import_zod2 = require("zod");
|
|
245
388
|
var import_sdk = require("@webhooks-cc/sdk");
|
|
246
|
-
var MAX_BODY_SIZE =
|
|
389
|
+
var MAX_BODY_SIZE = MAX_OUTPUT;
|
|
247
390
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
248
391
|
var TIME_SEPARATOR = " \u2014 ";
|
|
249
392
|
var httpUrlSchema = import_zod2.z.string().url().refine(
|
|
@@ -258,6 +401,8 @@ var httpUrlSchema = import_zod2.z.string().url().refine(
|
|
|
258
401
|
{ message: "Only http and https URLs are supported" }
|
|
259
402
|
);
|
|
260
403
|
var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
|
|
404
|
+
var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
|
|
405
|
+
var EMAIL_SCAN_LIMIT = 100;
|
|
261
406
|
var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
|
|
262
407
|
var ruleConditionSchema = import_zod2.z.object({
|
|
263
408
|
field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
|
|
@@ -337,6 +482,24 @@ function serializeJson(value, limit = MAX_BODY_SIZE) {
|
|
|
337
482
|
return full.slice(0, limit) + `
|
|
338
483
|
... [truncated, ${full.length} chars total]`;
|
|
339
484
|
}
|
|
485
|
+
function fitArrayField(value, key) {
|
|
486
|
+
const items = value[key];
|
|
487
|
+
if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
|
|
488
|
+
const cut = (count) => ({
|
|
489
|
+
...value,
|
|
490
|
+
[key]: items.slice(0, count),
|
|
491
|
+
truncated: true,
|
|
492
|
+
returned: count
|
|
493
|
+
});
|
|
494
|
+
let low = 0;
|
|
495
|
+
let high = items.length;
|
|
496
|
+
while (low < high) {
|
|
497
|
+
const mid = Math.ceil((low + high) / 2);
|
|
498
|
+
if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
|
|
499
|
+
else high = mid - 1;
|
|
500
|
+
}
|
|
501
|
+
return cut(low);
|
|
502
|
+
}
|
|
340
503
|
function jsonContent(value) {
|
|
341
504
|
return textContent(serializeJson(value));
|
|
342
505
|
}
|
|
@@ -485,6 +648,99 @@ async function summarizeResponse(response) {
|
|
|
485
648
|
body: await readBodyTruncated(response)
|
|
486
649
|
};
|
|
487
650
|
}
|
|
651
|
+
async function emailEndpointView(client, slug) {
|
|
652
|
+
const endpoint = await client.endpoints.get(slug);
|
|
653
|
+
return {
|
|
654
|
+
endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
|
|
655
|
+
includeExtracts: endpoint.showEmailExtracts !== false
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function emailSlugFromAddress(address) {
|
|
659
|
+
const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
|
|
660
|
+
return local.split("+")[0].toLowerCase();
|
|
661
|
+
}
|
|
662
|
+
var MAX_SUMMARY_LINK = 8e3;
|
|
663
|
+
function boundedLink(link, max) {
|
|
664
|
+
if (link == null || link.length <= max) return { link: link ?? null };
|
|
665
|
+
return {
|
|
666
|
+
link: null,
|
|
667
|
+
linkOmitted: `The main link is ${link.length} characters long, too long for the output`
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
var MAX_SUMMARY_FIELD = 1e3;
|
|
671
|
+
function summarizeEmail(email, includeExtracts) {
|
|
672
|
+
const from = email.email.from[0];
|
|
673
|
+
const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
|
|
674
|
+
const subject = email.email.subject;
|
|
675
|
+
return {
|
|
676
|
+
id: email.id,
|
|
677
|
+
receivedAt: new Date(email.receivedAt).toISOString(),
|
|
678
|
+
address: email.path,
|
|
679
|
+
tag: email.email.tag,
|
|
680
|
+
subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
|
|
681
|
+
from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
|
|
682
|
+
...includeExtracts ? { code: (0, import_sdk.extractCode)(email), ...boundedLink((0, import_sdk.extractLink)(email), MAX_SUMMARY_LINK) } : {},
|
|
683
|
+
attachments: email.email.attachments.length
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
var ESSENTIAL_DETAIL_FIELDS = [
|
|
687
|
+
"code",
|
|
688
|
+
"link",
|
|
689
|
+
"id",
|
|
690
|
+
"endpoint",
|
|
691
|
+
"receivedAt",
|
|
692
|
+
"address",
|
|
693
|
+
"tag",
|
|
694
|
+
"subject",
|
|
695
|
+
"from",
|
|
696
|
+
"size",
|
|
697
|
+
"test",
|
|
698
|
+
"text",
|
|
699
|
+
"textTruncated",
|
|
700
|
+
"htmlSize",
|
|
701
|
+
"html",
|
|
702
|
+
"htmlTruncated"
|
|
703
|
+
];
|
|
704
|
+
function emailDetail(client, email, view, includeHtml) {
|
|
705
|
+
const { data } = client.emails.toJson(email, {
|
|
706
|
+
endpoint: view.endpoint,
|
|
707
|
+
includeExtracts: view.includeExtracts
|
|
708
|
+
});
|
|
709
|
+
const { html, text: fullText, ...rest } = data;
|
|
710
|
+
const { text, cut } = cutEmailText(fullText ?? null);
|
|
711
|
+
const restoreText = () => {
|
|
712
|
+
detail.text = text;
|
|
713
|
+
if (cut) detail.textTruncated = true;
|
|
714
|
+
else delete detail.textTruncated;
|
|
715
|
+
};
|
|
716
|
+
const detail = {
|
|
717
|
+
...view.includeExtracts ? { code: (0, import_sdk.extractCode)(email), link: (0, import_sdk.extractLink)(email) } : {},
|
|
718
|
+
...rest,
|
|
719
|
+
text,
|
|
720
|
+
...cut ? { textTruncated: true } : {},
|
|
721
|
+
htmlSize: html?.length ?? 0,
|
|
722
|
+
...includeHtml && html !== null ? { html } : {}
|
|
723
|
+
};
|
|
724
|
+
shrinkToFit(detail, MAX_BODY_SIZE, [
|
|
725
|
+
() => omitHeaders(detail),
|
|
726
|
+
() => capExtracts(detail),
|
|
727
|
+
() => capLists(detail),
|
|
728
|
+
() => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
|
|
729
|
+
() => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
|
|
730
|
+
// Down to the essentials (and without a main link too long to use) there
|
|
731
|
+
// may be room for more of the text again.
|
|
732
|
+
() => {
|
|
733
|
+
keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
|
|
734
|
+
if (view.includeExtracts) {
|
|
735
|
+
Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
|
|
736
|
+
}
|
|
737
|
+
restoreText();
|
|
738
|
+
cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
|
|
739
|
+
},
|
|
740
|
+
() => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
|
|
741
|
+
]);
|
|
742
|
+
return detail;
|
|
743
|
+
}
|
|
488
744
|
function registerTools(server, client) {
|
|
489
745
|
server.tool(
|
|
490
746
|
"create_endpoint",
|
|
@@ -628,7 +884,7 @@ function registerTools(server, client) {
|
|
|
628
884
|
);
|
|
629
885
|
server.tool(
|
|
630
886
|
"update_endpoint",
|
|
631
|
-
"Update an endpoint name, mock response, conditional response rules, or
|
|
887
|
+
"Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
|
|
632
888
|
{
|
|
633
889
|
slug: import_zod2.z.string().describe("The endpoint slug to update"),
|
|
634
890
|
name: import_zod2.z.string().optional().describe("New display name"),
|
|
@@ -641,7 +897,8 @@ function registerTools(server, client) {
|
|
|
641
897
|
signingSecret: import_zod2.z.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
|
|
642
898
|
signingHeader: import_zod2.z.string().nullable().optional().describe(
|
|
643
899
|
"Custom signature header name (only for generic-hmac provider), or null to clear"
|
|
644
|
-
)
|
|
900
|
+
),
|
|
901
|
+
showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
|
|
645
902
|
},
|
|
646
903
|
withErrorHandling(
|
|
647
904
|
async ({
|
|
@@ -652,7 +909,8 @@ function registerTools(server, client) {
|
|
|
652
909
|
notificationUrl,
|
|
653
910
|
signingProvider,
|
|
654
911
|
signingSecret,
|
|
655
|
-
signingHeader
|
|
912
|
+
signingHeader,
|
|
913
|
+
showEmailExtracts
|
|
656
914
|
}) => {
|
|
657
915
|
const endpoint = await client.endpoints.update(slug, {
|
|
658
916
|
name,
|
|
@@ -661,7 +919,8 @@ function registerTools(server, client) {
|
|
|
661
919
|
notificationUrl,
|
|
662
920
|
signingProvider,
|
|
663
921
|
signingSecret,
|
|
664
|
-
signingHeader
|
|
922
|
+
signingHeader,
|
|
923
|
+
showEmailExtracts
|
|
665
924
|
});
|
|
666
925
|
return jsonContent(endpoint);
|
|
667
926
|
}
|
|
@@ -773,15 +1032,16 @@ function registerTools(server, client) {
|
|
|
773
1032
|
);
|
|
774
1033
|
server.tool(
|
|
775
1034
|
"list_requests",
|
|
776
|
-
"List recent captured requests for an endpoint.",
|
|
1035
|
+
"List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
|
|
777
1036
|
{
|
|
778
1037
|
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
779
1038
|
limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
|
|
780
|
-
since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms")
|
|
1039
|
+
since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms"),
|
|
1040
|
+
kind: kindSchema
|
|
781
1041
|
},
|
|
782
|
-
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
783
|
-
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
784
|
-
return jsonContent(requests);
|
|
1042
|
+
withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
|
|
1043
|
+
const requests = await client.requests.list(endpointSlug, { limit, since, kind });
|
|
1044
|
+
return jsonContent(requests.map(compactRequest));
|
|
785
1045
|
})
|
|
786
1046
|
);
|
|
787
1047
|
server.tool(
|
|
@@ -790,6 +1050,7 @@ function registerTools(server, client) {
|
|
|
790
1050
|
{
|
|
791
1051
|
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
792
1052
|
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
1053
|
+
kind: kindSchema,
|
|
793
1054
|
q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
|
|
794
1055
|
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
795
1056
|
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
|
|
@@ -797,10 +1058,11 @@ function registerTools(server, client) {
|
|
|
797
1058
|
offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("Result offset"),
|
|
798
1059
|
order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
|
|
799
1060
|
},
|
|
800
|
-
withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
|
|
1061
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
|
|
801
1062
|
const results = await client.requests.search({
|
|
802
1063
|
slug,
|
|
803
1064
|
method,
|
|
1065
|
+
kind,
|
|
804
1066
|
q,
|
|
805
1067
|
from,
|
|
806
1068
|
to,
|
|
@@ -808,7 +1070,7 @@ function registerTools(server, client) {
|
|
|
808
1070
|
offset,
|
|
809
1071
|
order
|
|
810
1072
|
});
|
|
811
|
-
return jsonContent(results);
|
|
1073
|
+
return jsonContent(results.map(compactRequest));
|
|
812
1074
|
})
|
|
813
1075
|
);
|
|
814
1076
|
server.tool(
|
|
@@ -817,12 +1079,13 @@ function registerTools(server, client) {
|
|
|
817
1079
|
{
|
|
818
1080
|
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
819
1081
|
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
1082
|
+
kind: kindSchema,
|
|
820
1083
|
q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
|
|
821
1084
|
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
822
1085
|
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
|
|
823
1086
|
},
|
|
824
|
-
withErrorHandling(async ({ slug, method, q, from, to }) => {
|
|
825
|
-
const count = await client.requests.count({ slug, method, q, from, to });
|
|
1087
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
|
|
1088
|
+
const count = await client.requests.count({ slug, method, kind, q, from, to });
|
|
826
1089
|
return jsonContent({ count });
|
|
827
1090
|
})
|
|
828
1091
|
);
|
|
@@ -832,7 +1095,7 @@ function registerTools(server, client) {
|
|
|
832
1095
|
{ requestId: import_zod2.z.string().describe("The request ID") },
|
|
833
1096
|
withErrorHandling(async ({ requestId }) => {
|
|
834
1097
|
const request = await client.requests.get(requestId);
|
|
835
|
-
return jsonContent(request);
|
|
1098
|
+
return jsonContent(compactRequest(request));
|
|
836
1099
|
})
|
|
837
1100
|
);
|
|
838
1101
|
server.tool(
|
|
@@ -845,7 +1108,7 @@ function registerTools(server, client) {
|
|
|
845
1108
|
},
|
|
846
1109
|
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
847
1110
|
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
848
|
-
return jsonContent(request);
|
|
1111
|
+
return jsonContent(compactRequest(request));
|
|
849
1112
|
})
|
|
850
1113
|
);
|
|
851
1114
|
server.tool(
|
|
@@ -865,7 +1128,9 @@ function registerTools(server, client) {
|
|
|
865
1128
|
pollInterval,
|
|
866
1129
|
method
|
|
867
1130
|
});
|
|
868
|
-
return jsonContent(
|
|
1131
|
+
return jsonContent(
|
|
1132
|
+
fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
|
|
1133
|
+
);
|
|
869
1134
|
})
|
|
870
1135
|
);
|
|
871
1136
|
server.tool(
|
|
@@ -1174,6 +1439,154 @@ function registerTools(server, client) {
|
|
|
1174
1439
|
}
|
|
1175
1440
|
)
|
|
1176
1441
|
);
|
|
1442
|
+
server.tool(
|
|
1443
|
+
"list_emails",
|
|
1444
|
+
"List the emails an endpoint received at its mailhooks.cc address, newest first: subject, sender, tag, and the one-time code and main link found in each.",
|
|
1445
|
+
{
|
|
1446
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1447
|
+
limit: import_zod2.z.number().int().min(1).max(100).default(25).describe(
|
|
1448
|
+
`Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
|
|
1449
|
+
),
|
|
1450
|
+
since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms"),
|
|
1451
|
+
tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
|
|
1452
|
+
subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
|
|
1453
|
+
from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)")
|
|
1454
|
+
},
|
|
1455
|
+
withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
|
|
1456
|
+
const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
|
|
1457
|
+
const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
|
|
1458
|
+
const [emails, view] = await Promise.all([
|
|
1459
|
+
client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
|
|
1460
|
+
emailEndpointView(client, endpointSlug)
|
|
1461
|
+
]);
|
|
1462
|
+
return jsonContent(
|
|
1463
|
+
emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
|
|
1464
|
+
);
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1467
|
+
server.tool(
|
|
1468
|
+
"get_email",
|
|
1469
|
+
"Get one captured email: by requestId, or the newest at an endpoint (optionally with a tag). Returns the parsed message with its one-time code and main link.",
|
|
1470
|
+
{
|
|
1471
|
+
requestId: import_zod2.z.string().optional().describe("The email's request ID"),
|
|
1472
|
+
endpointSlug: import_zod2.z.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
|
|
1473
|
+
tag: import_zod2.z.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
|
|
1474
|
+
includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
|
|
1475
|
+
},
|
|
1476
|
+
withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
|
|
1477
|
+
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1478
|
+
throw new Error("Pass exactly one of requestId or endpointSlug");
|
|
1479
|
+
}
|
|
1480
|
+
const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
|
|
1481
|
+
if (!email) {
|
|
1482
|
+
throw new import_sdk.NotFoundError(
|
|
1483
|
+
`No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
const view = await emailEndpointView(
|
|
1487
|
+
client,
|
|
1488
|
+
endpointSlug ?? emailSlugFromAddress(email.path)
|
|
1489
|
+
);
|
|
1490
|
+
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1491
|
+
})
|
|
1492
|
+
);
|
|
1493
|
+
server.tool(
|
|
1494
|
+
"wait_for_email",
|
|
1495
|
+
"Wait for an email to arrive at an endpoint (for example after triggering a signup), then return it with its one-time code and main link. Use a tag per run to find your own email.",
|
|
1496
|
+
{
|
|
1497
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1498
|
+
tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
|
|
1499
|
+
subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
|
|
1500
|
+
from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)"),
|
|
1501
|
+
timeout: durationOrTimestampSchema.default("30s").describe(
|
|
1502
|
+
`How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
|
|
1503
|
+
),
|
|
1504
|
+
since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
|
|
1505
|
+
includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
|
|
1506
|
+
},
|
|
1507
|
+
withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
|
|
1508
|
+
const [email, view] = await Promise.all([
|
|
1509
|
+
client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
|
|
1510
|
+
emailEndpointView(client, endpointSlug)
|
|
1511
|
+
]);
|
|
1512
|
+
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1513
|
+
})
|
|
1514
|
+
);
|
|
1515
|
+
server.tool(
|
|
1516
|
+
"send_test_email",
|
|
1517
|
+
"Deliver a sample email (with a 6-digit code and a link) to an endpoint, optionally to a +tag address. It counts as one request and skips SMTP, so sender checks do not run on it.",
|
|
1518
|
+
{
|
|
1519
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1520
|
+
tag: import_zod2.z.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
|
|
1521
|
+
},
|
|
1522
|
+
withErrorHandling(async ({ slug, tag }) => {
|
|
1523
|
+
const result = await client.emails.sendTest(slug, { tag });
|
|
1524
|
+
return jsonContent({ ...result, address: client.emails.address(slug, tag) });
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
server.tool(
|
|
1528
|
+
"configure_forwarding",
|
|
1529
|
+
"Set the URL captured emails are forwarded to (as signed JSON) and turn forwarding on or off. Owner only. Saving the first URL creates the signing secret.",
|
|
1530
|
+
{
|
|
1531
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1532
|
+
url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
|
|
1533
|
+
enabled: import_zod2.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
|
|
1534
|
+
},
|
|
1535
|
+
withErrorHandling(async ({ slug, url, enabled }) => {
|
|
1536
|
+
const endpoint = await client.forwarding.configure(slug, { url, enabled });
|
|
1537
|
+
return jsonContent({
|
|
1538
|
+
slug: endpoint.slug,
|
|
1539
|
+
forwardEnabled: endpoint.forwardEnabled ?? false,
|
|
1540
|
+
forwardUrl: endpoint.forwardUrl ?? null,
|
|
1541
|
+
hasForwardSecret: endpoint.hasForwardSecret ?? false
|
|
1542
|
+
});
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
server.tool(
|
|
1546
|
+
"get_forwarding_secret",
|
|
1547
|
+
"Read (or with rotate: true, replace) the whsec_ secret forwarded emails are signed with. Owner only. Treat the value as a credential: put it in the handler's environment, not in code.",
|
|
1548
|
+
{
|
|
1549
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1550
|
+
rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
|
|
1551
|
+
},
|
|
1552
|
+
withErrorHandling(async ({ slug, rotate }) => {
|
|
1553
|
+
const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1554
|
+
return jsonContent({ secret, rotated: rotate });
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
server.tool(
|
|
1558
|
+
"test_forwarding",
|
|
1559
|
+
"Post the endpoint's newest email (or a sample) to its forwarding URL once and report what the server answered. Works with forwarding on or off; nothing is retried or logged.",
|
|
1560
|
+
{ slug: import_zod2.z.string().describe("The endpoint slug") },
|
|
1561
|
+
withErrorHandling(async ({ slug }) => {
|
|
1562
|
+
return jsonContent(await client.forwarding.test(slug));
|
|
1563
|
+
})
|
|
1564
|
+
);
|
|
1565
|
+
server.tool(
|
|
1566
|
+
"list_deliveries",
|
|
1567
|
+
"List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
|
|
1568
|
+
{
|
|
1569
|
+
endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's latest deliveries"),
|
|
1570
|
+
requestId: import_zod2.z.string().optional().describe("List every delivery and try of this email"),
|
|
1571
|
+
limit: import_zod2.z.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
|
|
1572
|
+
},
|
|
1573
|
+
withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
|
|
1574
|
+
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1575
|
+
throw new Error("Pass exactly one of endpointSlug or requestId");
|
|
1576
|
+
}
|
|
1577
|
+
return jsonContent(
|
|
1578
|
+
requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
|
|
1579
|
+
);
|
|
1580
|
+
})
|
|
1581
|
+
);
|
|
1582
|
+
server.tool(
|
|
1583
|
+
"redeliver_email",
|
|
1584
|
+
"Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
|
|
1585
|
+
{ requestId: import_zod2.z.string().describe("The email's request ID") },
|
|
1586
|
+
withErrorHandling(async ({ requestId }) => {
|
|
1587
|
+
return jsonContent(await client.forwarding.redeliver(requestId));
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1177
1590
|
server.tool(
|
|
1178
1591
|
"describe",
|
|
1179
1592
|
"Describe all available SDK operations, parameters, and types.",
|
|
@@ -1288,7 +1701,7 @@ function registerAgentRegistrationTools(server) {
|
|
|
1288
1701
|
}
|
|
1289
1702
|
|
|
1290
1703
|
// src/index.ts
|
|
1291
|
-
var VERSION = true ? "1.
|
|
1704
|
+
var VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
1292
1705
|
function createServer(options = {}) {
|
|
1293
1706
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
1294
1707
|
const server = new import_mcp2.McpServer({
|