@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.mjs
CHANGED
|
@@ -114,6 +114,149 @@ function registerPrompts(server) {
|
|
|
114
114
|
|
|
115
115
|
// src/resources.ts
|
|
116
116
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
117
|
+
|
|
118
|
+
// src/compact.ts
|
|
119
|
+
var MAX_OUTPUT = 32768;
|
|
120
|
+
var MAX_EMAIL_TEXT = 8e3;
|
|
121
|
+
var MAX_EXTRACTS = 10;
|
|
122
|
+
var MAX_LINK_TEXT = 200;
|
|
123
|
+
var MAX_LINK_URL = 2e3;
|
|
124
|
+
var MAX_LIST_ITEMS = 5;
|
|
125
|
+
var EMAIL_LISTS = ["from", "to", "cc", "replyTo", "sender", "inReplyTo", "attachments"];
|
|
126
|
+
var TEXT_FLOOR = 2e3;
|
|
127
|
+
function jsonSize(value) {
|
|
128
|
+
return JSON.stringify(value, null, 2).length;
|
|
129
|
+
}
|
|
130
|
+
function sliceText(text, end) {
|
|
131
|
+
if (end <= 0) return "";
|
|
132
|
+
if (end >= text.length) return text;
|
|
133
|
+
const last = text.charCodeAt(end - 1);
|
|
134
|
+
return text.slice(0, last >= 55296 && last <= 56319 ? end - 1 : end);
|
|
135
|
+
}
|
|
136
|
+
function cutEmailText(text) {
|
|
137
|
+
if (text === null || text.length <= MAX_EMAIL_TEXT) return { text, cut: false };
|
|
138
|
+
return { text: sliceText(text, MAX_EMAIL_TEXT), cut: true };
|
|
139
|
+
}
|
|
140
|
+
function shrinkToFit(root, budget, steps) {
|
|
141
|
+
for (const step of steps) {
|
|
142
|
+
if (jsonSize(root) <= budget) return;
|
|
143
|
+
step();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function omitHeaders(holder) {
|
|
147
|
+
const headers = holder.headers;
|
|
148
|
+
if (headers === null || typeof headers !== "object") return;
|
|
149
|
+
const count = Object.keys(headers).length;
|
|
150
|
+
if (count === 0) return;
|
|
151
|
+
holder.headers = {};
|
|
152
|
+
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
153
|
+
}
|
|
154
|
+
function capExtracts(holder) {
|
|
155
|
+
if (Array.isArray(holder.codes) && holder.codes.length > MAX_EXTRACTS) {
|
|
156
|
+
holder.codes = holder.codes.slice(0, MAX_EXTRACTS);
|
|
157
|
+
holder.codesTruncated = true;
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(holder.links)) {
|
|
160
|
+
if (holder.links.length > MAX_EXTRACTS) {
|
|
161
|
+
holder.links = holder.links.slice(0, MAX_EXTRACTS);
|
|
162
|
+
holder.linksTruncated = true;
|
|
163
|
+
}
|
|
164
|
+
holder.links = holder.links.map((link) => ({
|
|
165
|
+
...link,
|
|
166
|
+
...typeof link.url === "string" ? { url: sliceText(link.url, MAX_LINK_URL) } : {},
|
|
167
|
+
...typeof link.text === "string" ? { text: sliceText(link.text, MAX_LINK_TEXT) } : {}
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function capLists(holder) {
|
|
172
|
+
for (const key of EMAIL_LISTS) {
|
|
173
|
+
const list = holder[key];
|
|
174
|
+
if (Array.isArray(list) && list.length > MAX_LIST_ITEMS) {
|
|
175
|
+
holder[key] = list.slice(0, MAX_LIST_ITEMS);
|
|
176
|
+
holder[`${key}Total`] = list.length;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function keepOnly(holder, keep) {
|
|
181
|
+
for (const key of Object.keys(holder)) {
|
|
182
|
+
if (!keep.includes(key)) delete holder[key];
|
|
183
|
+
}
|
|
184
|
+
holder.trimmed = "The email was too big for the output, so only these fields are shown.";
|
|
185
|
+
}
|
|
186
|
+
function cutStringToFit(root, holder, key, flag, budget, floor = 0) {
|
|
187
|
+
const value = holder[key];
|
|
188
|
+
if (typeof value !== "string" || value.length <= floor) return;
|
|
189
|
+
const flagged = holder[flag] === true;
|
|
190
|
+
holder[key] = "";
|
|
191
|
+
holder[flag] = true;
|
|
192
|
+
const room = budget - jsonSize(root) + 2;
|
|
193
|
+
let low = 0;
|
|
194
|
+
let high = value.length;
|
|
195
|
+
while (low < high) {
|
|
196
|
+
const mid = Math.ceil((low + high) / 2);
|
|
197
|
+
if (JSON.stringify(sliceText(value, mid)).length <= room) low = mid;
|
|
198
|
+
else high = mid - 1;
|
|
199
|
+
}
|
|
200
|
+
const kept = sliceText(value, Math.max(low, floor));
|
|
201
|
+
holder[key] = kept;
|
|
202
|
+
if (kept.length === value.length && !flagged) delete holder[flag];
|
|
203
|
+
}
|
|
204
|
+
function keepFirst(holder, key) {
|
|
205
|
+
const list = holder[key];
|
|
206
|
+
if (!Array.isArray(list) || list.length <= 1) return;
|
|
207
|
+
holder[`${key}Total`] = holder[`${key}Total`] ?? list.length;
|
|
208
|
+
holder[key] = list.slice(0, 1);
|
|
209
|
+
}
|
|
210
|
+
var ESSENTIAL_EMAIL_FIELDS = [
|
|
211
|
+
"subject",
|
|
212
|
+
"from",
|
|
213
|
+
"fromTotal",
|
|
214
|
+
"tag",
|
|
215
|
+
"date",
|
|
216
|
+
"text",
|
|
217
|
+
"textTruncated",
|
|
218
|
+
"htmlSize"
|
|
219
|
+
];
|
|
220
|
+
function compactRequest(request) {
|
|
221
|
+
if (request.kind !== "email") return request;
|
|
222
|
+
const compact = { ...request };
|
|
223
|
+
delete compact.body;
|
|
224
|
+
delete compact.bodyRaw;
|
|
225
|
+
compact.rawMessageOmitted = "The raw message and the HTML part are left out; size is the message size in bytes. get_email returns the HTML.";
|
|
226
|
+
if (!request.email) return compact;
|
|
227
|
+
const { html, ...rest } = request.email;
|
|
228
|
+
const { text, cut } = cutEmailText(rest.text);
|
|
229
|
+
const restoreText = () => {
|
|
230
|
+
email.text = text;
|
|
231
|
+
if (cut) email.textTruncated = true;
|
|
232
|
+
else delete email.textTruncated;
|
|
233
|
+
};
|
|
234
|
+
const email = {
|
|
235
|
+
...rest,
|
|
236
|
+
text,
|
|
237
|
+
...cut ? { textTruncated: true } : {},
|
|
238
|
+
htmlSize: html?.length ?? 0
|
|
239
|
+
};
|
|
240
|
+
compact.email = email;
|
|
241
|
+
const root = { items: [compact], truncated: true, total: 1e3, returned: 1e3 };
|
|
242
|
+
const budget = MAX_OUTPUT - 128;
|
|
243
|
+
shrinkToFit(root, budget, [
|
|
244
|
+
() => omitHeaders(compact),
|
|
245
|
+
() => capLists(email),
|
|
246
|
+
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
247
|
+
// Down to the essentials there may be room for more of the text again.
|
|
248
|
+
() => {
|
|
249
|
+
keepOnly(email, ESSENTIAL_EMAIL_FIELDS);
|
|
250
|
+
keepFirst(email, "from");
|
|
251
|
+
restoreText();
|
|
252
|
+
cutStringToFit(root, email, "text", "textTruncated", budget);
|
|
253
|
+
},
|
|
254
|
+
() => cutStringToFit(root, email, "subject", "subjectTruncated", budget)
|
|
255
|
+
]);
|
|
256
|
+
return compact;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/resources.ts
|
|
117
260
|
var ENDPOINTS_RESOURCE_URI = "webhooks://endpoints";
|
|
118
261
|
var ENDPOINT_RECENT_TEMPLATE_URI = "webhooks://endpoint/{slug}/recent";
|
|
119
262
|
var REQUEST_TEMPLATE_URI = "webhooks://request/{id}";
|
|
@@ -211,7 +354,7 @@ function registerResources(server, client) {
|
|
|
211
354
|
]);
|
|
212
355
|
return jsonResource(uri.toString(), {
|
|
213
356
|
endpoint,
|
|
214
|
-
requests
|
|
357
|
+
requests: requests.map(compactRequest)
|
|
215
358
|
});
|
|
216
359
|
}
|
|
217
360
|
);
|
|
@@ -229,7 +372,7 @@ function registerResources(server, client) {
|
|
|
229
372
|
async (uri, variables) => {
|
|
230
373
|
const id = variableToString(variables.id, "id");
|
|
231
374
|
const request = await client.requests.get(id);
|
|
232
|
-
return jsonResource(uri.toString(), request);
|
|
375
|
+
return jsonResource(uri.toString(), compactRequest(request));
|
|
233
376
|
}
|
|
234
377
|
);
|
|
235
378
|
}
|
|
@@ -257,9 +400,11 @@ import {
|
|
|
257
400
|
VERIFY_PROVIDERS,
|
|
258
401
|
verifySignature,
|
|
259
402
|
WebhooksCC,
|
|
260
|
-
WebhooksCCError
|
|
403
|
+
WebhooksCCError,
|
|
404
|
+
extractCode,
|
|
405
|
+
extractLink
|
|
261
406
|
} from "@webhooks-cc/sdk";
|
|
262
|
-
var MAX_BODY_SIZE =
|
|
407
|
+
var MAX_BODY_SIZE = MAX_OUTPUT;
|
|
263
408
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
264
409
|
var TIME_SEPARATOR = " \u2014 ";
|
|
265
410
|
var httpUrlSchema = z2.string().url().refine(
|
|
@@ -274,6 +419,8 @@ var httpUrlSchema = z2.string().url().refine(
|
|
|
274
419
|
{ message: "Only http and https URLs are supported" }
|
|
275
420
|
);
|
|
276
421
|
var methodSchema = z2.enum(HTTP_METHODS).default("POST").describe("HTTP method (default: POST)");
|
|
422
|
+
var kindSchema = z2.enum(["http", "email"]).optional().describe('Only HTTP requests ("http") or only captured emails ("email")');
|
|
423
|
+
var EMAIL_SCAN_LIMIT = 100;
|
|
277
424
|
var durationOrTimestampSchema = z2.union([z2.string(), z2.number()]);
|
|
278
425
|
var ruleConditionSchema = z2.object({
|
|
279
426
|
field: z2.enum(["method", "path", "header", "body_contains", "body_path", "query"]).describe("Request field to match against"),
|
|
@@ -353,6 +500,24 @@ function serializeJson(value, limit = MAX_BODY_SIZE) {
|
|
|
353
500
|
return full.slice(0, limit) + `
|
|
354
501
|
... [truncated, ${full.length} chars total]`;
|
|
355
502
|
}
|
|
503
|
+
function fitArrayField(value, key) {
|
|
504
|
+
const items = value[key];
|
|
505
|
+
if (!Array.isArray(items) || jsonSize(value) <= MAX_BODY_SIZE) return value;
|
|
506
|
+
const cut = (count) => ({
|
|
507
|
+
...value,
|
|
508
|
+
[key]: items.slice(0, count),
|
|
509
|
+
truncated: true,
|
|
510
|
+
returned: count
|
|
511
|
+
});
|
|
512
|
+
let low = 0;
|
|
513
|
+
let high = items.length;
|
|
514
|
+
while (low < high) {
|
|
515
|
+
const mid = Math.ceil((low + high) / 2);
|
|
516
|
+
if (jsonSize(cut(mid)) <= MAX_BODY_SIZE) low = mid;
|
|
517
|
+
else high = mid - 1;
|
|
518
|
+
}
|
|
519
|
+
return cut(low);
|
|
520
|
+
}
|
|
356
521
|
function jsonContent(value) {
|
|
357
522
|
return textContent(serializeJson(value));
|
|
358
523
|
}
|
|
@@ -501,6 +666,99 @@ async function summarizeResponse(response) {
|
|
|
501
666
|
body: await readBodyTruncated(response)
|
|
502
667
|
};
|
|
503
668
|
}
|
|
669
|
+
async function emailEndpointView(client, slug) {
|
|
670
|
+
const endpoint = await client.endpoints.get(slug);
|
|
671
|
+
return {
|
|
672
|
+
endpoint: { slug: endpoint.slug, name: endpoint.name ?? null },
|
|
673
|
+
includeExtracts: endpoint.showEmailExtracts !== false
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function emailSlugFromAddress(address) {
|
|
677
|
+
const local = address.slice(0, Math.max(0, address.lastIndexOf("@")));
|
|
678
|
+
return local.split("+")[0].toLowerCase();
|
|
679
|
+
}
|
|
680
|
+
var MAX_SUMMARY_LINK = 8e3;
|
|
681
|
+
function boundedLink(link, max) {
|
|
682
|
+
if (link == null || link.length <= max) return { link: link ?? null };
|
|
683
|
+
return {
|
|
684
|
+
link: null,
|
|
685
|
+
linkOmitted: `The main link is ${link.length} characters long, too long for the output`
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
var MAX_SUMMARY_FIELD = 1e3;
|
|
689
|
+
function summarizeEmail(email, includeExtracts) {
|
|
690
|
+
const from = email.email.from[0];
|
|
691
|
+
const sender = from ? from.name ? `${from.name} <${from.address ?? ""}>` : from.address : null;
|
|
692
|
+
const subject = email.email.subject;
|
|
693
|
+
return {
|
|
694
|
+
id: email.id,
|
|
695
|
+
receivedAt: new Date(email.receivedAt).toISOString(),
|
|
696
|
+
address: email.path,
|
|
697
|
+
tag: email.email.tag,
|
|
698
|
+
subject: subject === null ? null : sliceText(subject, MAX_SUMMARY_FIELD),
|
|
699
|
+
from: sender == null ? null : sliceText(sender, MAX_SUMMARY_FIELD),
|
|
700
|
+
...includeExtracts ? { code: extractCode(email), ...boundedLink(extractLink(email), MAX_SUMMARY_LINK) } : {},
|
|
701
|
+
attachments: email.email.attachments.length
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
var ESSENTIAL_DETAIL_FIELDS = [
|
|
705
|
+
"code",
|
|
706
|
+
"link",
|
|
707
|
+
"id",
|
|
708
|
+
"endpoint",
|
|
709
|
+
"receivedAt",
|
|
710
|
+
"address",
|
|
711
|
+
"tag",
|
|
712
|
+
"subject",
|
|
713
|
+
"from",
|
|
714
|
+
"size",
|
|
715
|
+
"test",
|
|
716
|
+
"text",
|
|
717
|
+
"textTruncated",
|
|
718
|
+
"htmlSize",
|
|
719
|
+
"html",
|
|
720
|
+
"htmlTruncated"
|
|
721
|
+
];
|
|
722
|
+
function emailDetail(client, email, view, includeHtml) {
|
|
723
|
+
const { data } = client.emails.toJson(email, {
|
|
724
|
+
endpoint: view.endpoint,
|
|
725
|
+
includeExtracts: view.includeExtracts
|
|
726
|
+
});
|
|
727
|
+
const { html, text: fullText, ...rest } = data;
|
|
728
|
+
const { text, cut } = cutEmailText(fullText ?? null);
|
|
729
|
+
const restoreText = () => {
|
|
730
|
+
detail.text = text;
|
|
731
|
+
if (cut) detail.textTruncated = true;
|
|
732
|
+
else delete detail.textTruncated;
|
|
733
|
+
};
|
|
734
|
+
const detail = {
|
|
735
|
+
...view.includeExtracts ? { code: extractCode(email), link: extractLink(email) } : {},
|
|
736
|
+
...rest,
|
|
737
|
+
text,
|
|
738
|
+
...cut ? { textTruncated: true } : {},
|
|
739
|
+
htmlSize: html?.length ?? 0,
|
|
740
|
+
...includeHtml && html !== null ? { html } : {}
|
|
741
|
+
};
|
|
742
|
+
shrinkToFit(detail, MAX_BODY_SIZE, [
|
|
743
|
+
() => omitHeaders(detail),
|
|
744
|
+
() => capExtracts(detail),
|
|
745
|
+
() => capLists(detail),
|
|
746
|
+
() => cutStringToFit(detail, detail, "html", "htmlTruncated", MAX_BODY_SIZE),
|
|
747
|
+
() => cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE, TEXT_FLOOR),
|
|
748
|
+
// Down to the essentials (and without a main link too long to use) there
|
|
749
|
+
// may be room for more of the text again.
|
|
750
|
+
() => {
|
|
751
|
+
keepOnly(detail, ESSENTIAL_DETAIL_FIELDS);
|
|
752
|
+
if (view.includeExtracts) {
|
|
753
|
+
Object.assign(detail, boundedLink(detail.link, MAX_BODY_SIZE / 2));
|
|
754
|
+
}
|
|
755
|
+
restoreText();
|
|
756
|
+
cutStringToFit(detail, detail, "text", "textTruncated", MAX_BODY_SIZE);
|
|
757
|
+
},
|
|
758
|
+
() => cutStringToFit(detail, detail, "subject", "subjectTruncated", MAX_BODY_SIZE)
|
|
759
|
+
]);
|
|
760
|
+
return detail;
|
|
761
|
+
}
|
|
504
762
|
function registerTools(server, client) {
|
|
505
763
|
server.tool(
|
|
506
764
|
"create_endpoint",
|
|
@@ -644,7 +902,7 @@ function registerTools(server, client) {
|
|
|
644
902
|
);
|
|
645
903
|
server.tool(
|
|
646
904
|
"update_endpoint",
|
|
647
|
-
"Update an endpoint name, mock response, conditional response rules, or
|
|
905
|
+
"Update an endpoint name, mock response, conditional response rules, signing configuration, or whether codes and links are picked out of its emails.",
|
|
648
906
|
{
|
|
649
907
|
slug: z2.string().describe("The endpoint slug to update"),
|
|
650
908
|
name: z2.string().optional().describe("New display name"),
|
|
@@ -657,7 +915,8 @@ function registerTools(server, client) {
|
|
|
657
915
|
signingSecret: z2.string().nullable().optional().describe("New signing secret (encrypted server-side). Pass null to clear."),
|
|
658
916
|
signingHeader: z2.string().nullable().optional().describe(
|
|
659
917
|
"Custom signature header name (only for generic-hmac provider), or null to clear"
|
|
660
|
-
)
|
|
918
|
+
),
|
|
919
|
+
showEmailExtracts: z2.boolean().optional().describe("Pick codes and links out of emails (dashboard and forwarded JSON). Owner only.")
|
|
661
920
|
},
|
|
662
921
|
withErrorHandling(
|
|
663
922
|
async ({
|
|
@@ -668,7 +927,8 @@ function registerTools(server, client) {
|
|
|
668
927
|
notificationUrl,
|
|
669
928
|
signingProvider,
|
|
670
929
|
signingSecret,
|
|
671
|
-
signingHeader
|
|
930
|
+
signingHeader,
|
|
931
|
+
showEmailExtracts
|
|
672
932
|
}) => {
|
|
673
933
|
const endpoint = await client.endpoints.update(slug, {
|
|
674
934
|
name,
|
|
@@ -677,7 +937,8 @@ function registerTools(server, client) {
|
|
|
677
937
|
notificationUrl,
|
|
678
938
|
signingProvider,
|
|
679
939
|
signingSecret,
|
|
680
|
-
signingHeader
|
|
940
|
+
signingHeader,
|
|
941
|
+
showEmailExtracts
|
|
681
942
|
});
|
|
682
943
|
return jsonContent(endpoint);
|
|
683
944
|
}
|
|
@@ -789,15 +1050,16 @@ function registerTools(server, client) {
|
|
|
789
1050
|
);
|
|
790
1051
|
server.tool(
|
|
791
1052
|
"list_requests",
|
|
792
|
-
"List recent captured requests for an endpoint.",
|
|
1053
|
+
"List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
|
|
793
1054
|
{
|
|
794
1055
|
endpointSlug: z2.string().describe("The endpoint slug"),
|
|
795
1056
|
limit: z2.number().int().min(1).max(100).default(25).describe("Max requests to return"),
|
|
796
|
-
since: z2.number().optional().describe("Only return requests after this timestamp in ms")
|
|
1057
|
+
since: z2.number().optional().describe("Only return requests after this timestamp in ms"),
|
|
1058
|
+
kind: kindSchema
|
|
797
1059
|
},
|
|
798
|
-
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
799
|
-
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
800
|
-
return jsonContent(requests);
|
|
1060
|
+
withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
|
|
1061
|
+
const requests = await client.requests.list(endpointSlug, { limit, since, kind });
|
|
1062
|
+
return jsonContent(requests.map(compactRequest));
|
|
801
1063
|
})
|
|
802
1064
|
);
|
|
803
1065
|
server.tool(
|
|
@@ -806,6 +1068,7 @@ function registerTools(server, client) {
|
|
|
806
1068
|
{
|
|
807
1069
|
slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
|
|
808
1070
|
method: z2.string().optional().describe("Filter by HTTP method"),
|
|
1071
|
+
kind: kindSchema,
|
|
809
1072
|
q: z2.string().optional().describe("Free-text search across path, body, and headers"),
|
|
810
1073
|
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
811
1074
|
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"'),
|
|
@@ -813,10 +1076,11 @@ function registerTools(server, client) {
|
|
|
813
1076
|
offset: z2.number().int().min(0).max(1e4).default(0).describe("Result offset"),
|
|
814
1077
|
order: z2.enum(["asc", "desc"]).default("desc").describe("Sort order by received time")
|
|
815
1078
|
},
|
|
816
|
-
withErrorHandling(async ({ slug, method, q, from, to, limit, offset, order }) => {
|
|
1079
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to, limit, offset, order }) => {
|
|
817
1080
|
const results = await client.requests.search({
|
|
818
1081
|
slug,
|
|
819
1082
|
method,
|
|
1083
|
+
kind,
|
|
820
1084
|
q,
|
|
821
1085
|
from,
|
|
822
1086
|
to,
|
|
@@ -824,7 +1088,7 @@ function registerTools(server, client) {
|
|
|
824
1088
|
offset,
|
|
825
1089
|
order
|
|
826
1090
|
});
|
|
827
|
-
return jsonContent(results);
|
|
1091
|
+
return jsonContent(results.map(compactRequest));
|
|
828
1092
|
})
|
|
829
1093
|
);
|
|
830
1094
|
server.tool(
|
|
@@ -833,12 +1097,13 @@ function registerTools(server, client) {
|
|
|
833
1097
|
{
|
|
834
1098
|
slug: z2.string().optional().describe("Filter to a specific endpoint slug"),
|
|
835
1099
|
method: z2.string().optional().describe("Filter by HTTP method"),
|
|
1100
|
+
kind: kindSchema,
|
|
836
1101
|
q: z2.string().optional().describe("Free-text search across path, body, and headers"),
|
|
837
1102
|
from: durationOrTimestampSchema.optional().describe('Start time as a timestamp or duration like "1h" or "7d"'),
|
|
838
1103
|
to: durationOrTimestampSchema.optional().describe('End time as a timestamp or duration like "1h" or "7d"')
|
|
839
1104
|
},
|
|
840
|
-
withErrorHandling(async ({ slug, method, q, from, to }) => {
|
|
841
|
-
const count = await client.requests.count({ slug, method, q, from, to });
|
|
1105
|
+
withErrorHandling(async ({ slug, method, kind, q, from, to }) => {
|
|
1106
|
+
const count = await client.requests.count({ slug, method, kind, q, from, to });
|
|
842
1107
|
return jsonContent({ count });
|
|
843
1108
|
})
|
|
844
1109
|
);
|
|
@@ -848,7 +1113,7 @@ function registerTools(server, client) {
|
|
|
848
1113
|
{ requestId: z2.string().describe("The request ID") },
|
|
849
1114
|
withErrorHandling(async ({ requestId }) => {
|
|
850
1115
|
const request = await client.requests.get(requestId);
|
|
851
|
-
return jsonContent(request);
|
|
1116
|
+
return jsonContent(compactRequest(request));
|
|
852
1117
|
})
|
|
853
1118
|
);
|
|
854
1119
|
server.tool(
|
|
@@ -861,7 +1126,7 @@ function registerTools(server, client) {
|
|
|
861
1126
|
},
|
|
862
1127
|
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
863
1128
|
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
864
|
-
return jsonContent(request);
|
|
1129
|
+
return jsonContent(compactRequest(request));
|
|
865
1130
|
})
|
|
866
1131
|
);
|
|
867
1132
|
server.tool(
|
|
@@ -881,7 +1146,9 @@ function registerTools(server, client) {
|
|
|
881
1146
|
pollInterval,
|
|
882
1147
|
method
|
|
883
1148
|
});
|
|
884
|
-
return jsonContent(
|
|
1149
|
+
return jsonContent(
|
|
1150
|
+
fitArrayField({ ...result, requests: result.requests.map(compactRequest) }, "requests")
|
|
1151
|
+
);
|
|
885
1152
|
})
|
|
886
1153
|
);
|
|
887
1154
|
server.tool(
|
|
@@ -1190,6 +1457,154 @@ function registerTools(server, client) {
|
|
|
1190
1457
|
}
|
|
1191
1458
|
)
|
|
1192
1459
|
);
|
|
1460
|
+
server.tool(
|
|
1461
|
+
"list_emails",
|
|
1462
|
+
"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.",
|
|
1463
|
+
{
|
|
1464
|
+
endpointSlug: z2.string().describe("The endpoint slug"),
|
|
1465
|
+
limit: z2.number().int().min(1).max(100).default(25).describe(
|
|
1466
|
+
`Max emails to return. With tag, subject or from, the newest ${EMAIL_SCAN_LIMIT} emails are searched.`
|
|
1467
|
+
),
|
|
1468
|
+
since: z2.number().optional().describe("Only emails received after this timestamp in ms"),
|
|
1469
|
+
tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc (exact, case-sensitive)"),
|
|
1470
|
+
subject: z2.string().optional().describe("Only emails whose subject contains this text"),
|
|
1471
|
+
from: z2.string().optional().describe("Only emails from this address (case-insensitive)")
|
|
1472
|
+
},
|
|
1473
|
+
withErrorHandling(async ({ endpointSlug, limit, since, tag, subject, from }) => {
|
|
1474
|
+
const filtered = tag !== void 0 || subject !== void 0 || from !== void 0;
|
|
1475
|
+
const scan = filtered ? Math.max(limit, EMAIL_SCAN_LIMIT) : limit;
|
|
1476
|
+
const [emails, view] = await Promise.all([
|
|
1477
|
+
client.emails.list(endpointSlug, { limit: scan, since, tag, subject, from }),
|
|
1478
|
+
emailEndpointView(client, endpointSlug)
|
|
1479
|
+
]);
|
|
1480
|
+
return jsonContent(
|
|
1481
|
+
emails.slice(0, limit).map((email) => summarizeEmail(email, view.includeExtracts))
|
|
1482
|
+
);
|
|
1483
|
+
})
|
|
1484
|
+
);
|
|
1485
|
+
server.tool(
|
|
1486
|
+
"get_email",
|
|
1487
|
+
"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.",
|
|
1488
|
+
{
|
|
1489
|
+
requestId: z2.string().optional().describe("The email's request ID"),
|
|
1490
|
+
endpointSlug: z2.string().optional().describe("Get the newest email at this endpoint instead (with requestId unset)"),
|
|
1491
|
+
tag: z2.string().optional().describe("With endpointSlug: only emails sent to this +tag"),
|
|
1492
|
+
includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false, only its size)")
|
|
1493
|
+
},
|
|
1494
|
+
withErrorHandling(async ({ requestId, endpointSlug, tag, includeHtml }) => {
|
|
1495
|
+
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1496
|
+
throw new Error("Pass exactly one of requestId or endpointSlug");
|
|
1497
|
+
}
|
|
1498
|
+
const email = requestId ? await client.emails.get(requestId) : await client.emails.latest(endpointSlug, { tag });
|
|
1499
|
+
if (!email) {
|
|
1500
|
+
throw new NotFoundError(
|
|
1501
|
+
`No email at ${endpointSlug}${tag ? ` with tag "${tag}"` : ""} yet. Send one with send_test_email, or wait with wait_for_email.`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const view = await emailEndpointView(
|
|
1505
|
+
client,
|
|
1506
|
+
endpointSlug ?? emailSlugFromAddress(email.path)
|
|
1507
|
+
);
|
|
1508
|
+
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1509
|
+
})
|
|
1510
|
+
);
|
|
1511
|
+
server.tool(
|
|
1512
|
+
"wait_for_email",
|
|
1513
|
+
"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.",
|
|
1514
|
+
{
|
|
1515
|
+
endpointSlug: z2.string().describe("The endpoint slug"),
|
|
1516
|
+
tag: z2.string().optional().describe("Only emails sent to {slug}+{tag}@mailhooks.cc"),
|
|
1517
|
+
subject: z2.string().optional().describe("Only emails whose subject contains this text"),
|
|
1518
|
+
from: z2.string().optional().describe("Only emails from this address (case-insensitive)"),
|
|
1519
|
+
timeout: durationOrTimestampSchema.default("30s").describe(
|
|
1520
|
+
`How long to wait (default "30s"). Keep it under your MCP client's request timeout, often 60 seconds.`
|
|
1521
|
+
),
|
|
1522
|
+
since: z2.number().optional().describe("Only emails received after this timestamp in ms (default: five minutes ago)"),
|
|
1523
|
+
includeHtml: z2.boolean().default(false).describe("Include the HTML part (default: false)")
|
|
1524
|
+
},
|
|
1525
|
+
withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
|
|
1526
|
+
const [email, view] = await Promise.all([
|
|
1527
|
+
client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
|
|
1528
|
+
emailEndpointView(client, endpointSlug)
|
|
1529
|
+
]);
|
|
1530
|
+
return jsonContent(emailDetail(client, email, view, includeHtml));
|
|
1531
|
+
})
|
|
1532
|
+
);
|
|
1533
|
+
server.tool(
|
|
1534
|
+
"send_test_email",
|
|
1535
|
+
"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.",
|
|
1536
|
+
{
|
|
1537
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1538
|
+
tag: z2.string().optional().describe("Deliver to {slug}+{tag}@mailhooks.cc")
|
|
1539
|
+
},
|
|
1540
|
+
withErrorHandling(async ({ slug, tag }) => {
|
|
1541
|
+
const result = await client.emails.sendTest(slug, { tag });
|
|
1542
|
+
return jsonContent({ ...result, address: client.emails.address(slug, tag) });
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
server.tool(
|
|
1546
|
+
"configure_forwarding",
|
|
1547
|
+
"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.",
|
|
1548
|
+
{
|
|
1549
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1550
|
+
url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
|
|
1551
|
+
enabled: z2.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
|
|
1552
|
+
},
|
|
1553
|
+
withErrorHandling(async ({ slug, url, enabled }) => {
|
|
1554
|
+
const endpoint = await client.forwarding.configure(slug, { url, enabled });
|
|
1555
|
+
return jsonContent({
|
|
1556
|
+
slug: endpoint.slug,
|
|
1557
|
+
forwardEnabled: endpoint.forwardEnabled ?? false,
|
|
1558
|
+
forwardUrl: endpoint.forwardUrl ?? null,
|
|
1559
|
+
hasForwardSecret: endpoint.hasForwardSecret ?? false
|
|
1560
|
+
});
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1563
|
+
server.tool(
|
|
1564
|
+
"get_forwarding_secret",
|
|
1565
|
+
"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.",
|
|
1566
|
+
{
|
|
1567
|
+
slug: z2.string().describe("The endpoint slug"),
|
|
1568
|
+
rotate: z2.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
|
|
1569
|
+
},
|
|
1570
|
+
withErrorHandling(async ({ slug, rotate }) => {
|
|
1571
|
+
const secret = rotate ? await client.forwarding.rotateSecret(slug) : await client.forwarding.secret(slug);
|
|
1572
|
+
return jsonContent({ secret, rotated: rotate });
|
|
1573
|
+
})
|
|
1574
|
+
);
|
|
1575
|
+
server.tool(
|
|
1576
|
+
"test_forwarding",
|
|
1577
|
+
"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.",
|
|
1578
|
+
{ slug: z2.string().describe("The endpoint slug") },
|
|
1579
|
+
withErrorHandling(async ({ slug }) => {
|
|
1580
|
+
return jsonContent(await client.forwarding.test(slug));
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
server.tool(
|
|
1584
|
+
"list_deliveries",
|
|
1585
|
+
"List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
|
|
1586
|
+
{
|
|
1587
|
+
endpointSlug: z2.string().optional().describe("List the endpoint's latest deliveries"),
|
|
1588
|
+
requestId: z2.string().optional().describe("List every delivery and try of this email"),
|
|
1589
|
+
limit: z2.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
|
|
1590
|
+
},
|
|
1591
|
+
withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
|
|
1592
|
+
if (requestId === void 0 === (endpointSlug === void 0)) {
|
|
1593
|
+
throw new Error("Pass exactly one of endpointSlug or requestId");
|
|
1594
|
+
}
|
|
1595
|
+
return jsonContent(
|
|
1596
|
+
requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
|
|
1597
|
+
);
|
|
1598
|
+
})
|
|
1599
|
+
);
|
|
1600
|
+
server.tool(
|
|
1601
|
+
"redeliver_email",
|
|
1602
|
+
"Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
|
|
1603
|
+
{ requestId: z2.string().describe("The email's request ID") },
|
|
1604
|
+
withErrorHandling(async ({ requestId }) => {
|
|
1605
|
+
return jsonContent(await client.forwarding.redeliver(requestId));
|
|
1606
|
+
})
|
|
1607
|
+
);
|
|
1193
1608
|
server.tool(
|
|
1194
1609
|
"describe",
|
|
1195
1610
|
"Describe all available SDK operations, parameters, and types.",
|
|
@@ -1304,7 +1719,7 @@ function registerAgentRegistrationTools(server) {
|
|
|
1304
1719
|
}
|
|
1305
1720
|
|
|
1306
1721
|
// src/index.ts
|
|
1307
|
-
var VERSION = true ? "1.
|
|
1722
|
+
var VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
1308
1723
|
function createServer(options = {}) {
|
|
1309
1724
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
1310
1725
|
const server = new McpServer2({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webhooks-cc/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://webhooks.cc",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
-
"zod": "^4.
|
|
41
|
-
"@webhooks-cc/sdk": "1.
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
40
|
+
"zod": "^4.6.5",
|
|
41
|
+
"@webhooks-cc/sdk": "1.12.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^26.4
|
|
44
|
+
"@types/node": "^26.6.4",
|
|
45
45
|
"tsup": "^8.5.1",
|
|
46
46
|
"typescript": "^6.0.3",
|
|
47
|
-
"vitest": "^
|
|
47
|
+
"vitest": "^5.0.2"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsup",
|