@webhooks-cc/mcp 1.17.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 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 46 tools: `send_to` and `replay_request` are left out and `targetUrl` is refused, because a hosted server cannot reach your machine, and waits are capped at 50 seconds. It needs an account (no sandbox), and serves tools only, without prompts or resources.
37
+ A client that can send headers may use an API key instead: `Authorization: Bearer whcc_...`. The remote server has 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,7 +83,7 @@ Leave out `env` to start without a key. Get your API key at [webhooks.cc/account
83
83
 
84
84
  ## What it exposes
85
85
 
86
- The server exposes `48` tools, `3` prompts, and `3` resource surfaces with an API key, the same `48` tools and the prompts with a saved connection, and `11` sandbox tools otherwise. The remote server exposes `46` tools.
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
 
@@ -155,6 +155,17 @@ retried and logged (owner only):
155
155
 
156
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.
157
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
+
158
169
  ### Discovery and account tools
159
170
 
160
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),
@@ -446,7 +456,7 @@ var import_zod3 = require("zod");
446
456
  var import_sdk2 = require("@webhooks-cc/sdk");
447
457
 
448
458
  // src/version.ts
449
- var VERSION = true ? "1.17.0" : "0.0.0-dev";
459
+ var VERSION = true ? "1.18.0" : "0.0.0-dev";
450
460
  var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
451
461
 
452
462
  // src/tools.ts
@@ -1000,7 +1010,7 @@ function emailDetail(client, email, view, includeHtml) {
1000
1010
  ]);
1001
1011
  return detail;
1002
1012
  }
1003
- 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"]);
1004
1014
  var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
1005
1015
  var REMOTE_MAX_WAIT_MS = 5e4;
1006
1016
  var REMOTE_MIN_POLL_MS = 1e3;
@@ -1058,9 +1068,78 @@ function toolTarget(target, remote, handles) {
1058
1068
  }
1059
1069
  };
1060
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
+ }
1061
1138
  function registerTools(target, client, options = {}) {
1062
1139
  const handles = [];
1063
- const server = toolTarget(target, options.remote === true, handles);
1140
+ const remote = options.remote === true;
1141
+ const server = toolTarget(target, remote, handles);
1142
+ const shareWith = remote ? REMOTE_SHARE : "share_port";
1064
1143
  server.tool(
1065
1144
  "create_endpoint",
1066
1145
  "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
@@ -2121,6 +2200,143 @@ function registerTools(target, client, options = {}) {
2121
2200
  );
2122
2201
  })
2123
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
+ );
2124
2340
  server.tool(
2125
2341
  "describe",
2126
2342
  "Describe all available SDK operations, parameters, and types.",
@@ -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 server = toolTarget(target, options.remote === true, handles);
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.",
@@ -1899,6 +1978,143 @@ function registerTools(target, client, options = {}) {
1899
1978
  );
1900
1979
  })
1901
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
+ );
1902
2118
  server.tool(
1903
2119
  "describe",
1904
2120
  "Describe all available SDK operations, parameters, and types.",
@@ -1927,5 +2143,8 @@ export {
1927
2143
  withErrorHandling,
1928
2144
  REMOTE_MAX_WAIT_MS,
1929
2145
  REMOTE_MIN_POLL_MS,
2146
+ shareHints,
2147
+ toolWords,
2148
+ withShareHints,
1930
2149
  registerTools
1931
2150
  };
package/dist/index.js CHANGED
@@ -182,6 +182,15 @@ function omitHeaders(holder) {
182
182
  holder.headers = {};
183
183
  holder.headersOmitted = `${count} headers left out to fit the output`;
184
184
  }
185
+ function trimAuth(holder) {
186
+ const auth = holder.auth;
187
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
188
+ const verdicts = { ...auth };
189
+ delete verdicts.setup;
190
+ delete verdicts.authenticationResults;
191
+ holder.auth = verdicts;
192
+ }
193
+ }
185
194
  function trimSpam(holder) {
186
195
  const spam = holder.spam;
187
196
  if (spam && typeof spam === "object" && !Array.isArray(spam)) {
@@ -329,6 +338,7 @@ function compactRequest(request) {
329
338
  shrinkToFit(root, budget, [
330
339
  () => trimParsed(compact),
331
340
  () => omitHeaders(compact),
341
+ () => trimAuth(email),
332
342
  () => trimSpam(email),
333
343
  () => capLists(email),
334
344
  () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
@@ -470,7 +480,7 @@ var import_zod3 = require("zod");
470
480
  var import_sdk2 = require("@webhooks-cc/sdk");
471
481
 
472
482
  // src/version.ts
473
- var VERSION = true ? "1.17.0" : "0.0.0-dev";
483
+ var VERSION = true ? "1.18.0" : "0.0.0-dev";
474
484
  var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
475
485
 
476
486
  // src/tools.ts
@@ -1024,7 +1034,7 @@ function emailDetail(client, email, view, includeHtml) {
1024
1034
  ]);
1025
1035
  return detail;
1026
1036
  }
1027
- var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
1037
+ var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request", "share_port"]);
1028
1038
  var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
1029
1039
  var REMOTE_MAX_WAIT_MS = 5e4;
1030
1040
  var REMOTE_MIN_POLL_MS = 1e3;
@@ -1082,9 +1092,78 @@ function toolTarget(target, remote, handles) {
1082
1092
  }
1083
1093
  };
1084
1094
  }
1095
+ function shareSummary(live) {
1096
+ const notes = [];
1097
+ if (live.notice) {
1098
+ notes.push(
1099
+ "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)."
1100
+ );
1101
+ }
1102
+ if (live.password) notes.push("Visitors need the password.");
1103
+ if (live.expiresAt) {
1104
+ notes.push(
1105
+ `The plan stops this share at ${live.expiresAt}; reconnecting does not extend it, and starting it again gives a new link.`
1106
+ );
1107
+ }
1108
+ notes.push("It runs until stop_share or the end of this MCP server.");
1109
+ return {
1110
+ url: live.url,
1111
+ name: live.name,
1112
+ port: live.port,
1113
+ expiresAt: live.expiresAt,
1114
+ notice: live.notice,
1115
+ password: live.password,
1116
+ notes: notes.join(" ")
1117
+ };
1118
+ }
1119
+ 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)";
1120
+ function shareHints(list, remote = false) {
1121
+ const start = remote ? REMOTE_SHARE : "share_port";
1122
+ const hints = [];
1123
+ const limits = list.limits;
1124
+ const usage = list.usage;
1125
+ const previews = list.shares.filter((s) => s.kind !== "live" && !s.endedAt && s.mine !== false);
1126
+ if (usage?.over) {
1127
+ hints.push(
1128
+ `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.`
1129
+ );
1130
+ }
1131
+ if (limits && previews.length >= limits.shares) {
1132
+ hints.push(
1133
+ `All ${limits.shares} of the plan's preview slots are in use: stop one with stop_share (its id is in shares) before ${start}.`
1134
+ );
1135
+ }
1136
+ if (limits && limits.sessionMinutes) {
1137
+ hints.push(
1138
+ `Previews on this plan stop after ${limits.sessionMinutes} minutes; ${start} again gives a new link.`
1139
+ );
1140
+ }
1141
+ if (limits && !limits.passwords) {
1142
+ hints.push("Passwords and reserved names come with Pro and Business.");
1143
+ }
1144
+ return hints;
1145
+ }
1146
+ function toolWords(hint, remote = false) {
1147
+ 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");
1148
+ if (remote) return words;
1149
+ return words.replace(/whk share <port> --password/g, "share_port with a password").replace(/whk share <port>/g, "share_port");
1150
+ }
1151
+ function withShareHints(list, remote = false) {
1152
+ const server = (list.hints ?? []).map((h) => toolWords(h, remote));
1153
+ const ours = shareHints(list, remote);
1154
+ if (list.suspended && !server.some((h) => /suspended/i.test(h))) {
1155
+ ours.unshift(
1156
+ `Sharing is suspended on this account: ${remote ? "new previews are" : "share_port is"} refused. The person can write to support@webhooks.cc.`
1157
+ );
1158
+ }
1159
+ const hints = [.../* @__PURE__ */ new Set([...server, ...ours])];
1160
+ return hints.length > 0 ? { ...list, hints } : { ...list };
1161
+ }
1085
1162
  function registerTools(target, client, options = {}) {
1086
1163
  const handles = [];
1087
- const server = toolTarget(target, options.remote === true, handles);
1164
+ const remote = options.remote === true;
1165
+ const server = toolTarget(target, remote, handles);
1166
+ const shareWith = remote ? REMOTE_SHARE : "share_port";
1088
1167
  server.tool(
1089
1168
  "create_endpoint",
1090
1169
  "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
@@ -2145,6 +2224,143 @@ function registerTools(target, client, options = {}) {
2145
2224
  );
2146
2225
  })
2147
2226
  );
2227
+ const started = /* @__PURE__ */ new Map();
2228
+ server.tool(
2229
+ "share_port",
2230
+ "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.",
2231
+ {
2232
+ port: import_zod2.z.number().int().min(1).max(65535).describe("The local port the site runs on"),
2233
+ name: import_zod2.z.string().optional().describe("A name you reserved (Pro and Business); leave out for a random one"),
2234
+ password: import_zod2.z.string().min(8).max(200).optional().describe("What visitors must type (Pro and Business), 8 to 200 characters")
2235
+ },
2236
+ withErrorHandling(async ({ port, name, password }) => {
2237
+ const running = started.get(port);
2238
+ if (running && !name && !password) {
2239
+ return jsonContent({ ...shareSummary(running), alreadyRunning: true });
2240
+ }
2241
+ if (running) {
2242
+ started.delete(port);
2243
+ await running.stop();
2244
+ }
2245
+ let live;
2246
+ try {
2247
+ live = await client.shares.start({
2248
+ port,
2249
+ ...name ? { name } : {},
2250
+ ...password ? { password } : {},
2251
+ ...process.env.WHK_PATH ? { whkPath: process.env.WHK_PATH } : {}
2252
+ });
2253
+ } catch (error) {
2254
+ const message = error instanceof Error ? error.message : String(error);
2255
+ throw new Error(
2256
+ `${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.`
2257
+ );
2258
+ }
2259
+ started.set(port, live);
2260
+ void live.closed.then(() => {
2261
+ if (started.get(port) === live) started.delete(port);
2262
+ });
2263
+ const summary = shareSummary(live);
2264
+ try {
2265
+ const list = await client.shares.list({ state: "live" });
2266
+ summary.limits = list.limits;
2267
+ summary.usage = list.usage;
2268
+ const hints = shareHints(list);
2269
+ if (hints.length > 0) summary.hints = hints;
2270
+ } catch {
2271
+ }
2272
+ return jsonContent(summary);
2273
+ })
2274
+ );
2275
+ server.tool(
2276
+ "list_shares",
2277
+ `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.`,
2278
+ {
2279
+ state: import_zod2.z.enum(["live", "ended"]).optional().describe("Only the running ones, or only the ended ones")
2280
+ },
2281
+ withErrorHandling(async ({ state }) => {
2282
+ return jsonContent(withShareHints(await client.shares.list(state ? { state } : {}), remote));
2283
+ })
2284
+ );
2285
+ server.tool(
2286
+ "stop_share",
2287
+ "Stop a public preview or live tunnel by its id (from list_shares). The CLI running it is told why and exits.",
2288
+ {
2289
+ id: import_zod2.z.string().describe("The share's id, from list_shares")
2290
+ },
2291
+ withErrorHandling(async ({ id }) => {
2292
+ const result = await client.shares.stop(id);
2293
+ return jsonContent({
2294
+ id,
2295
+ stopped: result.stopped,
2296
+ edgeReached: result.edgeReached,
2297
+ 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."
2298
+ });
2299
+ })
2300
+ );
2301
+ server.tool(
2302
+ "share_requests",
2303
+ "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.",
2304
+ {
2305
+ id: import_zod2.z.string().describe("The share's id, from list_shares")
2306
+ },
2307
+ withErrorHandling(async ({ id }) => {
2308
+ return jsonContent(await client.shares.requests(id));
2309
+ })
2310
+ );
2311
+ server.tool(
2312
+ "list_names",
2313
+ `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"}.`,
2314
+ {},
2315
+ withErrorHandling(async () => {
2316
+ return jsonContent(await client.names.list());
2317
+ })
2318
+ );
2319
+ server.tool(
2320
+ "reserve_name",
2321
+ `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.`,
2322
+ {
2323
+ name: import_zod2.z.string().min(1).max(63).describe("The name: lowercase letters, digits and dashes"),
2324
+ team: import_zod2.z.string().optional().describe("A team's id or name (from list_teams), to keep the name inside that team")
2325
+ },
2326
+ withErrorHandling(async ({ name, team }) => {
2327
+ return jsonContent(await client.names.reserve(name, team ? { team } : {}));
2328
+ })
2329
+ );
2330
+ server.tool(
2331
+ "release_name",
2332
+ "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.",
2333
+ {
2334
+ name: import_zod2.z.string().min(1).max(63).describe("The name, from list_names")
2335
+ },
2336
+ withErrorHandling(async ({ name }) => {
2337
+ await client.names.release(name);
2338
+ return jsonContent({ released: name });
2339
+ })
2340
+ );
2341
+ server.tool(
2342
+ "set_name_password",
2343
+ "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.",
2344
+ {
2345
+ name: import_zod2.z.string().min(1).max(63).describe("The name, from list_names"),
2346
+ password: import_zod2.z.string().min(8).max(200).describe("What visitors must type, 8 to 200 characters")
2347
+ },
2348
+ withErrorHandling(async ({ name, password }) => {
2349
+ await client.names.setPassword(name, password);
2350
+ return jsonContent({ name, password: true });
2351
+ })
2352
+ );
2353
+ server.tool(
2354
+ "clear_name_password",
2355
+ "Take the password off a reserved name, so its previews open without one.",
2356
+ {
2357
+ name: import_zod2.z.string().min(1).max(63).describe("The name, from list_names")
2358
+ },
2359
+ withErrorHandling(async ({ name }) => {
2360
+ await client.names.clearPassword(name);
2361
+ return jsonContent({ name, password: false });
2362
+ })
2363
+ );
2148
2364
  server.tool(
2149
2365
  "describe",
2150
2366
  "Describe all available SDK operations, parameters, and types.",
package/dist/index.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  registerTools,
14
14
  waitForRequestShape,
15
15
  withErrorHandling
16
- } from "./chunk-ZAPNMGCE.mjs";
16
+ } from "./chunk-GTEE7L2Y.mjs";
17
17
 
18
18
  // src/index.ts
19
19
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -262,7 +262,7 @@ import {
262
262
  } from "@webhooks-cc/sdk";
263
263
 
264
264
  // src/version.ts
265
- var VERSION = true ? "1.17.0" : "0.0.0-dev";
265
+ var VERSION = true ? "1.18.0" : "0.0.0-dev";
266
266
  var MCP_USER_AGENT = `webhooks-cc-mcp/${VERSION}`;
267
267
 
268
268
  // src/sandbox-tools.ts
package/dist/tools.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { WebhooksCC, Request, PayloadSchemaFormat } from '@webhooks-cc/sdk';
2
+ import { WebhooksCC, Request, PayloadSchemaFormat, TunnelShareList } from '@webhooks-cc/sdk';
3
3
 
4
4
  declare const createEndpointShape: {
5
5
  name: z.ZodOptional<z.ZodString>;
@@ -166,11 +166,31 @@ interface RegisterToolsOptions {
166
166
  declare const REMOTE_MAX_WAIT_MS = 50000;
167
167
  /** Shortest poll interval on the hosted server: each poll is an API request. */
168
168
  declare const REMOTE_MIN_POLL_MS = 1000;
169
+ /**
170
+ * What an agent can do about the account's sharing limits, from a share
171
+ * list: the next step when slots or data run out, so it never has to send
172
+ * the person to the dashboard. `remote` for the hosted server, which has no
173
+ * share_port.
174
+ */
175
+ declare function shareHints(list: TunnelShareList, remote?: boolean): string[];
176
+ /**
177
+ * A server hint in this server's words: the whk commands it names become
178
+ * the tools that do the same, so an agent does not reach for a CLI. On the
179
+ * hosted server (`remote`) `whk share` stays a command for the person's
180
+ * computer, since that server has no share_port.
181
+ */
182
+ declare function toolWords(hint: string, remote?: boolean): string;
183
+ /**
184
+ * A share list with hints for an agent, for list_shares and share_port: the
185
+ * server's own (in tool words) and this server's, without repeats, so
186
+ * nothing the server says (a suspension, a pause) is lost.
187
+ */
188
+ declare function withShareHints(list: TunnelShareList, remote?: boolean): Record<string, unknown>;
169
189
  /**
170
190
  * Registers the webhooks.cc tools for an account client. Returns their
171
191
  * handles, so a caller can swap the tool set later.
172
192
  */
173
193
  declare function registerTools(target: ToolServer, client: WebhooksCC, options?: RegisterToolsOptions): RegisteredToolHandle[];
174
194
 
175
- export { REMOTE_MAX_WAIT_MS, REMOTE_MIN_POLL_MS, createEndpointShape, deleteEndpointShape, getEndpointShape, getRequestShape, jsonContent, listEndpointsShape, listRequestsShape, payloadSchemaResult, payloadSchemaShape, registerTools, textContent, waitForRequestShape, withErrorHandling };
195
+ export { REMOTE_MAX_WAIT_MS, REMOTE_MIN_POLL_MS, createEndpointShape, deleteEndpointShape, getEndpointShape, getRequestShape, jsonContent, listEndpointsShape, listRequestsShape, payloadSchemaResult, payloadSchemaShape, registerTools, shareHints, textContent, toolWords, waitForRequestShape, withErrorHandling, withShareHints };
176
196
  export type { RegisterToolsOptions, RegisteredToolHandle, ToolResult, ToolServer };
package/dist/tools.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { WebhooksCC, Request, PayloadSchemaFormat } from '@webhooks-cc/sdk';
2
+ import { WebhooksCC, Request, PayloadSchemaFormat, TunnelShareList } from '@webhooks-cc/sdk';
3
3
 
4
4
  declare const createEndpointShape: {
5
5
  name: z.ZodOptional<z.ZodString>;
@@ -166,11 +166,31 @@ interface RegisterToolsOptions {
166
166
  declare const REMOTE_MAX_WAIT_MS = 50000;
167
167
  /** Shortest poll interval on the hosted server: each poll is an API request. */
168
168
  declare const REMOTE_MIN_POLL_MS = 1000;
169
+ /**
170
+ * What an agent can do about the account's sharing limits, from a share
171
+ * list: the next step when slots or data run out, so it never has to send
172
+ * the person to the dashboard. `remote` for the hosted server, which has no
173
+ * share_port.
174
+ */
175
+ declare function shareHints(list: TunnelShareList, remote?: boolean): string[];
176
+ /**
177
+ * A server hint in this server's words: the whk commands it names become
178
+ * the tools that do the same, so an agent does not reach for a CLI. On the
179
+ * hosted server (`remote`) `whk share` stays a command for the person's
180
+ * computer, since that server has no share_port.
181
+ */
182
+ declare function toolWords(hint: string, remote?: boolean): string;
183
+ /**
184
+ * A share list with hints for an agent, for list_shares and share_port: the
185
+ * server's own (in tool words) and this server's, without repeats, so
186
+ * nothing the server says (a suspension, a pause) is lost.
187
+ */
188
+ declare function withShareHints(list: TunnelShareList, remote?: boolean): Record<string, unknown>;
169
189
  /**
170
190
  * Registers the webhooks.cc tools for an account client. Returns their
171
191
  * handles, so a caller can swap the tool set later.
172
192
  */
173
193
  declare function registerTools(target: ToolServer, client: WebhooksCC, options?: RegisterToolsOptions): RegisteredToolHandle[];
174
194
 
175
- export { REMOTE_MAX_WAIT_MS, REMOTE_MIN_POLL_MS, createEndpointShape, deleteEndpointShape, getEndpointShape, getRequestShape, jsonContent, listEndpointsShape, listRequestsShape, payloadSchemaResult, payloadSchemaShape, registerTools, textContent, waitForRequestShape, withErrorHandling };
195
+ export { REMOTE_MAX_WAIT_MS, REMOTE_MIN_POLL_MS, createEndpointShape, deleteEndpointShape, getEndpointShape, getRequestShape, jsonContent, listEndpointsShape, listRequestsShape, payloadSchemaResult, payloadSchemaShape, registerTools, shareHints, textContent, toolWords, waitForRequestShape, withErrorHandling, withShareHints };
176
196
  export type { RegisterToolsOptions, RegisteredToolHandle, ToolResult, ToolServer };
package/dist/tools.js CHANGED
@@ -32,9 +32,12 @@ __export(tools_exports, {
32
32
  payloadSchemaResult: () => payloadSchemaResult,
33
33
  payloadSchemaShape: () => payloadSchemaShape,
34
34
  registerTools: () => registerTools,
35
+ shareHints: () => shareHints,
35
36
  textContent: () => textContent,
37
+ toolWords: () => toolWords,
36
38
  waitForRequestShape: () => waitForRequestShape,
37
- withErrorHandling: () => withErrorHandling
39
+ withErrorHandling: () => withErrorHandling,
40
+ withShareHints: () => withShareHints
38
41
  });
39
42
  module.exports = __toCommonJS(tools_exports);
40
43
  var import_zod = require("zod");
@@ -76,6 +79,15 @@ function omitHeaders(holder) {
76
79
  holder.headers = {};
77
80
  holder.headersOmitted = `${count} headers left out to fit the output`;
78
81
  }
82
+ function trimAuth(holder) {
83
+ const auth = holder.auth;
84
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
85
+ const verdicts = { ...auth };
86
+ delete verdicts.setup;
87
+ delete verdicts.authenticationResults;
88
+ holder.auth = verdicts;
89
+ }
90
+ }
79
91
  function trimSpam(holder) {
80
92
  const spam = holder.spam;
81
93
  if (spam && typeof spam === "object" && !Array.isArray(spam)) {
@@ -223,6 +235,7 @@ function compactRequest(request) {
223
235
  shrinkToFit(root, budget, [
224
236
  () => trimParsed(compact),
225
237
  () => omitHeaders(compact),
238
+ () => trimAuth(email),
226
239
  () => trimSpam(email),
227
240
  () => capLists(email),
228
241
  () => cutStringToFit(root, email, "text", "textTruncated", budget, TEXT_FLOOR),
@@ -787,7 +800,7 @@ function emailDetail(client, email, view, includeHtml) {
787
800
  ]);
788
801
  return detail;
789
802
  }
790
- var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
803
+ var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request", "share_port"]);
791
804
  var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
792
805
  var REMOTE_MAX_WAIT_MS = 5e4;
793
806
  var REMOTE_MIN_POLL_MS = 1e3;
@@ -845,9 +858,78 @@ function toolTarget(target, remote, handles) {
845
858
  }
846
859
  };
847
860
  }
861
+ function shareSummary(live) {
862
+ const notes = [];
863
+ if (live.notice) {
864
+ notes.push(
865
+ "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)."
866
+ );
867
+ }
868
+ if (live.password) notes.push("Visitors need the password.");
869
+ if (live.expiresAt) {
870
+ notes.push(
871
+ `The plan stops this share at ${live.expiresAt}; reconnecting does not extend it, and starting it again gives a new link.`
872
+ );
873
+ }
874
+ notes.push("It runs until stop_share or the end of this MCP server.");
875
+ return {
876
+ url: live.url,
877
+ name: live.name,
878
+ port: live.port,
879
+ expiresAt: live.expiresAt,
880
+ notice: live.notice,
881
+ password: live.password,
882
+ notes: notes.join(" ")
883
+ };
884
+ }
885
+ 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)";
886
+ function shareHints(list, remote = false) {
887
+ const start = remote ? REMOTE_SHARE : "share_port";
888
+ const hints = [];
889
+ const limits = list.limits;
890
+ const usage = list.usage;
891
+ const previews = list.shares.filter((s) => s.kind !== "live" && !s.endedAt && s.mine !== false);
892
+ if (usage?.over) {
893
+ hints.push(
894
+ `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.`
895
+ );
896
+ }
897
+ if (limits && previews.length >= limits.shares) {
898
+ hints.push(
899
+ `All ${limits.shares} of the plan's preview slots are in use: stop one with stop_share (its id is in shares) before ${start}.`
900
+ );
901
+ }
902
+ if (limits && limits.sessionMinutes) {
903
+ hints.push(
904
+ `Previews on this plan stop after ${limits.sessionMinutes} minutes; ${start} again gives a new link.`
905
+ );
906
+ }
907
+ if (limits && !limits.passwords) {
908
+ hints.push("Passwords and reserved names come with Pro and Business.");
909
+ }
910
+ return hints;
911
+ }
912
+ function toolWords(hint, remote = false) {
913
+ 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");
914
+ if (remote) return words;
915
+ return words.replace(/whk share <port> --password/g, "share_port with a password").replace(/whk share <port>/g, "share_port");
916
+ }
917
+ function withShareHints(list, remote = false) {
918
+ const server = (list.hints ?? []).map((h) => toolWords(h, remote));
919
+ const ours = shareHints(list, remote);
920
+ if (list.suspended && !server.some((h) => /suspended/i.test(h))) {
921
+ ours.unshift(
922
+ `Sharing is suspended on this account: ${remote ? "new previews are" : "share_port is"} refused. The person can write to support@webhooks.cc.`
923
+ );
924
+ }
925
+ const hints = [.../* @__PURE__ */ new Set([...server, ...ours])];
926
+ return hints.length > 0 ? { ...list, hints } : { ...list };
927
+ }
848
928
  function registerTools(target, client, options = {}) {
849
929
  const handles = [];
850
- const server = toolTarget(target, options.remote === true, handles);
930
+ const remote = options.remote === true;
931
+ const server = toolTarget(target, remote, handles);
932
+ const shareWith = remote ? REMOTE_SHARE : "share_port";
851
933
  server.tool(
852
934
  "create_endpoint",
853
935
  "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
@@ -1908,6 +1990,143 @@ function registerTools(target, client, options = {}) {
1908
1990
  );
1909
1991
  })
1910
1992
  );
1993
+ const started = /* @__PURE__ */ new Map();
1994
+ server.tool(
1995
+ "share_port",
1996
+ "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.",
1997
+ {
1998
+ port: import_zod.z.number().int().min(1).max(65535).describe("The local port the site runs on"),
1999
+ name: import_zod.z.string().optional().describe("A name you reserved (Pro and Business); leave out for a random one"),
2000
+ password: import_zod.z.string().min(8).max(200).optional().describe("What visitors must type (Pro and Business), 8 to 200 characters")
2001
+ },
2002
+ withErrorHandling(async ({ port, name, password }) => {
2003
+ const running = started.get(port);
2004
+ if (running && !name && !password) {
2005
+ return jsonContent({ ...shareSummary(running), alreadyRunning: true });
2006
+ }
2007
+ if (running) {
2008
+ started.delete(port);
2009
+ await running.stop();
2010
+ }
2011
+ let live;
2012
+ try {
2013
+ live = await client.shares.start({
2014
+ port,
2015
+ ...name ? { name } : {},
2016
+ ...password ? { password } : {},
2017
+ ...process.env.WHK_PATH ? { whkPath: process.env.WHK_PATH } : {}
2018
+ });
2019
+ } catch (error) {
2020
+ const message = error instanceof Error ? error.message : String(error);
2021
+ throw new Error(
2022
+ `${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.`
2023
+ );
2024
+ }
2025
+ started.set(port, live);
2026
+ void live.closed.then(() => {
2027
+ if (started.get(port) === live) started.delete(port);
2028
+ });
2029
+ const summary = shareSummary(live);
2030
+ try {
2031
+ const list = await client.shares.list({ state: "live" });
2032
+ summary.limits = list.limits;
2033
+ summary.usage = list.usage;
2034
+ const hints = shareHints(list);
2035
+ if (hints.length > 0) summary.hints = hints;
2036
+ } catch {
2037
+ }
2038
+ return jsonContent(summary);
2039
+ })
2040
+ );
2041
+ server.tool(
2042
+ "list_shares",
2043
+ `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.`,
2044
+ {
2045
+ state: import_zod.z.enum(["live", "ended"]).optional().describe("Only the running ones, or only the ended ones")
2046
+ },
2047
+ withErrorHandling(async ({ state }) => {
2048
+ return jsonContent(withShareHints(await client.shares.list(state ? { state } : {}), remote));
2049
+ })
2050
+ );
2051
+ server.tool(
2052
+ "stop_share",
2053
+ "Stop a public preview or live tunnel by its id (from list_shares). The CLI running it is told why and exits.",
2054
+ {
2055
+ id: import_zod.z.string().describe("The share's id, from list_shares")
2056
+ },
2057
+ withErrorHandling(async ({ id }) => {
2058
+ const result = await client.shares.stop(id);
2059
+ return jsonContent({
2060
+ id,
2061
+ stopped: result.stopped,
2062
+ edgeReached: result.edgeReached,
2063
+ 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."
2064
+ });
2065
+ })
2066
+ );
2067
+ server.tool(
2068
+ "share_requests",
2069
+ "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.",
2070
+ {
2071
+ id: import_zod.z.string().describe("The share's id, from list_shares")
2072
+ },
2073
+ withErrorHandling(async ({ id }) => {
2074
+ return jsonContent(await client.shares.requests(id));
2075
+ })
2076
+ );
2077
+ server.tool(
2078
+ "list_names",
2079
+ `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"}.`,
2080
+ {},
2081
+ withErrorHandling(async () => {
2082
+ return jsonContent(await client.names.list());
2083
+ })
2084
+ );
2085
+ server.tool(
2086
+ "reserve_name",
2087
+ `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.`,
2088
+ {
2089
+ name: import_zod.z.string().min(1).max(63).describe("The name: lowercase letters, digits and dashes"),
2090
+ team: import_zod.z.string().optional().describe("A team's id or name (from list_teams), to keep the name inside that team")
2091
+ },
2092
+ withErrorHandling(async ({ name, team }) => {
2093
+ return jsonContent(await client.names.reserve(name, team ? { team } : {}));
2094
+ })
2095
+ );
2096
+ server.tool(
2097
+ "release_name",
2098
+ "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.",
2099
+ {
2100
+ name: import_zod.z.string().min(1).max(63).describe("The name, from list_names")
2101
+ },
2102
+ withErrorHandling(async ({ name }) => {
2103
+ await client.names.release(name);
2104
+ return jsonContent({ released: name });
2105
+ })
2106
+ );
2107
+ server.tool(
2108
+ "set_name_password",
2109
+ "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.",
2110
+ {
2111
+ name: import_zod.z.string().min(1).max(63).describe("The name, from list_names"),
2112
+ password: import_zod.z.string().min(8).max(200).describe("What visitors must type, 8 to 200 characters")
2113
+ },
2114
+ withErrorHandling(async ({ name, password }) => {
2115
+ await client.names.setPassword(name, password);
2116
+ return jsonContent({ name, password: true });
2117
+ })
2118
+ );
2119
+ server.tool(
2120
+ "clear_name_password",
2121
+ "Take the password off a reserved name, so its previews open without one.",
2122
+ {
2123
+ name: import_zod.z.string().min(1).max(63).describe("The name, from list_names")
2124
+ },
2125
+ withErrorHandling(async ({ name }) => {
2126
+ await client.names.clearPassword(name);
2127
+ return jsonContent({ name, password: false });
2128
+ })
2129
+ );
1911
2130
  server.tool(
1912
2131
  "describe",
1913
2132
  "Describe all available SDK operations, parameters, and types.",
@@ -1933,7 +2152,10 @@ function registerTools(target, client, options = {}) {
1933
2152
  payloadSchemaResult,
1934
2153
  payloadSchemaShape,
1935
2154
  registerTools,
2155
+ shareHints,
1936
2156
  textContent,
2157
+ toolWords,
1937
2158
  waitForRequestShape,
1938
- withErrorHandling
2159
+ withErrorHandling,
2160
+ withShareHints
1939
2161
  });
package/dist/tools.mjs CHANGED
@@ -11,10 +11,13 @@ import {
11
11
  payloadSchemaResult,
12
12
  payloadSchemaShape,
13
13
  registerTools,
14
+ shareHints,
14
15
  textContent,
16
+ toolWords,
15
17
  waitForRequestShape,
16
- withErrorHandling
17
- } from "./chunk-ZAPNMGCE.mjs";
18
+ withErrorHandling,
19
+ withShareHints
20
+ } from "./chunk-GTEE7L2Y.mjs";
18
21
  export {
19
22
  REMOTE_MAX_WAIT_MS,
20
23
  REMOTE_MIN_POLL_MS,
@@ -28,7 +31,10 @@ export {
28
31
  payloadSchemaResult,
29
32
  payloadSchemaShape,
30
33
  registerTools,
34
+ shareHints,
31
35
  textContent,
36
+ toolWords,
32
37
  waitForRequestShape,
33
- withErrorHandling
38
+ withErrorHandling,
39
+ withShareHints
34
40
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webhooks-cc/mcp",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "mcpName": "io.github.kroqdotdev/webhooks-cc",
5
5
  "description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
6
6
  "main": "dist/index.js",
@@ -42,15 +42,15 @@
42
42
  },
43
43
  "homepage": "https://webhooks.cc",
44
44
  "dependencies": {
45
- "@modelcontextprotocol/sdk": "^1.32.1",
45
+ "@modelcontextprotocol/sdk": "^1.32.2",
46
46
  "zod": "^4.6.7",
47
- "@webhooks-cc/sdk": "1.21.0"
47
+ "@webhooks-cc/sdk": "1.22.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@types/node": "^26.6.5",
50
+ "@types/node": "^26.6.6",
51
51
  "tsup": "^9.0.0",
52
52
  "typescript": "^6.0.3",
53
- "vitest": "^5.0.5"
53
+ "vitest": "^5.1.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup",