@webhooks-cc/mcp 1.8.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 +24 -1
- package/dist/bin/mcp.js +433 -20
- package/dist/index.js +433 -20
- package/dist/index.mjs +436 -21
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -142,6 +142,149 @@ function registerPrompts(server) {
|
|
|
142
142
|
|
|
143
143
|
// src/resources.ts
|
|
144
144
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
145
|
+
|
|
146
|
+
// src/compact.ts
|
|
147
|
+
var MAX_OUTPUT = 32768;
|
|
148
|
+
var MAX_EMAIL_TEXT = 8e3;
|
|
149
|
+
var MAX_EXTRACTS = 10;
|
|
150
|
+
var MAX_LINK_TEXT = 200;
|
|
151
|
+
var MAX_LINK_URL = 2e3;
|
|
152
|
+
var MAX_LIST_ITEMS = 5;
|
|
153
|
+
var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
|
|
154
|
+
var TEXT_FLOOR = 2e3;
|
|
155
|
+
function jsonSize(value) {
|
|
156
|
+
return JSON.stringify(value, null, 2).length;
|
|
157
|
+
}
|
|
158
|
+
function sliceText(text, end) {
|
|
159
|
+
if (end <= 0) return "";
|
|
160
|
+
if (end >= text.length) return text;
|
|
161
|
+
const last = text.charCodeAt(end - 1);
|
|
162
|
+
return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
|
|
163
|
+
}
|
|
164
|
+
function cutEmailText(text) {
|
|
165
|
+
if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
|
|
166
|
+
return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
|
|
167
|
+
}
|
|
168
|
+
function shrinkToFit(root, budget, steps) {
|
|
169
|
+
for (const step of steps) {
|
|
170
|
+
if (jsonSize(root) <= budget) return;
|
|
171
|
+
step();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function omitHeaders(holder) {
|
|
175
|
+
const headers = holder.headers;
|
|
176
|
+
if (headers === null || typeof headers !== "object") return;
|
|
177
|
+
const count = Object.keys(headers).length;
|
|
178
|
+
if (count === 0) return;
|
|
179
|
+
holder.headers = {};
|
|
180
|
+
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
181
|
+
}
|
|
182
|
+
function capExtracts(holder) {
|
|
183
|
+
if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
|
|
184
|
+
holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
|
|
185
|
+
holder.codesTruncated = true;
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(holder.links)) {
|
|
188
|
+
if (holder.links.length > MAX_EXTRACTS) {
|
|
189
|
+
holder.links = holder.links.slice(0, MAX_EXTRACTS);
|
|
190
|
+
holder.linksTruncated = true;
|
|
191
|
+
}
|
|
192
|
+
holder.links = holder.links.map((link) => ({
|
|
193
|
+
...link,
|
|
194
|
+
...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
|
|
195
|
+
...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function capLists(holder) {
|
|
200
|
+
for (const key of EMAIL_LISTS) {
|
|
201
|
+
const list = holder[key];
|
|
202
|
+
if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
|
|
203
|
+
holder[key] = list.slice(0, MAX_LIST_ITEMS);
|
|
204
|
+
holder[`${key}Total`] = list.length;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function keepOnly(holder, keep) {
|
|
209
|
+
for (const key of Object.keys(holder)) {
|
|
210
|
+
if (!keep.includes(key)) delete holder[key];
|
|
211
|
+
}
|
|
212
|
+
holder.trimmed = "The email was too big for the output, so only these fields are shown.";
|
|
213
|
+
}
|
|
214
|
+
function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
|
|
215
|
+
const value = holder[key];
|
|
216
|
+
if (typeof value !== "string" || value.length <= floor) return;
|
|
217
|
+
const flagged = holder[flag] === true;
|
|
218
|
+
holder[key] = "";
|
|
219
|
+
holder[flag] = true;
|
|
220
|
+
const room = budget - jsonSize(root) + 2;
|
|
221
|
+
let low = 0;
|
|
222
|
+
let high = value.length;
|
|
223
|
+
while (low < high) {
|
|
224
|
+
const mid = Math.ceil((low + high) / 2);
|
|
225
|
+
if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
|
|
226
|
+
else high = mid - 1;
|
|
227
|
+
}
|
|
228
|
+
const kept = sliceText(value, Math.max(low, floor));
|
|
229
|
+
holder[key] = kept;
|
|
230
|
+
if (kept.length === value.length && !flagged) delete holder[flag];
|
|
231
|
+
}
|
|
232
|
+
function keepFirst(holder, key) {
|
|
233
|
+
const list = holder[key];
|
|
234
|
+
if (!Array.isArray(list) || list.length <= 1) return;
|
|
235
|
+
holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
|
|
236
|
+
holder[key] = list.slice(0, 1);
|
|
237
|
+
}
|
|
238
|
+
var ESSENTIAL_EMAIL_FIELDS = [
|
|
239
|
+
"subject",
|
|
240
|
+
"from",
|
|
241
|
+
"fromTotal",
|
|
242
|
+
"tag",
|
|
243
|
+
"date",
|
|
244
|
+
"text",
|
|
245
|
+
"textTruncated",
|
|
246
|
+
"htmlSize"
|
|
247
|
+
];
|
|
248
|
+
function compactRequest(request) {
|
|
249
|
+
if (request.kind !== "email") return request;
|
|
250
|
+
const compact = { ...request };
|
|
251
|
+
delete compact.body;
|
|
252
|
+
delete compact.bodyRaw;
|
|
253
|
+
compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
|
|
254
|
+
if (!request.email) return compact;
|
|
255
|
+
const { html, ...rest } = request.email;
|
|
256
|
+
const { text, cut } = cutEmailText(rest.text);
|
|
257
|
+
const restoreText = () => {
|
|
258
|
+
email.text = text;
|
|
259
|
+
if (cut) email.textTruncated = true;
|
|
260
|
+
else delete email.textTruncated;
|
|
261
|
+
};
|
|
262
|
+
const email = {
|
|
263
|
+
...rest,
|
|
264
|
+
text,
|
|
265
|
+
...cut ? { textTruncated: true } : {},
|
|
266
|
+
htmlSize: html?.length ?? 0
|
|
267
|
+
};
|
|
268
|
+
compact.email = email;
|
|
269
|
+
const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
|
|
270
|
+
const budget = MAX_OUTPUT - 128;
|
|
271
|
+
shrinkToFit(root, budget, [
|
|
272
|
+
() => omitHeaders(compact),
|
|
273
|
+
() => capLists(email),
|
|
274
|
+
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
275
|
+
// Down to the essentials there may be room for more of the text again.
|
|
276
|
+
() => {
|
|
277
|
+
keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
|
|
278
|
+
keepFirst(email, "from");
|
|
279
|
+
restoreText();
|
|
280
|
+
cutStringToFit(root, email, "text", "textTruncated", budget);
|
|
281
|
+
},
|
|
282
|
+
() => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
|
|
283
|
+
]);
|
|
284
|
+
return compact;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/resources.ts
|
|
145
288
|
var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
|
|
146
289
|
var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
|
|
147
290
|
var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
|
|
@@ -239,7 +382,7 @@ function registerResources(server, client) {
|
|
|
239
382
|
]);
|
|
240
383
|
return jsonResource(uri.toString(), {
|
|
241
384
|
endpoint,
|
|
242
|
-
requests
|
|
385
|
+
requests: requests.map(compactRequest)
|
|
243
386
|
});
|
|
244
387
|
}
|
|
245
388
|
);
|
|
@@ -257,7 +400,7 @@ function registerResources(server, client) {
|
|
|
257
400
|
async (uri, variables) => {
|
|
258
401
|
const id = variableToString(variables.id, "id");
|
|
259
402
|
const request = await client.requests.get(id);
|
|
260
|
-
return jsonResource(uri.toString(), request);
|
|
403
|
+
return jsonResource(uri.toString(), compactRequest(request));
|
|
261
404
|
}
|
|
262
405
|
);
|
|
263
406
|
}
|
|
@@ -265,7 +408,7 @@ function registerResources(server, client) {
|
|
|
265
408
|
// src/tools.ts
|
|
266
409
|
var import_zod2 = require("zod");
|
|
267
410
|
var import_sdk = require("@webhooks-cc/sdk");
|
|
268
|
-
var MAX_BODY_SIZE =
|
|
411
|
+
var MAX_BODY_SIZE = MAX_OUTPUT;
|
|
269
412
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
270
413
|
var TIME_SEPARATOR = " \u2014 ";
|
|
271
414
|
var httpUrlSchema = import_zod2.z.string().url().refine(
|
|
@@ -280,6 +423,8 @@ var httpUrlSchema = import_zod2.z.string().url().refine(
|
|
|
280
423
|
{ message: "Only http and https URLs are supported" }
|
|
281
424
|
);
|
|
282
425
|
var methodSchema = import_zod2.z.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
|
|
426
|
+
var kindSchema = import_zod2.z.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
|
|
427
|
+
var EMAIL_SCAN_LIMIT = 100;
|
|
283
428
|
var durationOrTimestampSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]);
|
|
284
429
|
var ruleConditionSchema = import_zod2.z.object({
|
|
285
430
|
field: import_zod2.z.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
|
|
@@ -359,6 +504,24 @@ function serializeJson(value, limit = MAX_BODY_SIZE) {
|
|
|
359
504
|
return full.slice(0, limit) + `
|
|
360
505
|
... [truncated, ${full.length} chars total]`;
|
|
361
506
|
}
|
|
507
|
+
function fitArrayField(value, key) {
|
|
508
|
+
const items = value[key];
|
|
509
|
+
if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
|
|
510
|
+
const cut = (count) => ({
|
|
511
|
+
...value,
|
|
512
|
+
[key]: items.slice(0, count),
|
|
513
|
+
truncated: true,
|
|
514
|
+
returned: count
|
|
515
|
+
});
|
|
516
|
+
let low = 0;
|
|
517
|
+
let high = items.length;
|
|
518
|
+
while (low < high) {
|
|
519
|
+
const mid = Math.ceil((low + high) / 2);
|
|
520
|
+
if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
|
|
521
|
+
else high = mid - 1;
|
|
522
|
+
}
|
|
523
|
+
return cut(low);
|
|
524
|
+
}
|
|
362
525
|
function jsonContent(value) {
|
|
363
526
|
return textContent(serializeJson(value));
|
|
364
527
|
}
|
|
@@ -507,6 +670,99 @@ async function summarizeResponse(response) {
|
|
|
507
670
|
body: await readBodyTruncated(response)
|
|
508
671
|
};
|
|
509
672
|
}
|
|
673
|
+
async function emailEndpointView(client, slug) {
|
|
674
|
+
const endpoint = await client.endpoints.get(slug);
|
|
675
|
+
return {
|
|
676
|
+
endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
|
|
677
|
+
includeExtracts: endpoint.showEmailExtracts !== false
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function emailSlugFromAddress(address) {
|
|
681
|
+
const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
|
|
682
|
+
return local.split("+")[0].toLowerCase();
|
|
683
|
+
}
|
|
684
|
+
var MAX_SUMMARY_LINK = 8e3;
|
|
685
|
+
function boundedLink(link, max) {
|
|
686
|
+
if (link == null || link.length <= max) return { link: link ?? null };
|
|
687
|
+
return {
|
|
688
|
+
link: null,
|
|
689
|
+
linkOmitted: `The main link is ${link.length} characters long, too long for the output`
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
var MAX_SUMMARY_FIELD = 1e3;
|
|
693
|
+
function summarizeEmail(email, includeExtracts) {
|
|
694
|
+
const from = email.email.from[0];
|
|
695
|
+
const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
|
|
696
|
+
const subject = email.email.subject;
|
|
697
|
+
return {
|
|
698
|
+
id: email.id,
|
|
699
|
+
receivedAt: new Date(email.receivedAt).toISOString(),
|
|
700
|
+
address: email.path,
|
|
701
|
+
tag: email.email.tag,
|
|
702
|
+
subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
|
|
703
|
+
from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
|
|
704
|
+
...includeExtracts ? { code: (0, import_sdk.extractCode)(email), ...boundedLink((0, import_sdk.extractLink)(email), MAX_SUMMARY_LINK) } : {},
|
|
705
|
+
attachments: email.email.attachments.length
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
var ESSENTIAL_DETAIL_FIELDS = [
|
|
709
|
+
"code",
|
|
710
|
+
"link",
|
|
711
|
+
"id",
|
|
712
|
+
"endpoint",
|
|
713
|
+
"receivedAt",
|
|
714
|
+
"address",
|
|
715
|
+
"tag",
|
|
716
|
+
"subject",
|
|
717
|
+
"from",
|
|
718
|
+
"size",
|
|
719
|
+
"test",
|
|
720
|
+
"text",
|
|
721
|
+
"textTruncated",
|
|
722
|
+
"htmlSize",
|
|
723
|
+
"html",
|
|
724
|
+
"htmlTruncated"
|
|
725
|
+
];
|
|
726
|
+
function emailDetail(client, email, view, includeHtml) {
|
|
727
|
+
const { data } = client.emails.toJson(email, {
|
|
728
|
+
endpoint: view.endpoint,
|
|
729
|
+
includeExtracts: view.includeExtracts
|
|
730
|
+
});
|
|
731
|
+
const { html, text: fullText, ...rest } = data;
|
|
732
|
+
const { text, cut } = cutEmailText(fullText ?? null);
|
|
733
|
+
const restoreText = () => {
|
|
734
|
+
detail.text = text;
|
|
735
|
+
if (cut) detail.textTruncated = true;
|
|
736
|
+
else delete detail.textTruncated;
|
|
737
|
+
};
|
|
738
|
+
const detail = {
|
|
739
|
+
...view.includeExtracts ? { code: (0, import_sdk.extractCode)(email), link: (0, import_sdk.extractLink)(email) } : {},
|
|
740
|
+
...rest,
|
|
741
|
+
text,
|
|
742
|
+
...cut ? { textTruncated: true } : {},
|
|
743
|
+
htmlSize: html?.length ?? 0,
|
|
744
|
+
...includeHtml && html !== null ? { html } : {}
|
|
745
|
+
};
|
|
746
|
+
shrinkToFit(detail, MAX_BODY_SIZE, [
|
|
747
|
+
() => omitHeaders(detail),
|
|
748
|
+
() => capExtracts(detail),
|
|
749
|
+
() => capLists(detail),
|
|
750
|
+
() => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
|
|
751
|
+
() => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
|
|
752
|
+
// Down to the essentials (and without a main link too long to use) there
|
|
753
|
+
// may be room for more of the text again.
|
|
754
|
+
() => {
|
|
755
|
+
keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
|
|
756
|
+
if (view.includeExtracts) {
|
|
757
|
+
Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
|
|
758
|
+
}
|
|
759
|
+
restoreText();
|
|
760
|
+
cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
|
|
761
|
+
},
|
|
762
|
+
() => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
|
|
763
|
+
]);
|
|
764
|
+
return detail;
|
|
765
|
+
}
|
|
510
766
|
function registerTools(server, client) {
|
|
511
767
|
server.tool(
|
|
512
768
|
"create_endpoint",
|
|
@@ -650,7 +906,7 @@ function registerTools(server, client) {
|
|
|
650
906
|
);
|
|
651
907
|
server.tool(
|
|
652
908
|
"update_endpoint",
|
|
653
|
-
"Update an endpoint name, mock response, conditional response rules, or
|
|
909
|
+
"Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
|
|
654
910
|
{
|
|
655
911
|
slug: import_zod2.z.string().describe("The endpoint slug to update"),
|
|
656
912
|
name: import_zod2.z.string().optional().describe("New display name"),
|
|
@@ -663,7 +919,8 @@ function registerTools(server, client) {
|
|
|
663
919
|
signingSecret: import_zod2.z.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
|
|
664
920
|
signingHeader: import_zod2.z.string().nullable().optional().describe(
|
|
665
921
|
"Custom signature header name (only for generic-hmac provider), or null to clear"
|
|
666
|
-
)
|
|
922
|
+
),
|
|
923
|
+
showEmailExtracts: import_zod2.z.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
|
|
667
924
|
},
|
|
668
925
|
withErrorHandling(
|
|
669
926
|
async ({
|
|
@@ -674,7 +931,8 @@ function registerTools(server, client) {
|
|
|
674
931
|
notificationUrl,
|
|
675
932
|
signingProvider,
|
|
676
933
|
signingSecret,
|
|
677
|
-
signingHeader
|
|
934
|
+
signingHeader,
|
|
935
|
+
showEmailExtracts
|
|
678
936
|
}) => {
|
|
679
937
|
const endpoint = await client.endpoints.update(slug, {
|
|
680
938
|
name,
|
|
@@ -683,7 +941,8 @@ function registerTools(server, client) {
|
|
|
683
941
|
notificationUrl,
|
|
684
942
|
signingProvider,
|
|
685
943
|
signingSecret,
|
|
686
|
-
signingHeader
|
|
944
|
+
signingHeader,
|
|
945
|
+
showEmailExtracts
|
|
687
946
|
});
|
|
688
947
|
return jsonContent(endpoint);
|
|
689
948
|
}
|
|
@@ -795,15 +1054,16 @@ function registerTools(server, client) {
|
|
|
795
1054
|
);
|
|
796
1055
|
server.tool(
|
|
797
1056
|
"list_requests",
|
|
798
|
-
"List recent captured requests for an endpoint.",
|
|
1057
|
+
"List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
|
|
799
1058
|
{
|
|
800
1059
|
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
801
1060
|
limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
|
|
802
|
-
since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms")
|
|
1061
|
+
since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms"),
|
|
1062
|
+
kind: kindSchema
|
|
803
1063
|
},
|
|
804
|
-
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
805
|
-
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
806
|
-
return jsonContent(requests);
|
|
1064
|
+
withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
|
|
1065
|
+
const requests = await client.requests.list(endpointSlug, { limit, since, kind });
|
|
1066
|
+
return jsonContent(requests.map(compactRequest));
|
|
807
1067
|
})
|
|
808
1068
|
);
|
|
809
1069
|
server.tool(
|
|
@@ -812,6 +1072,7 @@ function registerTools(server, client) {
|
|
|
812
1072
|
{
|
|
813
1073
|
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
814
1074
|
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
1075
|
+
kind: kindSchema,
|
|
815
1076
|
q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
|
|
816
1077
|
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
817
1078
|
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
|
|
@@ -819,10 +1080,11 @@ function registerTools(server, client) {
|
|
|
819
1080
|
offset: import_zod2.z.number().int().min(0).max(1e4).default(0).describe("Result offset"),
|
|
820
1081
|
order: import_zod2.z.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
|
|
821
1082
|
},
|
|
822
|
-
withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
|
|
1083
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
|
|
823
1084
|
const results = await client.requests.search({
|
|
824
1085
|
slug,
|
|
825
1086
|
method,
|
|
1087
|
+
kind,
|
|
826
1088
|
q,
|
|
827
1089
|
from,
|
|
828
1090
|
to,
|
|
@@ -830,7 +1092,7 @@ function registerTools(server, client) {
|
|
|
830
1092
|
offset,
|
|
831
1093
|
order
|
|
832
1094
|
});
|
|
833
|
-
return jsonContent(results);
|
|
1095
|
+
return jsonContent(results.map(compactRequest));
|
|
834
1096
|
})
|
|
835
1097
|
);
|
|
836
1098
|
server.tool(
|
|
@@ -839,12 +1101,13 @@ function registerTools(server, client) {
|
|
|
839
1101
|
{
|
|
840
1102
|
slug: import_zod2.z.string().optional().describe("Filter to a specific endpoint slug"),
|
|
841
1103
|
method: import_zod2.z.string().optional().describe("Filter by HTTP method"),
|
|
1104
|
+
kind: kindSchema,
|
|
842
1105
|
q: import_zod2.z.string().optional().describe("Free-text search across path, body, and headers"),
|
|
843
1106
|
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
844
1107
|
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
|
|
845
1108
|
},
|
|
846
|
-
withErrorHandling(async ({ slug, method, q, from, to }) => {
|
|
847
|
-
const count = await client.requests.count({ slug, method, q, from, to });
|
|
1109
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
|
|
1110
|
+
const count = await client.requests.count({ slug, method, kind, q, from, to });
|
|
848
1111
|
return jsonContent({ count });
|
|
849
1112
|
})
|
|
850
1113
|
);
|
|
@@ -854,7 +1117,7 @@ function registerTools(server, client) {
|
|
|
854
1117
|
{ requestId: import_zod2.z.string().describe("The request ID") },
|
|
855
1118
|
withErrorHandling(async ({ requestId }) => {
|
|
856
1119
|
const request = await client.requests.get(requestId);
|
|
857
|
-
return jsonContent(request);
|
|
1120
|
+
return jsonContent(compactRequest(request));
|
|
858
1121
|
})
|
|
859
1122
|
);
|
|
860
1123
|
server.tool(
|
|
@@ -867,7 +1130,7 @@ function registerTools(server, client) {
|
|
|
867
1130
|
},
|
|
868
1131
|
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
869
1132
|
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
870
|
-
return jsonContent(request);
|
|
1133
|
+
return jsonContent(compactRequest(request));
|
|
871
1134
|
})
|
|
872
1135
|
);
|
|
873
1136
|
server.tool(
|
|
@@ -887,7 +1150,9 @@ function registerTools(server, client) {
|
|
|
887
1150
|
pollInterval,
|
|
888
1151
|
method
|
|
889
1152
|
});
|
|
890
|
-
return jsonContent(
|
|
1153
|
+
return jsonContent(
|
|
1154
|
+
fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
|
|
1155
|
+
);
|
|
891
1156
|
})
|
|
892
1157
|
);
|
|
893
1158
|
server.tool(
|
|
@@ -1196,6 +1461,154 @@ function registerTools(server, client) {
|
|
|
1196
1461
|
}
|
|
1197
1462
|
)
|
|
1198
1463
|
);
|
|
1464
|
+
server.tool(
|
|
1465
|
+
"list_emails",
|
|
1466
|
+
"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.",
|
|
1467
|
+
{
|
|
1468
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1469
|
+
limit: import_zod2.z.number().int().min(1).max(100).default(25).describe(
|
|
1470
|
+
`Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
|
|
1471
|
+
),
|
|
1472
|
+
since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms"),
|
|
1473
|
+
tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
|
|
1474
|
+
subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
|
|
1475
|
+
from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)")
|
|
1476
|
+
},
|
|
1477
|
+
withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
|
|
1478
|
+
const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
|
|
1479
|
+
const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
|
|
1480
|
+
const [emails, view] = await Promise.all([
|
|
1481
|
+
client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
|
|
1482
|
+
emailEndpointView(client, endpointSlug)
|
|
1483
|
+
]);
|
|
1484
|
+
return jsonContent(
|
|
1485
|
+
emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
|
|
1486
|
+
);
|
|
1487
|
+
})
|
|
1488
|
+
);
|
|
1489
|
+
server.tool(
|
|
1490
|
+
"get_email",
|
|
1491
|
+
"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.",
|
|
1492
|
+
{
|
|
1493
|
+
requestId: import_zod2.z.string().optional().describe("The email's request ID"),
|
|
1494
|
+
endpointSlug: import_zod2.z.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
|
|
1495
|
+
tag: import_zod2.z.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
|
|
1496
|
+
includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
|
|
1497
|
+
},
|
|
1498
|
+
withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
|
|
1499
|
+
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1500
|
+
throw new Error("Pass exactly one of requestId or endpointSlug");
|
|
1501
|
+
}
|
|
1502
|
+
const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
|
|
1503
|
+
if (!email) {
|
|
1504
|
+
throw new import_sdk.NotFoundError(
|
|
1505
|
+
`No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
const view = await emailEndpointView(
|
|
1509
|
+
client,
|
|
1510
|
+
endpointSlug ?? emailSlugFromAddress(email.path)
|
|
1511
|
+
);
|
|
1512
|
+
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1513
|
+
})
|
|
1514
|
+
);
|
|
1515
|
+
server.tool(
|
|
1516
|
+
"wait_for_email",
|
|
1517
|
+
"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.",
|
|
1518
|
+
{
|
|
1519
|
+
endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1520
|
+
tag: import_zod2.z.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
|
|
1521
|
+
subject: import_zod2.z.string().optional().describe("Only emails whose subject contains this text"),
|
|
1522
|
+
from: import_zod2.z.string().optional().describe("Only emails from this address (case-insensitive)"),
|
|
1523
|
+
timeout: durationOrTimestampSchema.default("30s").describe(
|
|
1524
|
+
`How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
|
|
1525
|
+
),
|
|
1526
|
+
since: import_zod2.z.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
|
|
1527
|
+
includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
|
|
1528
|
+
},
|
|
1529
|
+
withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
|
|
1530
|
+
const [email, view] = await Promise.all([
|
|
1531
|
+
client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
|
|
1532
|
+
emailEndpointView(client, endpointSlug)
|
|
1533
|
+
]);
|
|
1534
|
+
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1535
|
+
})
|
|
1536
|
+
);
|
|
1537
|
+
server.tool(
|
|
1538
|
+
"send_test_email",
|
|
1539
|
+
"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.",
|
|
1540
|
+
{
|
|
1541
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1542
|
+
tag: import_zod2.z.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
|
|
1543
|
+
},
|
|
1544
|
+
withErrorHandling(async ({ slug, tag }) => {
|
|
1545
|
+
const result = await client.emails.sendTest(slug, { tag });
|
|
1546
|
+
return jsonContent({ ...result, address: client.emails.address(slug, tag) });
|
|
1547
|
+
})
|
|
1548
|
+
);
|
|
1549
|
+
server.tool(
|
|
1550
|
+
"configure_forwarding",
|
|
1551
|
+
"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.",
|
|
1552
|
+
{
|
|
1553
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1554
|
+
url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
|
|
1555
|
+
enabled: import_zod2.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
|
|
1556
|
+
},
|
|
1557
|
+
withErrorHandling(async ({ slug, url, enabled }) => {
|
|
1558
|
+
const endpoint = await client.forwarding.configure(slug, { url, enabled });
|
|
1559
|
+
return jsonContent({
|
|
1560
|
+
slug: endpoint.slug,
|
|
1561
|
+
forwardEnabled: endpoint.forwardEnabled ?? false,
|
|
1562
|
+
forwardUrl: endpoint.forwardUrl ?? null,
|
|
1563
|
+
hasForwardSecret: endpoint.hasForwardSecret ?? false
|
|
1564
|
+
});
|
|
1565
|
+
})
|
|
1566
|
+
);
|
|
1567
|
+
server.tool(
|
|
1568
|
+
"get_forwarding_secret",
|
|
1569
|
+
"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.",
|
|
1570
|
+
{
|
|
1571
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1572
|
+
rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
|
|
1573
|
+
},
|
|
1574
|
+
withErrorHandling(async ({ slug, rotate }) => {
|
|
1575
|
+
const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1576
|
+
return jsonContent({ secret, rotated: rotate });
|
|
1577
|
+
})
|
|
1578
|
+
);
|
|
1579
|
+
server.tool(
|
|
1580
|
+
"test_forwarding",
|
|
1581
|
+
"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.",
|
|
1582
|
+
{ slug: import_zod2.z.string().describe("The endpoint slug") },
|
|
1583
|
+
withErrorHandling(async ({ slug }) => {
|
|
1584
|
+
return jsonContent(await client.forwarding.test(slug));
|
|
1585
|
+
})
|
|
1586
|
+
);
|
|
1587
|
+
server.tool(
|
|
1588
|
+
"list_deliveries",
|
|
1589
|
+
"List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
|
|
1590
|
+
{
|
|
1591
|
+
endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's latest deliveries"),
|
|
1592
|
+
requestId: import_zod2.z.string().optional().describe("List every delivery and try of this email"),
|
|
1593
|
+
limit: import_zod2.z.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
|
|
1594
|
+
},
|
|
1595
|
+
withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
|
|
1596
|
+
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1597
|
+
throw new Error("Pass exactly one of endpointSlug or requestId");
|
|
1598
|
+
}
|
|
1599
|
+
return jsonContent(
|
|
1600
|
+
requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
|
|
1601
|
+
);
|
|
1602
|
+
})
|
|
1603
|
+
);
|
|
1604
|
+
server.tool(
|
|
1605
|
+
"redeliver_email",
|
|
1606
|
+
"Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
|
|
1607
|
+
{ requestId: import_zod2.z.string().describe("The email's request ID") },
|
|
1608
|
+
withErrorHandling(async ({ requestId }) => {
|
|
1609
|
+
return jsonContent(await client.forwarding.redeliver(requestId));
|
|
1610
|
+
})
|
|
1611
|
+
);
|
|
1199
1612
|
server.tool(
|
|
1200
1613
|
"describe",
|
|
1201
1614
|
"Describe all available SDK operations, parameters, and types.",
|
|
@@ -1310,7 +1723,7 @@ function registerAgentRegistrationTools(server) {
|
|
|
1310
1723
|
}
|
|
1311
1724
|
|
|
1312
1725
|
// src/index.ts
|
|
1313
|
-
var VERSION = true ? "1.
|
|
1726
|
+
var VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
1314
1727
|
function createServer(options = {}) {
|
|
1315
1728
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
1316
1729
|
const server = new import_mcp2.McpServer({
|