@webhooks-cc/mcp 1.14.0 → 1.15.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/dist/bin/mcp.js CHANGED
@@ -754,7 +754,9 @@ async function waitForMultipleRequests(client, endpointSlug, options) {
754
754
  };
755
755
  }
756
756
  }
757
- await sleep(Math.max(10, pollIntervalMs));
757
+ await sleep(
758
+ Math.min(Math.max(10, pollIntervalMs), Math.max(0, timeoutMs - (Date.now() - startedAt)))
759
+ );
758
760
  }
759
761
  return {
760
762
  requests,
@@ -912,7 +914,67 @@ function emailDetail(client, email, view, includeHtml) {
912
914
  ]);
913
915
  return detail;
914
916
  }
915
- function registerTools(server, client) {
917
+ var LOCAL_ONLY_TOOLS = /* @__PURE__ */ new Set(["send_to", "replay_request"]);
918
+ var TARGET_URL_TOOLS = /* @__PURE__ */ new Set(["render_email", "test_webhook_flow"]);
919
+ var REMOTE_MAX_WAIT_MS = 5e4;
920
+ var REMOTE_MIN_POLL_MS = 1e3;
921
+ function durationMs(value) {
922
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
923
+ if (typeof value !== "string") return null;
924
+ try {
925
+ const ms = (0, import_sdk.parseDuration)(value);
926
+ return Number.isFinite(ms) && ms >= 0 ? ms : null;
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ function toolTarget(target, remote, handles) {
932
+ return {
933
+ tool(name, description, shape, handler) {
934
+ if (remote && LOCAL_ONLY_TOOLS.has(name)) return { remove() {
935
+ } };
936
+ const wrapped = remote ? async (args2) => {
937
+ const input = args2;
938
+ if (TARGET_URL_TOOLS.has(name) && input.targetUrl !== void 0) {
939
+ return {
940
+ ...textContent(
941
+ "targetUrl needs the local MCP server (npx -y @webhooks-cc/mcp): the hosted server cannot reach your machine. Leave it out here."
942
+ ),
943
+ isError: true
944
+ };
945
+ }
946
+ const bounded = { ...input };
947
+ if ("pollInterval" in shape && input.pollInterval === void 0) {
948
+ bounded.pollInterval = REMOTE_MIN_POLL_MS;
949
+ }
950
+ for (const [key, bound] of [
951
+ ["timeout", (ms) => Math.min(ms, REMOTE_MAX_WAIT_MS)],
952
+ [
953
+ "pollInterval",
954
+ (ms) => Math.min(Math.max(ms, REMOTE_MIN_POLL_MS), REMOTE_MAX_WAIT_MS)
955
+ ]
956
+ ]) {
957
+ if (bounded[key] === void 0) continue;
958
+ const ms = durationMs(bounded[key]);
959
+ if (ms === null) {
960
+ return {
961
+ ...textContent(`${key} must be a duration such as "30s" or milliseconds.`),
962
+ isError: true
963
+ };
964
+ }
965
+ bounded[key] = bound(ms);
966
+ }
967
+ return handler(bounded);
968
+ } : handler;
969
+ const handle = target.tool(name, description, shape, wrapped);
970
+ handles.push(handle);
971
+ return handle;
972
+ }
973
+ };
974
+ }
975
+ function registerTools(target, client, options = {}) {
976
+ const handles = [];
977
+ const server = toolTarget(target, options.remote === true, handles);
916
978
  server.tool(
917
979
  "create_endpoint",
918
980
  "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
@@ -1594,7 +1656,7 @@ function registerTools(server, client) {
1594
1656
  verifySignature: shouldVerify,
1595
1657
  cleanup
1596
1658
  }) => {
1597
- const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s" });
1659
+ const flow = client.flow().createEndpoint({ expiresIn: "1h" }).waitForCapture({ timeout: "30s", pollInterval: "1s" });
1598
1660
  if (mockStatus !== void 0) {
1599
1661
  flow.setMock({
1600
1662
  status: mockStatus,
@@ -1719,10 +1781,14 @@ function registerTools(server, client) {
1719
1781
  includeHtml: import_zod2.z.boolean().default(false).describe("Include the HTML part (default: false)")
1720
1782
  },
1721
1783
  withErrorHandling(async ({ endpointSlug, tag, subject, from, timeout, since, includeHtml }) => {
1722
- const [email, view] = await Promise.all([
1723
- client.emails.waitFor(endpointSlug, { tag, subject, from, timeout, since }),
1724
- emailEndpointView(client, endpointSlug)
1725
- ]);
1784
+ const view = await emailEndpointView(client, endpointSlug);
1785
+ const email = await client.emails.waitFor(endpointSlug, {
1786
+ tag,
1787
+ subject,
1788
+ from,
1789
+ timeout,
1790
+ since
1791
+ });
1726
1792
  return jsonContent(emailDetail(client, email, view, includeHtml));
1727
1793
  })
1728
1794
  );
@@ -1835,8 +1901,8 @@ function registerTools(server, client) {
1835
1901
  "JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
1836
1902
  )
1837
1903
  },
1838
- withErrorHandling(async ({ slug, ...options }) => {
1839
- const endpoint = await client.forwarding.configure(slug, options);
1904
+ withErrorHandling(async ({ slug, ...options2 }) => {
1905
+ const endpoint = await client.forwarding.configure(slug, options2);
1840
1906
  return jsonContent({
1841
1907
  slug: endpoint.slug,
1842
1908
  forwardEnabled: endpoint.forwardEnabled ?? false,
@@ -1933,9 +1999,9 @@ function registerTools(server, client) {
1933
1999
  if (requestId === void 0 === (endpointSlug === void 0)) {
1934
2000
  throw new Error("Pass exactly one of requestId or endpointSlug");
1935
2001
  }
1936
- const options = destinationId ? { destinationId } : {};
2002
+ const options2 = destinationId ? { destinationId } : {};
1937
2003
  return jsonContent(
1938
- requestId ? await client.forwarding.redeliver(requestId, options) : await client.forwarding.redeliverFailed(endpointSlug, options)
2004
+ requestId ? await client.forwarding.redeliver(requestId, options2) : await client.forwarding.redeliverFailed(endpointSlug, options2)
1939
2005
  );
1940
2006
  })
1941
2007
  );
@@ -1948,6 +2014,7 @@ function registerTools(server, client) {
1948
2014
  return jsonContent(description);
1949
2015
  })
1950
2016
  );
2017
+ return handles;
1951
2018
  }
1952
2019
 
1953
2020
  // src/sandbox-tools.ts
@@ -1959,7 +2026,6 @@ var SANDBOX = {
1959
2026
  };
1960
2027
  var DEFAULT_WAIT_SECONDS = 45;
1961
2028
  var MAX_WAIT_SECONDS = 600;
1962
- var TOKEN_REFRESH_MARGIN_MS = 6e4;
1963
2029
  var DOCS_URL = "https://webhooks.cc/docs/agents";
1964
2030
  var ACCOUNT_ONLY_OPTIONS = [
1965
2031
  "mockResponse",
@@ -1970,20 +2036,29 @@ var ACCOUNT_ONLY_OPTIONS = [
1970
2036
  "signingHeader"
1971
2037
  ];
1972
2038
  var AgentSession = class {
1973
- constructor(baseUrl, webhookUrl, clientName) {
2039
+ constructor(baseUrl, webhookUrl, clientName, credentialsFile) {
1974
2040
  this.baseUrl = baseUrl;
1975
2041
  this.webhookUrl = webhookUrl;
1976
2042
  this.clientName = clientName;
2043
+ this.credentialsFile = credentialsFile;
1977
2044
  this.sandbox = null;
1978
- this.claim = null;
1979
- this.connected = false;
2045
+ this.connect = null;
2046
+ }
2047
+ sandboxOptions() {
2048
+ return {
2049
+ baseUrl: this.baseUrl,
2050
+ webhookUrl: this.webhookUrl,
2051
+ clientName: this.clientName,
2052
+ credentialsFile: this.credentialsFile
2053
+ };
1980
2054
  }
1981
2055
  /**
1982
- * The sandbox, registered on first use and again after it expired. Tool
1983
- * calls can run in parallel: this.sandbox is read again after every await,
1984
- * and a new sandbox is assigned with no await in between, so parallel
1985
- * calls share one registration. Calls waiting on a registration that fails
1986
- * get its error (a 429 asks them to wait); the next call starts again.
2056
+ * The sandbox: the one saved by an earlier session when it is still live,
2057
+ * else a new registration (and again after it expired). Tool calls can run
2058
+ * in parallel: this.sandbox is read again after every await, and a new
2059
+ * sandbox is assigned with no await in between, so parallel calls share
2060
+ * one registration. Calls waiting on a registration that fails get its
2061
+ * error (a 429 asks them to wait); the next call starts again.
1987
2062
  */
1988
2063
  async sandboxClient() {
1989
2064
  while (this.sandbox) {
@@ -1991,35 +2066,36 @@ var AgentSession = class {
1991
2066
  const sandbox = await pending;
1992
2067
  if (!this.expired(pending, sandbox)) return sandbox;
1993
2068
  }
1994
- const created = import_sdk2.WebhooksCC.sandbox({
1995
- baseUrl: this.baseUrl,
1996
- webhookUrl: this.webhookUrl,
1997
- clientName: this.clientName
1998
- });
2069
+ const created = import_sdk2.WebhooksCC.sandbox(this.sandboxOptions());
1999
2070
  this.sandbox = created;
2000
2071
  created.catch(() => {
2001
2072
  if (this.sandbox === created) this.sandbox = null;
2002
2073
  });
2003
2074
  return created;
2004
2075
  }
2005
- /** The live sandbox, if one was registered; never registers one. */
2076
+ /** The live sandbox, from this session or the credential file; never registers one. */
2006
2077
  async existingSandbox() {
2007
2078
  const pending = this.sandbox;
2008
- if (!pending) return null;
2009
- let sandbox;
2010
- try {
2011
- sandbox = await pending;
2012
- } catch {
2013
- return null;
2079
+ if (pending) {
2080
+ let sandbox;
2081
+ try {
2082
+ sandbox = await pending;
2083
+ } catch {
2084
+ return null;
2085
+ }
2086
+ return this.expired(pending, sandbox) ? null : sandbox;
2014
2087
  }
2015
- return this.expired(pending, sandbox) ? null : sandbox;
2088
+ const resumed = await import_sdk2.SandboxClient.resume(this.sandboxOptions()).catch(() => null);
2089
+ if (!resumed) return null;
2090
+ if (!this.sandbox) this.sandbox = Promise.resolve(resumed);
2091
+ return this.sandbox;
2016
2092
  }
2017
2093
  /** True when the sandbox is past its 24 hours; then it is dropped. */
2018
2094
  expired(pending, sandbox) {
2019
2095
  if (sandbox.expiresAt.getTime() > Date.now()) return false;
2020
2096
  if (this.sandbox === pending) this.sandbox = null;
2021
- if (this.claim?.kind === "sandbox" && this.claim.claimToken === sandbox.registration.claimToken) {
2022
- this.claim = null;
2097
+ if (this.connect?.via === "sandbox" && this.connect.sandbox === sandbox) {
2098
+ this.connect = null;
2023
2099
  }
2024
2100
  return true;
2025
2101
  }
@@ -2030,26 +2106,11 @@ var AgentSession = class {
2030
2106
  }
2031
2107
  return sandbox;
2032
2108
  }
2033
- forgetSandbox() {
2109
+ reset() {
2034
2110
  this.sandbox = null;
2111
+ this.connect = null;
2035
2112
  }
2036
2113
  };
2037
- function claimedTokens(baseUrl, assertion, first) {
2038
- let current = first;
2039
- let pending = null;
2040
- return async (options) => {
2041
- if (!options?.forceRefresh && current.expiresAt.getTime() - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
2042
- return current.accessToken;
2043
- }
2044
- if (!pending) {
2045
- pending = import_sdk2.WebhooksCC.agent.exchange({ baseUrl, assertion }).finally(() => {
2046
- pending = null;
2047
- });
2048
- }
2049
- current = await pending;
2050
- return current.accessToken;
2051
- };
2052
- }
2053
2114
  function minutesLeft(until) {
2054
2115
  return Math.max(1, Math.round((until.getTime() - Date.now()) / 6e4));
2055
2116
  }
@@ -2057,16 +2118,53 @@ function onlyKind(requests, kind) {
2057
2118
  if (!kind) return requests;
2058
2119
  return requests.filter((request) => (request.kind ?? "http") === kind);
2059
2120
  }
2121
+ function savedTo(credentialsFile) {
2122
+ return import_sdk2.AgentCredentialStore.resolve(credentialsFile)?.path ?? null;
2123
+ }
2060
2124
  function registerSandboxTools(server, options = {}) {
2061
2125
  const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL ?? "https://webhooks.cc";
2126
+ const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
2062
2127
  const session = new AgentSession(
2063
2128
  baseUrl,
2064
- options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
2065
- options.clientName ?? "webhooks-cc-mcp"
2066
- );
2067
- const registered = [];
2129
+ webhookUrl,
2130
+ options.clientName ?? "webhooks-cc-mcp",
2131
+ options.credentialsFile
2132
+ );
2133
+ let handles = [];
2134
+ let shown = 0;
2135
+ const swap = (register) => {
2136
+ const id = ++shown;
2137
+ for (const handle of handles) handle.remove();
2138
+ handles = register(id);
2139
+ };
2140
+ const showSandbox = () => swap(() => sandboxToolSet(server, session, showAccount));
2141
+ const savedAccount = (id) => options.credentialsFile === false ? null : import_sdk2.WebhooksCC.fromSavedConnection({
2142
+ baseUrl,
2143
+ webhookUrl,
2144
+ credentialsFile: options.credentialsFile,
2145
+ // The human disconnected the agent: back to the sandbox, unless a
2146
+ // newer connection is already on display.
2147
+ onRevoked: () => queueMicrotask(() => {
2148
+ if (shown === id) showSandbox();
2149
+ })
2150
+ });
2151
+ function showAccount(client) {
2152
+ swap((id) => registerTools(server, savedAccount(id) ?? client));
2153
+ options.onConnected?.(client);
2154
+ }
2155
+ const saved = savedAccount(1);
2156
+ if (saved) {
2157
+ swap(() => registerTools(server, saved));
2158
+ options.onConnected?.(saved);
2159
+ } else {
2160
+ showSandbox();
2161
+ }
2162
+ }
2163
+ function sandboxToolSet(server, session, showAccount) {
2164
+ const tools = [];
2068
2165
  const limits = `up to ${SANDBOX.maxEndpoints} endpoints at a time, ${SANDBOX.requestsPerEndpoint} captured requests each and ${SANDBOX.requests} in all, deleted ${SANDBOX.lifetimeHours} hours after the sandbox was created`;
2069
- registered.push(
2166
+ const file = savedTo(session.credentialsFile);
2167
+ tools.push(
2070
2168
  server.tool(
2071
2169
  "create_endpoint",
2072
2170
  `Create a temporary webhook capture endpoint in the agent sandbox (no account needed): ${limits}. The first call registers the sandbox, which takes a few seconds of proof of work. Mock responses, notifications and signature verification need an account: see connect_account.`,
@@ -2090,7 +2188,7 @@ function registerSandboxTools(server, options = {}) {
2090
2188
  })
2091
2189
  )
2092
2190
  );
2093
- registered.push(
2191
+ tools.push(
2094
2192
  server.tool(
2095
2193
  "list_endpoints",
2096
2194
  "List this agent's sandbox endpoints. Empty until create_endpoint starts the sandbox.",
@@ -2104,7 +2202,7 @@ function registerSandboxTools(server, options = {}) {
2104
2202
  })
2105
2203
  )
2106
2204
  );
2107
- registered.push(
2205
+ tools.push(
2108
2206
  server.tool(
2109
2207
  "get_endpoint",
2110
2208
  "Get one of this agent's sandbox endpoints by slug, with its sandbox limits and usage.",
@@ -2114,7 +2212,7 @@ function registerSandboxTools(server, options = {}) {
2114
2212
  })
2115
2213
  )
2116
2214
  );
2117
- registered.push(
2215
+ tools.push(
2118
2216
  server.tool(
2119
2217
  "delete_endpoint",
2120
2218
  "Delete one of this agent's sandbox endpoints and its captured requests. Frees a slot; the sandbox's request budget is not refunded.",
@@ -2125,7 +2223,7 @@ function registerSandboxTools(server, options = {}) {
2125
2223
  })
2126
2224
  )
2127
2225
  );
2128
- registered.push(
2226
+ tools.push(
2129
2227
  server.tool(
2130
2228
  "list_requests",
2131
2229
  "List recent requests captured by a sandbox endpoint, newest first.",
@@ -2137,7 +2235,7 @@ function registerSandboxTools(server, options = {}) {
2137
2235
  })
2138
2236
  )
2139
2237
  );
2140
- registered.push(
2238
+ tools.push(
2141
2239
  server.tool(
2142
2240
  "get_request",
2143
2241
  "Get full details for a request captured by a sandbox endpoint.",
@@ -2148,7 +2246,7 @@ function registerSandboxTools(server, options = {}) {
2148
2246
  })
2149
2247
  )
2150
2248
  );
2151
- registered.push(
2249
+ tools.push(
2152
2250
  server.tool(
2153
2251
  "wait_for_request",
2154
2252
  "Wait for a request to arrive at a sandbox endpoint.",
@@ -2160,7 +2258,7 @@ function registerSandboxTools(server, options = {}) {
2160
2258
  })
2161
2259
  )
2162
2260
  );
2163
- registered.push(
2261
+ tools.push(
2164
2262
  server.tool(
2165
2263
  "generate_payload_schema",
2166
2264
  "Generate TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema from the bodies of requests captured by a sandbox endpoint. Several requests of one event teach which fields are optional or nullable.",
@@ -2172,62 +2270,59 @@ function registerSandboxTools(server, options = {}) {
2172
2270
  })
2173
2271
  )
2174
2272
  );
2175
- registered.push(
2273
+ tools.push(
2176
2274
  server.tool(
2177
2275
  "connect_account",
2178
2276
  "Connect this agent to a human's webhooks.cc account, for the full tool set (mock responses, signatures, email, teams) and to keep the sandbox endpoints. Returns a link and a 6-digit code: show both to the human, who signs in or signs up with that email and enters the code. Then call wait_for_connection. Calling again gives a new code.",
2179
2277
  {
2180
- email: import_zod3.z.string().email({ message: "Invalid email address" }).describe("The human's email address; only someone signed in as it can connect")
2278
+ email: import_zod3.z.string().email({ message: "Invalid email address" }).describe(
2279
+ "The email the human signs in to webhooks.cc with (their GitHub or Google primary email if they sign in that way); only someone signed in as it can connect"
2280
+ )
2181
2281
  },
2182
2282
  withErrorHandling(async ({ email }) => {
2183
- if (session.connected) {
2184
- return jsonContent({ connected: true, message: "Already connected to an account." });
2185
- }
2186
2283
  const normalized = email.trim().toLowerCase();
2284
+ const previous = session.connect;
2187
2285
  const sandbox = await session.existingSandbox();
2188
- let claim;
2286
+ let attempt;
2287
+ let movesEndpoints = false;
2189
2288
  if (sandbox) {
2190
- claim = {
2191
- kind: "sandbox",
2192
- claimToken: sandbox.registration.claimToken,
2193
- email: normalized,
2194
- attempt: await sandbox.claim({ email: normalized }),
2195
- claimExpires: sandbox.expiresAt
2196
- };
2197
- } else if (session.claim?.kind === "service_auth" && session.claim.email === normalized && session.claim.claimExpires.getTime() > Date.now()) {
2198
- const attempt = await import_sdk2.WebhooksCC.agent.startClaim({
2199
- baseUrl,
2200
- claimToken: session.claim.claimToken
2201
- });
2202
- claim = { ...session.claim, attempt };
2289
+ const claim = await sandbox.claim({ email: normalized });
2290
+ session.connect = { via: "sandbox", sandbox, email: normalized, attempt: claim };
2291
+ attempt = claim;
2292
+ movesEndpoints = claim.movesEndpoints;
2203
2293
  } else {
2204
- const registration = await import_sdk2.WebhooksCC.agent.registerServiceAuth({
2205
- baseUrl,
2206
- email: normalized,
2207
- clientName: session.clientName
2208
- });
2209
- claim = {
2210
- kind: "service_auth",
2211
- claimToken: registration.claimToken,
2294
+ let pending = null;
2295
+ if (previous?.via === "connect" && previous.email === normalized) {
2296
+ pending = await previous.pending.restart().catch((error) => {
2297
+ if (error instanceof import_sdk2.AgentAuthError) return null;
2298
+ throw error;
2299
+ });
2300
+ }
2301
+ pending ?? (pending = await import_sdk2.WebhooksCC.connect({
2302
+ baseUrl: session.baseUrl,
2303
+ webhookUrl: session.webhookUrl,
2212
2304
  email: normalized,
2213
- attempt: registration.claim,
2214
- claimExpires: registration.claimTokenExpires
2215
- };
2305
+ clientName: session.clientName,
2306
+ credentialsFile: session.credentialsFile
2307
+ }));
2308
+ session.connect = { via: "connect", pending, email: normalized };
2309
+ attempt = pending;
2216
2310
  }
2217
- session.claim = claim;
2218
- const { verificationUri, userCode, expiresAt } = claim.attempt;
2311
+ const { verificationUri, userCode, expiresAt } = attempt;
2219
2312
  return jsonContent({
2220
2313
  verificationUri,
2221
2314
  userCode,
2222
2315
  expiresAt: expiresAt.toISOString(),
2223
2316
  tellTheHuman: `Open ${verificationUri}, sign in or sign up as ${normalized}, and enter the code ${userCode}.`,
2224
2317
  next: `Show the human that line, then call wait_for_connection. The code works for about ${minutesLeft(expiresAt)} minutes; connect_account gives a new one.`,
2225
- ...claim.kind === "sandbox" ? { endpoints: "The sandbox endpoints move into that account with what they captured." } : {}
2318
+ ...sandbox ? {
2319
+ endpoints: movesEndpoints ? "The sandbox endpoints move into that account with what they captured." : "The sandbox endpoints were made in an earlier session, so they stay in the sandbox until it expires; the account starts without them."
2320
+ } : {}
2226
2321
  });
2227
2322
  })
2228
2323
  )
2229
2324
  );
2230
- registered.push(
2325
+ tools.push(
2231
2326
  server.tool(
2232
2327
  "wait_for_connection",
2233
2328
  `Wait for the human to enter the code from connect_account. On success the full webhooks.cc tools for their account replace these sandbox tools. Returns after timeoutSeconds (default ${DEFAULT_WAIT_SECONDS}) if they have not finished yet; call it again to keep waiting.`,
@@ -2235,28 +2330,22 @@ function registerSandboxTools(server, options = {}) {
2235
2330
  timeoutSeconds: import_zod3.z.number().int().min(1).max(MAX_WAIT_SECONDS).optional().describe(`How long to wait, in seconds (default ${DEFAULT_WAIT_SECONDS})`)
2236
2331
  },
2237
2332
  withErrorHandling(async ({ timeoutSeconds }) => {
2238
- if (session.connected) {
2239
- return jsonContent({ connected: true, message: "Already connected to an account." });
2240
- }
2241
- const claim = session.claim;
2242
- if (!claim) {
2333
+ const pending = session.connect;
2334
+ if (!pending) {
2243
2335
  throw new Error("Nothing to wait for: call connect_account first.");
2244
2336
  }
2245
- let claimed;
2337
+ const timeout = (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
2338
+ const expiresAt = pending.via === "sandbox" ? pending.attempt.expiresAt : pending.pending.expiresAt;
2339
+ let client;
2246
2340
  try {
2247
- claimed = await import_sdk2.WebhooksCC.agent.waitForClaim({
2248
- baseUrl,
2249
- claimToken: claim.claimToken,
2250
- interval: claim.attempt.interval,
2251
- timeout: (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3
2252
- });
2341
+ client = pending.via === "sandbox" ? await pending.sandbox.waitForClaim({ timeout }) : await pending.pending.wait({ timeout });
2253
2342
  } catch (error) {
2254
2343
  if (!(error instanceof import_sdk2.AgentAuthError)) throw error;
2255
2344
  if (error.code === "timeout") {
2256
2345
  return jsonContent({
2257
2346
  connected: false,
2258
2347
  status: "waiting",
2259
- message: `Not connected yet. The code works until ${claim.attempt.expiresAt.toISOString()}; call wait_for_connection again.`
2348
+ message: `Not connected yet. The code works until ${expiresAt.toISOString()}; call wait_for_connection again.`
2260
2349
  });
2261
2350
  }
2262
2351
  if (error.code === "expired_token") {
@@ -2275,34 +2364,25 @@ function registerSandboxTools(server, options = {}) {
2275
2364
  }
2276
2365
  throw error;
2277
2366
  }
2278
- const client = new import_sdk2.WebhooksCC({
2279
- baseUrl,
2280
- webhookUrl: session.webhookUrl,
2281
- getAccessToken: claimedTokens(baseUrl, claimed.identityAssertion, claimed.token)
2282
- });
2283
- session.connected = true;
2284
- session.claim = null;
2285
- session.forgetSandbox();
2286
- for (const tool of registered) tool.remove();
2287
- registerTools(server, client);
2288
- options.onConnected?.(client);
2367
+ const movedEndpoints = pending.via === "sandbox" && pending.attempt.movesEndpoints;
2368
+ session.reset();
2369
+ showAccount(client);
2289
2370
  return jsonContent({
2290
2371
  connected: true,
2291
- email: claim.email,
2292
- scope: claimed.token.scope,
2293
- message: `Connected to the webhooks.cc account of ${claim.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${claim.kind === "sandbox" ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY.`
2372
+ email: pending.email,
2373
+ message: `Connected to the webhooks.cc account of ${pending.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${movedEndpoints ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} ${file ? `The connection is saved in ${file}, so later sessions, the whk CLI and the SDKs start connected; it lasts 90 days, and the human can remove it under Account, Connected agents.` : "This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY."}`
2294
2374
  });
2295
2375
  })
2296
2376
  )
2297
2377
  );
2298
- registered.push(
2378
+ tools.push(
2299
2379
  server.tool(
2300
2380
  "about_sandbox",
2301
2381
  "Explain what this agent can do without a webhooks.cc account, the sandbox limits, and how to connect an account. No network call.",
2302
2382
  {},
2303
2383
  withErrorHandling(async () => {
2304
2384
  return jsonContent({
2305
- mode: "sandbox (no WHK_API_KEY)",
2385
+ mode: "sandbox (no WHK_API_KEY, no saved connection)",
2306
2386
  sandbox: {
2307
2387
  maxEndpoints: SANDBOX.maxEndpoints,
2308
2388
  requestsPerEndpoint: SANDBOX.requestsPerEndpoint,
@@ -2313,21 +2393,23 @@ function registerSandboxTools(server, options = {}) {
2313
2393
  notAvailable: "Mock responses, response rules, notifications, signature verification, email capture, forwarding, teams and sending webhooks."
2314
2394
  },
2315
2395
  connectAnAccount: [
2316
- "Call connect_account with the human's email and show them the link and code it returns.",
2396
+ "Call connect_account with the email the human signs in to webhooks.cc with, and show them the link and code it returns.",
2317
2397
  "Call wait_for_connection until it reports connected; the full tools for their account then replace the sandbox tools.",
2318
- "Sandbox endpoints move into the account with their captured requests."
2398
+ "Sandbox endpoints registered by this server process move into the account with their captured requests; a sandbox resumed from an earlier session stays in the sandbox (connect_account says which)."
2319
2399
  ],
2400
+ savedIn: file ? `${file} (shared with the whk CLI and the SDKs): the sandbox and the connection survive restarts.` : "nowhere: this session only.",
2320
2401
  withAnApiKey: "Set WHK_API_KEY to a whcc_ key from the dashboard and restart for the full tools from the start.",
2321
2402
  docs: DOCS_URL,
2322
- protocol: `${baseUrl}/auth.md`
2403
+ protocol: `${session.baseUrl}/auth.md`
2323
2404
  });
2324
2405
  })
2325
2406
  )
2326
2407
  );
2408
+ return tools;
2327
2409
  }
2328
2410
 
2329
2411
  // src/index.ts
2330
- var VERSION = true ? "1.14.0" : "0.0.0-dev";
2412
+ var VERSION = true ? "1.15.0" : "0.0.0-dev";
2331
2413
  function createServer(options = {}) {
2332
2414
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
2333
2415
  const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
@@ -2338,7 +2420,7 @@ function createServer(options = {}) {
2338
2420
  { debouncedNotificationMethods: ["notifications/tools/list_changed"] }
2339
2421
  );
2340
2422
  if (!apiKey) {
2341
- registerSandboxTools(server, { baseUrl, webhookUrl });
2423
+ registerSandboxTools(server, { baseUrl, webhookUrl, credentialsFile: options.credentialsFile });
2342
2424
  registerPrompts(server);
2343
2425
  return server;
2344
2426
  }
@@ -2367,13 +2449,16 @@ function mcpServerConfig(apiKey) {
2367
2449
  return {
2368
2450
  command: "npx",
2369
2451
  args: ["-y", "@webhooks-cc/mcp"],
2370
- env: { WHK_API_KEY: apiKey }
2452
+ ...apiKey ? { env: { WHK_API_KEY: apiKey } } : {}
2371
2453
  };
2372
2454
  }
2455
+ function vscodeServerConfig(apiKey) {
2456
+ return { type: "stdio", ...mcpServerConfig(apiKey) };
2457
+ }
2373
2458
  function resolveApiKey(flags) {
2374
2459
  return flags["api-key"] ?? process.env.WHK_API_KEY ?? null;
2375
2460
  }
2376
- function mergeJsonConfig(filePath, serverName, serverConfig) {
2461
+ function mergeJsonConfig(filePath, serverName, serverConfig, key = "mcpServers") {
2377
2462
  let existing = {};
2378
2463
  if ((0, import_fs.existsSync)(filePath)) {
2379
2464
  try {
@@ -2384,9 +2469,9 @@ function mergeJsonConfig(filePath, serverName, serverConfig) {
2384
2469
  (0, import_fs.copyFileSync)(filePath, `${filePath}.bak`);
2385
2470
  }
2386
2471
  }
2387
- const mcpServers = existing.mcpServers ?? {};
2388
- mcpServers[serverName] = serverConfig;
2389
- existing.mcpServers = mcpServers;
2472
+ const servers = existing[key] ?? {};
2473
+ servers[serverName] = serverConfig;
2474
+ existing[key] = servers;
2390
2475
  const dir = (0, import_path.dirname)(filePath);
2391
2476
  if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
2392
2477
  (0, import_fs.writeFileSync)(filePath, JSON.stringify(existing, null, 2) + "\n");
@@ -2423,8 +2508,7 @@ function setupClaudeCode(apiKey) {
2423
2508
  "--transport",
2424
2509
  "stdio",
2425
2510
  "webhooks-cc",
2426
- "-e",
2427
- `WHK_API_KEY=${apiKey}`,
2511
+ ...apiKey ? ["-e", `WHK_API_KEY=${apiKey}`] : [],
2428
2512
  "--",
2429
2513
  "npx",
2430
2514
  "-y",
@@ -2439,7 +2523,7 @@ function setupClaudeCode(apiKey) {
2439
2523
  console.error("Install it: npm install -g @anthropic-ai/claude-code");
2440
2524
  console.error("\nManual alternative \u2014 run this command:");
2441
2525
  console.error(
2442
- ` claude mcp add -s user --transport stdio webhooks-cc -e WHK_API_KEY=<your-key> -- npx -y @webhooks-cc/mcp`
2526
+ ` claude mcp add -s user --transport stdio webhooks-cc${apiKey ? " -e WHK_API_KEY=<your-key>" : ""} -- npx -y @webhooks-cc/mcp`
2443
2527
  );
2444
2528
  process.exit(1);
2445
2529
  }
@@ -2458,7 +2542,7 @@ function setupCursor(apiKey) {
2458
2542
  }
2459
2543
  function setupVSCode(apiKey) {
2460
2544
  const configPath = (0, import_path.join)(process.cwd(), ".vscode", "mcp.json");
2461
- mergeJsonConfig(configPath, "webhooks-cc", mcpServerConfig(apiKey));
2545
+ mergeJsonConfig(configPath, "webhooks-cc", vscodeServerConfig(apiKey), "servers");
2462
2546
  console.log(`Done! Config written to ${configPath}`);
2463
2547
  console.log("Reload VS Code window for changes to take effect.");
2464
2548
  }
@@ -2470,8 +2554,7 @@ function setupCodex(apiKey) {
2470
2554
  "mcp",
2471
2555
  "add",
2472
2556
  "webhooks-cc",
2473
- "-e",
2474
- `WHK_API_KEY=${apiKey}`,
2557
+ ...apiKey ? ["-e", `WHK_API_KEY=${apiKey}`] : [],
2475
2558
  "--",
2476
2559
  "npx",
2477
2560
  "-y",
@@ -2484,13 +2567,13 @@ function setupCodex(apiKey) {
2484
2567
  console.error("\nFailed to run 'codex mcp add'. Is OpenAI Codex CLI installed?");
2485
2568
  console.error("\nManual alternative \u2014 add to ~/.codex/config.toml:");
2486
2569
  console.error(`
2487
- [mcp.webhooks-cc]
2570
+ [mcp_servers.webhooks-cc]
2488
2571
  command = "npx"
2489
2572
  args = ["-y", "@webhooks-cc/mcp"]
2490
-
2491
- [mcp.webhooks-cc.env]
2573
+ ${apiKey ? `
2574
+ [mcp_servers.webhooks-cc.env]
2492
2575
  WHK_API_KEY = "<your-key>"
2493
- `);
2576
+ ` : ""}`);
2494
2577
  process.exit(1);
2495
2578
  }
2496
2579
  }
@@ -2507,6 +2590,9 @@ function printUsage() {
2507
2590
  Usage:
2508
2591
  npx @webhooks-cc/mcp setup <tool> [--api-key <key>]
2509
2592
 
2593
+ Without a key the server starts in the keyless agent sandbox; your agent can
2594
+ connect your account later (connect_account), and the connection is saved.
2595
+
2510
2596
  Tools:
2511
2597
  claude-code Claude Code CLI (runs 'claude mcp add')
2512
2598
  claude-desktop Claude Desktop app
@@ -2516,9 +2602,10 @@ Tools:
2516
2602
  windsurf Windsurf editor
2517
2603
 
2518
2604
  Options:
2519
- --api-key <key> Your webhooks.cc API key (or set WHK_API_KEY env var)
2605
+ --api-key <key> Your webhooks.cc API key (or set WHK_API_KEY env var); optional
2520
2606
 
2521
2607
  Examples:
2608
+ npx @webhooks-cc/mcp setup claude-code
2522
2609
  npx @webhooks-cc/mcp setup claude-code --api-key whcc_abc123
2523
2610
  WHK_API_KEY=whcc_abc123 npx @webhooks-cc/mcp setup cursor
2524
2611
  npx @webhooks-cc/mcp setup vscode --api-key whcc_abc123
@@ -2546,14 +2633,13 @@ function runSetup(args2) {
2546
2633
  process.exit(tool ? 1 : 0);
2547
2634
  }
2548
2635
  const apiKey = resolveApiKey(flags);
2549
- if (!apiKey) {
2550
- console.error("Error: API key required. Pass --api-key or set WHK_API_KEY env var.");
2551
- console.error("Get your API key at https://webhooks.cc/account/api-keys\n");
2552
- process.exit(1);
2553
- }
2554
- validateApiKeyFormat(apiKey);
2636
+ if (apiKey) validateApiKeyFormat(apiKey);
2555
2637
  console.log(`Setting up webhooks-cc MCP server for ${tool}...
2556
2638
  `);
2639
+ if (!apiKey) {
2640
+ console.log("No API key: the server starts in the keyless agent sandbox. Your agent can");
2641
+ console.log("connect your account later with connect_account; the connection is saved.\n");
2642
+ }
2557
2643
  switch (tool) {
2558
2644
  case "claude-code":
2559
2645
  setupClaudeCode(apiKey);
@@ -2591,17 +2677,25 @@ Usage:
2591
2677
  npx @webhooks-cc/mcp --help Show this help
2592
2678
 
2593
2679
  Environment:
2594
- WHK_API_KEY Your webhooks.cc API key (required)
2680
+ WHK_API_KEY Your webhooks.cc API key (optional; without it the server
2681
+ starts connected to the account your agent was connected
2682
+ to before, or in the keyless agent sandbox)
2683
+ WHK_AGENT_FILE Where the sandbox and the connection are saved (default:
2684
+ ~/.config/whk/agent.json, shared with the whk CLI and SDKs)
2595
2685
  WHK_WEBHOOK_URL Custom webhook receiver URL (optional)
2596
2686
  WHK_BASE_URL Custom API base URL (optional)
2597
2687
 
2598
2688
  Setup:
2599
- npx @webhooks-cc/mcp setup claude-code --api-key whcc_...
2600
- npx @webhooks-cc/mcp setup cursor --api-key whcc_...
2601
- npx @webhooks-cc/mcp setup vscode --api-key whcc_...
2602
- npx @webhooks-cc/mcp setup codex --api-key whcc_...
2603
- npx @webhooks-cc/mcp setup windsurf --api-key whcc_...
2604
- npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
2689
+ npx @webhooks-cc/mcp setup claude-code
2690
+ npx @webhooks-cc/mcp setup cursor
2691
+ npx @webhooks-cc/mcp setup vscode
2692
+ npx @webhooks-cc/mcp setup codex
2693
+ npx @webhooks-cc/mcp setup windsurf
2694
+ npx @webhooks-cc/mcp setup claude-desktop
2695
+ (add --api-key whcc_... to start with your account's tools)
2696
+
2697
+ Hosted agents (claude.ai, ChatGPT): add https://webhooks.cc/mcp as a remote
2698
+ MCP server instead; you approve it once in your browser.
2605
2699
 
2606
2700
  Get your API key at https://webhooks.cc/account/api-keys
2607
2701
  `);