@webhooks-cc/mcp 1.16.0 → 1.18.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 +14 -2
- package/dist/bin/mcp.js +240 -6
- package/dist/{chunk-4LEPLCW5.mjs → chunk-GTEE7L2Y.mjs} +233 -2
- package/dist/index.js +240 -6
- package/dist/index.mjs +13 -5
- package/dist/tools.d.mts +22 -2
- package/dist/tools.d.ts +22 -2
- package/dist/tools.js +238 -4
- package/dist/tools.mjs +9 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ For other agents, `npx skills add kroqdotdev/webhooks-cc` installs the same skil
|
|
|
34
34
|
- **claude.ai and Claude Desktop**: Settings, Connectors, add a custom connector with the URL `https://webhooks.cc/mcp`
|
|
35
35
|
- **ChatGPT**: add it as a custom connector (developer mode)
|
|
36
36
|
|
|
37
|
-
A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has
|
|
37
|
+
A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has 54 tools: `send_to`, `replay_request` and `share_port` are left out and `targetUrl` is refused, because a hosted server cannot reach your machine, and waits are capped at 50 seconds. It needs an account (no sandbox), and serves tools only, without prompts or resources.
|
|
38
38
|
|
|
39
39
|
### One-click
|
|
40
40
|
|
|
@@ -83,13 +83,14 @@ Leave out `env` to start without a key. Get your API key at [webhooks.cc/account
|
|
|
83
83
|
|
|
84
84
|
## What it exposes
|
|
85
85
|
|
|
86
|
-
The server exposes `
|
|
86
|
+
The server exposes `57` tools, `3` prompts, and `3` resource surfaces with an API key, the same `57` tools and the prompts with a saved connection, and `11` sandbox tools otherwise. The remote server exposes `54` tools.
|
|
87
87
|
|
|
88
88
|
### Endpoint tools
|
|
89
89
|
|
|
90
90
|
- `create_endpoint`
|
|
91
91
|
- `list_endpoints`
|
|
92
92
|
- `get_endpoint`
|
|
93
|
+
- `favorite_endpoint` (mark or unmark one of your favourites; `list_endpoints` and `get_endpoint` show `favorite`)
|
|
93
94
|
- `update_endpoint`
|
|
94
95
|
- `preview_response`
|
|
95
96
|
- `delete_endpoint`
|
|
@@ -154,6 +155,17 @@ retried and logged (owner only):
|
|
|
154
155
|
|
|
155
156
|
Endpoints shared with you already appear in `list_endpoints` (with `fromTeam`) and work with every request tool except `clear_requests`, which stays with the owner. Invites are not exposed: accepting one claims a paid seat.
|
|
156
157
|
|
|
158
|
+
### Sharing tools
|
|
159
|
+
|
|
160
|
+
- `share_port`: shares a local port at a `https://<name>.sharehooks.cc` link by running `whk share` (local server only; needs [whk](https://webhooks.cc/installation) installed, or `WHK_PATH` naming it). The share runs until `stop_share`, the plan's session limit, or the end of the server. Free shares get a random name ending in `-free`, two hours, and a notice page before the site; Pro and Business can pass a reserved `name` and a `password`.
|
|
161
|
+
- `list_shares`: running and recent previews and live tunnels, this month's shared data, the plan's sharing limits, and `hints` with the next step (the server's own in tool names, plus slots or data running out).
|
|
162
|
+
- `stop_share`: stops a preview or live tunnel, and says whether it had already ended.
|
|
163
|
+
- `share_requests`: the last requests visitors made to a running preview (no addresses).
|
|
164
|
+
- `list_names`, `reserve_name` (optionally inside a team), `release_name`: reserved preview names (Pro and Business).
|
|
165
|
+
- `set_name_password`, `clear_name_password`: the password visitors need for every preview under a name. The password is never echoed back.
|
|
166
|
+
|
|
167
|
+
Everything the dashboard's Shares page does is here, so an agent never needs it; only `share_port` needs the local server.
|
|
168
|
+
|
|
157
169
|
### Discovery and account tools
|
|
158
170
|
|
|
159
171
|
- `list_provider_templates`
|
package/dist/bin/mcp.js
CHANGED
|
@@ -158,6 +158,15 @@ function omitHeaders(holder) {
|
|
|
158
158
|
holder.headers = {};
|
|
159
159
|
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
160
160
|
}
|
|
161
|
+
function trimAuth(holder) {
|
|
162
|
+
const auth = holder.auth;
|
|
163
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
164
|
+
const verdicts = { ...auth };
|
|
165
|
+
delete verdicts.setup;
|
|
166
|
+
delete verdicts.authenticationResults;
|
|
167
|
+
holder.auth = verdicts;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
161
170
|
function trimSpam(holder) {
|
|
162
171
|
const spam = holder.spam;
|
|
163
172
|
if (spam && typeof spam === "object" && !Array.isArray(spam)) {
|
|
@@ -305,6 +314,7 @@ function compactRequest(request) {
|
|
|
305
314
|
shrinkToFit(root, budget, [
|
|
306
315
|
() => trimParsed(compact),
|
|
307
316
|
() => omitHeaders(compact),
|
|
317
|
+
() => trimAuth(email),
|
|
308
318
|
() => trimSpam(email),
|
|
309
319
|
() => capLists(email),
|
|
310
320
|
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
@@ -445,6 +455,10 @@ function registerResources(server, client) {
|
|
|
445
455
|
var import_zod3 = require("zod");
|
|
446
456
|
var import_sdk2 = require("@webhooks-cc/sdk");
|
|
447
457
|
|
|
458
|
+
// src/version.ts
|
|
459
|
+
var VERSION = true ? "1.18.0" : "0.0.0-dev";
|
|
460
|
+
var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
|
|
461
|
+
|
|
448
462
|
// src/tools.ts
|
|
449
463
|
var import_zod2 = require("zod");
|
|
450
464
|
var import_sdk = require("@webhooks-cc/sdk");
|
|
@@ -996,7 +1010,7 @@ function emailDetail(client, email, view, includeHtml) {
|
|
|
996
1010
|
]);
|
|
997
1011
|
return detail;
|
|
998
1012
|
}
|
|
999
|
-
var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
|
|
1013
|
+
var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request", "share_port"]);
|
|
1000
1014
|
var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
|
|
1001
1015
|
var REMOTE_MAX_WAIT_MS = 5e4;
|
|
1002
1016
|
var REMOTE_MIN_POLL_MS = 1e3;
|
|
@@ -1054,9 +1068,78 @@ function toolTarget(target, remote, handles) {
|
|
|
1054
1068
|
}
|
|
1055
1069
|
};
|
|
1056
1070
|
}
|
|
1071
|
+
function shareSummary(live) {
|
|
1072
|
+
const notes = [];
|
|
1073
|
+
if (live.notice) {
|
|
1074
|
+
notes.push(
|
|
1075
|
+
"The first time each browser opens the link, it sees a short notice from webhooks.cc and clicks Open the preview (Free, and paid accounts in their first 7 days)."
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
if (live.password) notes.push("Visitors need the password.");
|
|
1079
|
+
if (live.expiresAt) {
|
|
1080
|
+
notes.push(
|
|
1081
|
+
`The plan stops this share at ${live.expiresAt}; reconnecting does not extend it, and starting it again gives a new link.`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
notes.push("It runs until stop_share or the end of this MCP server.");
|
|
1085
|
+
return {
|
|
1086
|
+
url: live.url,
|
|
1087
|
+
name: live.name,
|
|
1088
|
+
port: live.port,
|
|
1089
|
+
expiresAt: live.expiresAt,
|
|
1090
|
+
notice: live.notice,
|
|
1091
|
+
password: live.password,
|
|
1092
|
+
notes: notes.join(" ")
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
var REMOTE_SHARE = "`whk share <port>` on the person's computer (or the local MCP server, npx -y @webhooks-cc/mcp, which has share_port)";
|
|
1096
|
+
function shareHints(list, remote = false) {
|
|
1097
|
+
const start = remote ? REMOTE_SHARE : "share_port";
|
|
1098
|
+
const hints = [];
|
|
1099
|
+
const limits = list.limits;
|
|
1100
|
+
const usage = list.usage;
|
|
1101
|
+
const previews = list.shares.filter((s) => s.kind !== "live" && !s.endedAt && s.mine !== false);
|
|
1102
|
+
if (usage?.over) {
|
|
1103
|
+
hints.push(
|
|
1104
|
+
`This month's shared data is used up, so previews are paused until ${usage.resetsAt}. A larger plan (Pro 25 GB, Business 150 GB) raises it; the person upgrades on webhooks.cc.`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
if (limits && previews.length >= limits.shares) {
|
|
1108
|
+
hints.push(
|
|
1109
|
+
`All ${limits.shares} of the plan's preview slots are in use: stop one with stop_share (its id is in shares) before ${start}.`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
if (limits && limits.sessionMinutes) {
|
|
1113
|
+
hints.push(
|
|
1114
|
+
`Previews on this plan stop after ${limits.sessionMinutes} minutes; ${start} again gives a new link.`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (limits && !limits.passwords) {
|
|
1118
|
+
hints.push("Passwords and reserved names come with Pro and Business.");
|
|
1119
|
+
}
|
|
1120
|
+
return hints;
|
|
1121
|
+
}
|
|
1122
|
+
function toolWords(hint, remote = false) {
|
|
1123
|
+
const words = hint.replace(/whk shares stop(?: <[^>]*>)?/g, "stop_share").replace(/whk shares (?:ls|list)/g, "list_shares").replace(/whk shares log(?: <[^>]*>)?/g, "share_requests").replace(/whk names reserve(?: <[^>]*>)?(?: --team <[^>]*>)?/g, "reserve_name").replace(/whk names release(?: <[^>]*>)?/g, "release_name").replace(/whk names password(?: <[^>]*>)? --clear/g, "clear_name_password").replace(/whk names password(?: <[^>]*>)?/g, "set_name_password").replace(/whk names ls/g, "list_names");
|
|
1124
|
+
if (remote) return words;
|
|
1125
|
+
return words.replace(/whk share <port> --password/g, "share_port with a password").replace(/whk share <port>/g, "share_port");
|
|
1126
|
+
}
|
|
1127
|
+
function withShareHints(list, remote = false) {
|
|
1128
|
+
const server = (list.hints ?? []).map((h) => toolWords(h, remote));
|
|
1129
|
+
const ours = shareHints(list, remote);
|
|
1130
|
+
if (list.suspended && !server.some((h) => /suspended/i.test(h))) {
|
|
1131
|
+
ours.unshift(
|
|
1132
|
+
`Sharing is suspended on this account: ${remote ? "new previews are" : "share_port is"} refused. The person can write to support@webhooks.cc.`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const hints = [.../* @__PURE__ */ new Set([...server, ...ours])];
|
|
1136
|
+
return hints.length > 0 ? { ...list, hints } : { ...list };
|
|
1137
|
+
}
|
|
1057
1138
|
function registerTools(target, client, options = {}) {
|
|
1058
1139
|
const handles = [];
|
|
1059
|
-
const
|
|
1140
|
+
const remote = options.remote === true;
|
|
1141
|
+
const server = toolTarget(target, remote, handles);
|
|
1142
|
+
const shareWith = remote ? REMOTE_SHARE : "share_port";
|
|
1060
1143
|
server.tool(
|
|
1061
1144
|
"create_endpoint",
|
|
1062
1145
|
"Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
|
|
@@ -1177,6 +1260,18 @@ function registerTools(target, client, options = {}) {
|
|
|
1177
1260
|
return jsonContent(endpoint);
|
|
1178
1261
|
})
|
|
1179
1262
|
);
|
|
1263
|
+
server.tool(
|
|
1264
|
+
"favorite_endpoint",
|
|
1265
|
+
"Mark an endpoint as one of your favourites, or unmark it with favorite: false. A favourite is your own choice, so endpoints shared with you through teams can be marked too; list_endpoints and get_endpoint show it as favorite. Idempotent.",
|
|
1266
|
+
{
|
|
1267
|
+
slug: import_zod2.z.string().describe("The endpoint slug"),
|
|
1268
|
+
favorite: import_zod2.z.boolean().optional().describe("true marks it as a favourite (the default), false unmarks it")
|
|
1269
|
+
},
|
|
1270
|
+
withErrorHandling(async ({ slug, favorite }) => {
|
|
1271
|
+
const result = favorite === false ? await client.endpoints.unfavorite(slug) : await client.endpoints.favorite(slug);
|
|
1272
|
+
return jsonContent({ slug, favorite: result.favorite });
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1180
1275
|
server.tool(
|
|
1181
1276
|
"update_endpoint",
|
|
1182
1277
|
"Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
|
|
@@ -2105,6 +2200,143 @@ function registerTools(target, client, options = {}) {
|
|
|
2105
2200
|
);
|
|
2106
2201
|
})
|
|
2107
2202
|
);
|
|
2203
|
+
const started = /* @__PURE__ */ new Map();
|
|
2204
|
+
server.tool(
|
|
2205
|
+
"share_port",
|
|
2206
|
+
"Share a port on this computer at a public link (https://<name>.sharehooks.cc) by running the webhooks.cc CLI (whk share), so a person can open the site you are building. Returns the link. It keeps running until stop_share, the plan's session limit, or the end of this MCP server. Asking again for the same port without a name or password returns the running link; with a name or password, the running share on that port stops and a new one starts. Free accounts get a random name ending in -free, a 2-hour limit and a notice page before the site; Pro and Business can use a reserved name and a password. Needs whk installed on this computer. For a webhook provider that must get your local app's real answer, use `whk tunnel <port> --live` instead.",
|
|
2207
|
+
{
|
|
2208
|
+
port: import_zod2.z.number().int().min(1).max(65535).describe("The local port the site runs on"),
|
|
2209
|
+
name: import_zod2.z.string().optional().describe("A name you reserved (Pro and Business); leave out for a random one"),
|
|
2210
|
+
password: import_zod2.z.string().min(8).max(200).optional().describe("What visitors must type (Pro and Business), 8 to 200 characters")
|
|
2211
|
+
},
|
|
2212
|
+
withErrorHandling(async ({ port, name, password }) => {
|
|
2213
|
+
const running = started.get(port);
|
|
2214
|
+
if (running && !name && !password) {
|
|
2215
|
+
return jsonContent({ ...shareSummary(running), alreadyRunning: true });
|
|
2216
|
+
}
|
|
2217
|
+
if (running) {
|
|
2218
|
+
started.delete(port);
|
|
2219
|
+
await running.stop();
|
|
2220
|
+
}
|
|
2221
|
+
let live;
|
|
2222
|
+
try {
|
|
2223
|
+
live = await client.shares.start({
|
|
2224
|
+
port,
|
|
2225
|
+
...name ? { name } : {},
|
|
2226
|
+
...password ? { password } : {},
|
|
2227
|
+
...process.env.WHK_PATH ? { whkPath: process.env.WHK_PATH } : {}
|
|
2228
|
+
});
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2231
|
+
throw new Error(
|
|
2232
|
+
`${message} With these tools: list_shares shows what runs and the plan's limits, stop_share frees a slot, list_names shows the names you can use.`
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
started.set(port, live);
|
|
2236
|
+
void live.closed.then(() => {
|
|
2237
|
+
if (started.get(port) === live) started.delete(port);
|
|
2238
|
+
});
|
|
2239
|
+
const summary = shareSummary(live);
|
|
2240
|
+
try {
|
|
2241
|
+
const list = await client.shares.list({ state: "live" });
|
|
2242
|
+
summary.limits = list.limits;
|
|
2243
|
+
summary.usage = list.usage;
|
|
2244
|
+
const hints = shareHints(list);
|
|
2245
|
+
if (hints.length > 0) summary.hints = hints;
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
return jsonContent(summary);
|
|
2249
|
+
})
|
|
2250
|
+
);
|
|
2251
|
+
server.tool(
|
|
2252
|
+
"list_shares",
|
|
2253
|
+
`List your public previews of local ports (whk share${remote ? "" : ", share_port"}) and live tunnels (whk tunnel --live): live ones first, then the latest that ended, with this month's shared data and your plan's sharing limits. Not the same as share_endpoint, which shares an endpoint with a team.`,
|
|
2254
|
+
{
|
|
2255
|
+
state: import_zod2.z.enum(["live", "ended"]).optional().describe("Only the running ones, or only the ended ones")
|
|
2256
|
+
},
|
|
2257
|
+
withErrorHandling(async ({ state }) => {
|
|
2258
|
+
return jsonContent(withShareHints(await client.shares.list(state ? { state } : {}), remote));
|
|
2259
|
+
})
|
|
2260
|
+
);
|
|
2261
|
+
server.tool(
|
|
2262
|
+
"stop_share",
|
|
2263
|
+
"Stop a public preview or live tunnel by its id (from list_shares). The CLI running it is told why and exits.",
|
|
2264
|
+
{
|
|
2265
|
+
id: import_zod2.z.string().describe("The share's id, from list_shares")
|
|
2266
|
+
},
|
|
2267
|
+
withErrorHandling(async ({ id }) => {
|
|
2268
|
+
const result = await client.shares.stop(id);
|
|
2269
|
+
return jsonContent({
|
|
2270
|
+
id,
|
|
2271
|
+
stopped: result.stopped,
|
|
2272
|
+
edgeReached: result.edgeReached,
|
|
2273
|
+
note: result.stopped ? result.edgeReached ? "Stopped; its CLI was told and exits." : "Stopped; the edge could not be told at once, so it ends within half a minute." : "It had already ended."
|
|
2274
|
+
});
|
|
2275
|
+
})
|
|
2276
|
+
);
|
|
2277
|
+
server.tool(
|
|
2278
|
+
"share_requests",
|
|
2279
|
+
"The last requests visitors made to a running preview (from list_shares), newest first: method, path, status, time, size, country, device and a number per visitor, never an address. Use it to see whether the person opened the link and what failed for them. `available` is false when the log could not be read just now.",
|
|
2280
|
+
{
|
|
2281
|
+
id: import_zod2.z.string().describe("The share's id, from list_shares")
|
|
2282
|
+
},
|
|
2283
|
+
withErrorHandling(async ({ id }) => {
|
|
2284
|
+
return jsonContent(await client.shares.requests(id));
|
|
2285
|
+
})
|
|
2286
|
+
);
|
|
2287
|
+
server.tool(
|
|
2288
|
+
"list_names",
|
|
2289
|
+
`List the reserved preview names you can share under (Pro and Business): your own and your teams', each with its link, whether it has a password, whether a preview runs under it now, and whether your plan still covers it (usable). Includes how many your plan allows and how many you hold. Share under one with ${remote ? "`whk share <port> --name <name>` on the person's computer (or the local MCP server's share_port)" : "share_port and its name"}.`,
|
|
2290
|
+
{},
|
|
2291
|
+
withErrorHandling(async () => {
|
|
2292
|
+
return jsonContent(await client.names.list());
|
|
2293
|
+
})
|
|
2294
|
+
);
|
|
2295
|
+
server.tool(
|
|
2296
|
+
"reserve_name",
|
|
2297
|
+
`Reserve a preview name, so ${shareWith} can use the link https://<name>.sharehooks.cc (Pro and Business). Lowercase letters, digits and dashes; names that look like random ones or end in -free are refused. With team, the name belongs to that team and every member may share under it.`,
|
|
2298
|
+
{
|
|
2299
|
+
name: import_zod2.z.string().min(1).max(63).describe("The name: lowercase letters, digits and dashes"),
|
|
2300
|
+
team: import_zod2.z.string().optional().describe("A team's id or name (from list_teams), to keep the name inside that team")
|
|
2301
|
+
},
|
|
2302
|
+
withErrorHandling(async ({ name, team }) => {
|
|
2303
|
+
return jsonContent(await client.names.reserve(name, team ? { team } : {}));
|
|
2304
|
+
})
|
|
2305
|
+
);
|
|
2306
|
+
server.tool(
|
|
2307
|
+
"release_name",
|
|
2308
|
+
"Release a reserved preview name you kept (or, for a team name, as the team's owner). A preview running under it stops, and the link stops working.",
|
|
2309
|
+
{
|
|
2310
|
+
name: import_zod2.z.string().min(1).max(63).describe("The name, from list_names")
|
|
2311
|
+
},
|
|
2312
|
+
withErrorHandling(async ({ name }) => {
|
|
2313
|
+
await client.names.release(name);
|
|
2314
|
+
return jsonContent({ released: name });
|
|
2315
|
+
})
|
|
2316
|
+
);
|
|
2317
|
+
server.tool(
|
|
2318
|
+
"set_name_password",
|
|
2319
|
+
"Put a password on every preview under a reserved name (Pro and Business): visitors must type it before the page opens. The password is not shown back; give it to the person yourself.",
|
|
2320
|
+
{
|
|
2321
|
+
name: import_zod2.z.string().min(1).max(63).describe("The name, from list_names"),
|
|
2322
|
+
password: import_zod2.z.string().min(8).max(200).describe("What visitors must type, 8 to 200 characters")
|
|
2323
|
+
},
|
|
2324
|
+
withErrorHandling(async ({ name, password }) => {
|
|
2325
|
+
await client.names.setPassword(name, password);
|
|
2326
|
+
return jsonContent({ name, password: true });
|
|
2327
|
+
})
|
|
2328
|
+
);
|
|
2329
|
+
server.tool(
|
|
2330
|
+
"clear_name_password",
|
|
2331
|
+
"Take the password off a reserved name, so its previews open without one.",
|
|
2332
|
+
{
|
|
2333
|
+
name: import_zod2.z.string().min(1).max(63).describe("The name, from list_names")
|
|
2334
|
+
},
|
|
2335
|
+
withErrorHandling(async ({ name }) => {
|
|
2336
|
+
await client.names.clearPassword(name);
|
|
2337
|
+
return jsonContent({ name, password: false });
|
|
2338
|
+
})
|
|
2339
|
+
);
|
|
2108
2340
|
server.tool(
|
|
2109
2341
|
"describe",
|
|
2110
2342
|
"Describe all available SDK operations, parameters, and types.",
|
|
@@ -2149,7 +2381,8 @@ var AgentSession = class {
|
|
|
2149
2381
|
baseUrl: this.baseUrl,
|
|
2150
2382
|
webhookUrl: this.webhookUrl,
|
|
2151
2383
|
clientName: this.clientName,
|
|
2152
|
-
credentialsFile: this.credentialsFile
|
|
2384
|
+
credentialsFile: this.credentialsFile,
|
|
2385
|
+
userAgent: MCP_USER_AGENT
|
|
2153
2386
|
};
|
|
2154
2387
|
}
|
|
2155
2388
|
/**
|
|
@@ -2242,6 +2475,7 @@ function registerSandboxTools(server, options = {}) {
|
|
|
2242
2475
|
baseUrl,
|
|
2243
2476
|
webhookUrl,
|
|
2244
2477
|
credentialsFile: options.credentialsFile,
|
|
2478
|
+
userAgent: MCP_USER_AGENT,
|
|
2245
2479
|
// The human disconnected the agent: back to the sandbox, unless a
|
|
2246
2480
|
// newer connection is already on display.
|
|
2247
2481
|
onRevoked: () => queueMicrotask(() => {
|
|
@@ -2403,7 +2637,8 @@ function sandboxToolSet(server, session, showAccount) {
|
|
|
2403
2637
|
webhookUrl: session.webhookUrl,
|
|
2404
2638
|
email: normalized,
|
|
2405
2639
|
clientName: session.clientName,
|
|
2406
|
-
credentialsFile: session.credentialsFile
|
|
2640
|
+
credentialsFile: session.credentialsFile,
|
|
2641
|
+
userAgent: MCP_USER_AGENT
|
|
2407
2642
|
}));
|
|
2408
2643
|
session.connect = { via: "connect", pending, email: normalized };
|
|
2409
2644
|
attempt = pending;
|
|
@@ -2509,7 +2744,6 @@ function sandboxToolSet(server, session, showAccount) {
|
|
|
2509
2744
|
}
|
|
2510
2745
|
|
|
2511
2746
|
// src/index.ts
|
|
2512
|
-
var VERSION = true ? "1.16.0" : "0.0.0-dev";
|
|
2513
2747
|
function createServer(options = {}) {
|
|
2514
2748
|
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
2515
2749
|
const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
|
|
@@ -2524,7 +2758,7 @@ function createServer(options = {}) {
|
|
|
2524
2758
|
registerPrompts(server);
|
|
2525
2759
|
return server;
|
|
2526
2760
|
}
|
|
2527
|
-
const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl });
|
|
2761
|
+
const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl, userAgent: MCP_USER_AGENT });
|
|
2528
2762
|
registerTools(server, client);
|
|
2529
2763
|
registerPrompts(server);
|
|
2530
2764
|
registerResources(server, client);
|
|
@@ -67,6 +67,15 @@ function omitHeaders(holder) {
|
|
|
67
67
|
holder.headers = {};
|
|
68
68
|
holder.headersOmitted = `${count} headers left out to fit the output`;
|
|
69
69
|
}
|
|
70
|
+
function trimAuth(holder) {
|
|
71
|
+
const auth = holder.auth;
|
|
72
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
73
|
+
const verdicts = { ...auth };
|
|
74
|
+
delete verdicts.setup;
|
|
75
|
+
delete verdicts.authenticationResults;
|
|
76
|
+
holder.auth = verdicts;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
70
79
|
function trimSpam(holder) {
|
|
71
80
|
const spam = holder.spam;
|
|
72
81
|
if (spam && typeof spam === "object" && !Array.isArray(spam)) {
|
|
@@ -214,6 +223,7 @@ function compactRequest(request) {
|
|
|
214
223
|
shrinkToFit(root, budget, [
|
|
215
224
|
() => trimParsed(compact),
|
|
216
225
|
() => omitHeaders(compact),
|
|
226
|
+
() => trimAuth(email),
|
|
217
227
|
() => trimSpam(email),
|
|
218
228
|
() => capLists(email),
|
|
219
229
|
() => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
|
|
@@ -778,7 +788,7 @@ function emailDetail(client, email, view, includeHtml) {
|
|
|
778
788
|
]);
|
|
779
789
|
return detail;
|
|
780
790
|
}
|
|
781
|
-
var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
|
|
791
|
+
var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request", "share_port"]);
|
|
782
792
|
var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
|
|
783
793
|
var REMOTE_MAX_WAIT_MS = 5e4;
|
|
784
794
|
var REMOTE_MIN_POLL_MS = 1e3;
|
|
@@ -836,9 +846,78 @@ function toolTarget(target, remote, handles) {
|
|
|
836
846
|
}
|
|
837
847
|
};
|
|
838
848
|
}
|
|
849
|
+
function shareSummary(live) {
|
|
850
|
+
const notes = [];
|
|
851
|
+
if (live.notice) {
|
|
852
|
+
notes.push(
|
|
853
|
+
"The first time each browser opens the link, it sees a short notice from webhooks.cc and clicks Open the preview (Free, and paid accounts in their first 7 days)."
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
if (live.password) notes.push("Visitors need the password.");
|
|
857
|
+
if (live.expiresAt) {
|
|
858
|
+
notes.push(
|
|
859
|
+
`The plan stops this share at ${live.expiresAt}; reconnecting does not extend it, and starting it again gives a new link.`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
notes.push("It runs until stop_share or the end of this MCP server.");
|
|
863
|
+
return {
|
|
864
|
+
url: live.url,
|
|
865
|
+
name: live.name,
|
|
866
|
+
port: live.port,
|
|
867
|
+
expiresAt: live.expiresAt,
|
|
868
|
+
notice: live.notice,
|
|
869
|
+
password: live.password,
|
|
870
|
+
notes: notes.join(" ")
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
var REMOTE_SHARE = "`whk share <port>` on the person's computer (or the local MCP server, npx -y @webhooks-cc/mcp, which has share_port)";
|
|
874
|
+
function shareHints(list, remote = false) {
|
|
875
|
+
const start = remote ? REMOTE_SHARE : "share_port";
|
|
876
|
+
const hints = [];
|
|
877
|
+
const limits = list.limits;
|
|
878
|
+
const usage = list.usage;
|
|
879
|
+
const previews = list.shares.filter((s) => s.kind !== "live" && !s.endedAt && s.mine !== false);
|
|
880
|
+
if (usage?.over) {
|
|
881
|
+
hints.push(
|
|
882
|
+
`This month's shared data is used up, so previews are paused until ${usage.resetsAt}. A larger plan (Pro 25 GB, Business 150 GB) raises it; the person upgrades on webhooks.cc.`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
if (limits && previews.length >= limits.shares) {
|
|
886
|
+
hints.push(
|
|
887
|
+
`All ${limits.shares} of the plan's preview slots are in use: stop one with stop_share (its id is in shares) before ${start}.`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
if (limits && limits.sessionMinutes) {
|
|
891
|
+
hints.push(
|
|
892
|
+
`Previews on this plan stop after ${limits.sessionMinutes} minutes; ${start} again gives a new link.`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (limits && !limits.passwords) {
|
|
896
|
+
hints.push("Passwords and reserved names come with Pro and Business.");
|
|
897
|
+
}
|
|
898
|
+
return hints;
|
|
899
|
+
}
|
|
900
|
+
function toolWords(hint, remote = false) {
|
|
901
|
+
const words = hint.replace(/whk shares stop(?: <[^>]*>)?/g, "stop_share").replace(/whk shares (?:ls|list)/g, "list_shares").replace(/whk shares log(?: <[^>]*>)?/g, "share_requests").replace(/whk names reserve(?: <[^>]*>)?(?: --team <[^>]*>)?/g, "reserve_name").replace(/whk names release(?: <[^>]*>)?/g, "release_name").replace(/whk names password(?: <[^>]*>)? --clear/g, "clear_name_password").replace(/whk names password(?: <[^>]*>)?/g, "set_name_password").replace(/whk names ls/g, "list_names");
|
|
902
|
+
if (remote) return words;
|
|
903
|
+
return words.replace(/whk share <port> --password/g, "share_port with a password").replace(/whk share <port>/g, "share_port");
|
|
904
|
+
}
|
|
905
|
+
function withShareHints(list, remote = false) {
|
|
906
|
+
const server = (list.hints ?? []).map((h) => toolWords(h, remote));
|
|
907
|
+
const ours = shareHints(list, remote);
|
|
908
|
+
if (list.suspended && !server.some((h) => /suspended/i.test(h))) {
|
|
909
|
+
ours.unshift(
|
|
910
|
+
`Sharing is suspended on this account: ${remote ? "new previews are" : "share_port is"} refused. The person can write to support@webhooks.cc.`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
const hints = [.../* @__PURE__ */ new Set([...server, ...ours])];
|
|
914
|
+
return hints.length > 0 ? { ...list, hints } : { ...list };
|
|
915
|
+
}
|
|
839
916
|
function registerTools(target, client, options = {}) {
|
|
840
917
|
const handles = [];
|
|
841
|
-
const
|
|
918
|
+
const remote = options.remote === true;
|
|
919
|
+
const server = toolTarget(target, remote, handles);
|
|
920
|
+
const shareWith = remote ? REMOTE_SHARE : "share_port";
|
|
842
921
|
server.tool(
|
|
843
922
|
"create_endpoint",
|
|
844
923
|
"Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
|
|
@@ -959,6 +1038,18 @@ function registerTools(target, client, options = {}) {
|
|
|
959
1038
|
return jsonContent(endpoint);
|
|
960
1039
|
})
|
|
961
1040
|
);
|
|
1041
|
+
server.tool(
|
|
1042
|
+
"favorite_endpoint",
|
|
1043
|
+
"Mark an endpoint as one of your favourites, or unmark it with favorite: false. A favourite is your own choice, so endpoints shared with you through teams can be marked too; list_endpoints and get_endpoint show it as favorite. Idempotent.",
|
|
1044
|
+
{
|
|
1045
|
+
slug: z.string().describe("The endpoint slug"),
|
|
1046
|
+
favorite: z.boolean().optional().describe("true marks it as a favourite (the default), false unmarks it")
|
|
1047
|
+
},
|
|
1048
|
+
withErrorHandling(async ({ slug, favorite }) => {
|
|
1049
|
+
const result = favorite === false ? await client.endpoints.unfavorite(slug) : await client.endpoints.favorite(slug);
|
|
1050
|
+
return jsonContent({ slug, favorite: result.favorite });
|
|
1051
|
+
})
|
|
1052
|
+
);
|
|
962
1053
|
server.tool(
|
|
963
1054
|
"update_endpoint",
|
|
964
1055
|
"Update an endpoint name, mock response, conditional response rules, signing configuration, whether codes and links are picked out of its emails, or the named fields picked out of them.",
|
|
@@ -1887,6 +1978,143 @@ function registerTools(target, client, options = {}) {
|
|
|
1887
1978
|
);
|
|
1888
1979
|
})
|
|
1889
1980
|
);
|
|
1981
|
+
const started = /* @__PURE__ */ new Map();
|
|
1982
|
+
server.tool(
|
|
1983
|
+
"share_port",
|
|
1984
|
+
"Share a port on this computer at a public link (https://<name>.sharehooks.cc) by running the webhooks.cc CLI (whk share), so a person can open the site you are building. Returns the link. It keeps running until stop_share, the plan's session limit, or the end of this MCP server. Asking again for the same port without a name or password returns the running link; with a name or password, the running share on that port stops and a new one starts. Free accounts get a random name ending in -free, a 2-hour limit and a notice page before the site; Pro and Business can use a reserved name and a password. Needs whk installed on this computer. For a webhook provider that must get your local app's real answer, use `whk tunnel <port> --live` instead.",
|
|
1985
|
+
{
|
|
1986
|
+
port: z.number().int().min(1).max(65535).describe("The local port the site runs on"),
|
|
1987
|
+
name: z.string().optional().describe("A name you reserved (Pro and Business); leave out for a random one"),
|
|
1988
|
+
password: z.string().min(8).max(200).optional().describe("What visitors must type (Pro and Business), 8 to 200 characters")
|
|
1989
|
+
},
|
|
1990
|
+
withErrorHandling(async ({ port, name, password }) => {
|
|
1991
|
+
const running = started.get(port);
|
|
1992
|
+
if (running && !name && !password) {
|
|
1993
|
+
return jsonContent({ ...shareSummary(running), alreadyRunning: true });
|
|
1994
|
+
}
|
|
1995
|
+
if (running) {
|
|
1996
|
+
started.delete(port);
|
|
1997
|
+
await running.stop();
|
|
1998
|
+
}
|
|
1999
|
+
let live;
|
|
2000
|
+
try {
|
|
2001
|
+
live = await client.shares.start({
|
|
2002
|
+
port,
|
|
2003
|
+
...name ? { name } : {},
|
|
2004
|
+
...password ? { password } : {},
|
|
2005
|
+
...process.env.WHK_PATH ? { whkPath: process.env.WHK_PATH } : {}
|
|
2006
|
+
});
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
`${message} With these tools: list_shares shows what runs and the plan's limits, stop_share frees a slot, list_names shows the names you can use.`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
started.set(port, live);
|
|
2014
|
+
void live.closed.then(() => {
|
|
2015
|
+
if (started.get(port) === live) started.delete(port);
|
|
2016
|
+
});
|
|
2017
|
+
const summary = shareSummary(live);
|
|
2018
|
+
try {
|
|
2019
|
+
const list = await client.shares.list({ state: "live" });
|
|
2020
|
+
summary.limits = list.limits;
|
|
2021
|
+
summary.usage = list.usage;
|
|
2022
|
+
const hints = shareHints(list);
|
|
2023
|
+
if (hints.length > 0) summary.hints = hints;
|
|
2024
|
+
} catch {
|
|
2025
|
+
}
|
|
2026
|
+
return jsonContent(summary);
|
|
2027
|
+
})
|
|
2028
|
+
);
|
|
2029
|
+
server.tool(
|
|
2030
|
+
"list_shares",
|
|
2031
|
+
`List your public previews of local ports (whk share${remote ? "" : ", share_port"}) and live tunnels (whk tunnel --live): live ones first, then the latest that ended, with this month's shared data and your plan's sharing limits. Not the same as share_endpoint, which shares an endpoint with a team.`,
|
|
2032
|
+
{
|
|
2033
|
+
state: z.enum(["live", "ended"]).optional().describe("Only the running ones, or only the ended ones")
|
|
2034
|
+
},
|
|
2035
|
+
withErrorHandling(async ({ state }) => {
|
|
2036
|
+
return jsonContent(withShareHints(await client.shares.list(state ? { state } : {}), remote));
|
|
2037
|
+
})
|
|
2038
|
+
);
|
|
2039
|
+
server.tool(
|
|
2040
|
+
"stop_share",
|
|
2041
|
+
"Stop a public preview or live tunnel by its id (from list_shares). The CLI running it is told why and exits.",
|
|
2042
|
+
{
|
|
2043
|
+
id: z.string().describe("The share's id, from list_shares")
|
|
2044
|
+
},
|
|
2045
|
+
withErrorHandling(async ({ id }) => {
|
|
2046
|
+
const result = await client.shares.stop(id);
|
|
2047
|
+
return jsonContent({
|
|
2048
|
+
id,
|
|
2049
|
+
stopped: result.stopped,
|
|
2050
|
+
edgeReached: result.edgeReached,
|
|
2051
|
+
note: result.stopped ? result.edgeReached ? "Stopped; its CLI was told and exits." : "Stopped; the edge could not be told at once, so it ends within half a minute." : "It had already ended."
|
|
2052
|
+
});
|
|
2053
|
+
})
|
|
2054
|
+
);
|
|
2055
|
+
server.tool(
|
|
2056
|
+
"share_requests",
|
|
2057
|
+
"The last requests visitors made to a running preview (from list_shares), newest first: method, path, status, time, size, country, device and a number per visitor, never an address. Use it to see whether the person opened the link and what failed for them. `available` is false when the log could not be read just now.",
|
|
2058
|
+
{
|
|
2059
|
+
id: z.string().describe("The share's id, from list_shares")
|
|
2060
|
+
},
|
|
2061
|
+
withErrorHandling(async ({ id }) => {
|
|
2062
|
+
return jsonContent(await client.shares.requests(id));
|
|
2063
|
+
})
|
|
2064
|
+
);
|
|
2065
|
+
server.tool(
|
|
2066
|
+
"list_names",
|
|
2067
|
+
`List the reserved preview names you can share under (Pro and Business): your own and your teams', each with its link, whether it has a password, whether a preview runs under it now, and whether your plan still covers it (usable). Includes how many your plan allows and how many you hold. Share under one with ${remote ? "`whk share <port> --name <name>` on the person's computer (or the local MCP server's share_port)" : "share_port and its name"}.`,
|
|
2068
|
+
{},
|
|
2069
|
+
withErrorHandling(async () => {
|
|
2070
|
+
return jsonContent(await client.names.list());
|
|
2071
|
+
})
|
|
2072
|
+
);
|
|
2073
|
+
server.tool(
|
|
2074
|
+
"reserve_name",
|
|
2075
|
+
`Reserve a preview name, so ${shareWith} can use the link https://<name>.sharehooks.cc (Pro and Business). Lowercase letters, digits and dashes; names that look like random ones or end in -free are refused. With team, the name belongs to that team and every member may share under it.`,
|
|
2076
|
+
{
|
|
2077
|
+
name: z.string().min(1).max(63).describe("The name: lowercase letters, digits and dashes"),
|
|
2078
|
+
team: z.string().optional().describe("A team's id or name (from list_teams), to keep the name inside that team")
|
|
2079
|
+
},
|
|
2080
|
+
withErrorHandling(async ({ name, team }) => {
|
|
2081
|
+
return jsonContent(await client.names.reserve(name, team ? { team } : {}));
|
|
2082
|
+
})
|
|
2083
|
+
);
|
|
2084
|
+
server.tool(
|
|
2085
|
+
"release_name",
|
|
2086
|
+
"Release a reserved preview name you kept (or, for a team name, as the team's owner). A preview running under it stops, and the link stops working.",
|
|
2087
|
+
{
|
|
2088
|
+
name: z.string().min(1).max(63).describe("The name, from list_names")
|
|
2089
|
+
},
|
|
2090
|
+
withErrorHandling(async ({ name }) => {
|
|
2091
|
+
await client.names.release(name);
|
|
2092
|
+
return jsonContent({ released: name });
|
|
2093
|
+
})
|
|
2094
|
+
);
|
|
2095
|
+
server.tool(
|
|
2096
|
+
"set_name_password",
|
|
2097
|
+
"Put a password on every preview under a reserved name (Pro and Business): visitors must type it before the page opens. The password is not shown back; give it to the person yourself.",
|
|
2098
|
+
{
|
|
2099
|
+
name: z.string().min(1).max(63).describe("The name, from list_names"),
|
|
2100
|
+
password: z.string().min(8).max(200).describe("What visitors must type, 8 to 200 characters")
|
|
2101
|
+
},
|
|
2102
|
+
withErrorHandling(async ({ name, password }) => {
|
|
2103
|
+
await client.names.setPassword(name, password);
|
|
2104
|
+
return jsonContent({ name, password: true });
|
|
2105
|
+
})
|
|
2106
|
+
);
|
|
2107
|
+
server.tool(
|
|
2108
|
+
"clear_name_password",
|
|
2109
|
+
"Take the password off a reserved name, so its previews open without one.",
|
|
2110
|
+
{
|
|
2111
|
+
name: z.string().min(1).max(63).describe("The name, from list_names")
|
|
2112
|
+
},
|
|
2113
|
+
withErrorHandling(async ({ name }) => {
|
|
2114
|
+
await client.names.clearPassword(name);
|
|
2115
|
+
return jsonContent({ name, password: false });
|
|
2116
|
+
})
|
|
2117
|
+
);
|
|
1890
2118
|
server.tool(
|
|
1891
2119
|
"describe",
|
|
1892
2120
|
"Describe all available SDK operations, parameters, and types.",
|
|
@@ -1915,5 +2143,8 @@ export {
|
|
|
1915
2143
|
withErrorHandling,
|
|
1916
2144
|
REMOTE_MAX_WAIT_MS,
|
|
1917
2145
|
REMOTE_MIN_POLL_MS,
|
|
2146
|
+
shareHints,
|
|
2147
|
+
toolWords,
|
|
2148
|
+
withShareHints,
|
|
1918
2149
|
registerTools
|
|
1919
2150
|
};
|