@leadbay/mcp 0.32.0 → 0.32.1

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
@@ -634,7 +634,7 @@ Use `dry_run: true` to validate domain formatting and wizard reachability withou
634
634
  | `LEADBAY_MOCK` | no | unset | `"1"` serves all reads from on-disk fixtures (dev only) |
635
635
  | `LEADBAY_MOCK_DIR` | no | `./.context/leadbay-live-shapes/` | Fixture dir for mock mode |
636
636
  | `LEADBAY_LOG_LEVEL` | no | `error` | `debug` \| `info` \| `error`, logs to stderr |
637
- | `LEADBAY_TIMEOUT_MS` | no | (client default) | Per-request timeout override |
637
+ | `LEADBAY_TIMEOUT_MS` | no | `600000` | Backstop deadline for a single outbound Leadbay request, for the case where nothing cancels it. Not a latency budget: long work (enrichment, bulk qualify, import) is launched and polled, and a cancelled tool call already closes its own requests. On expiry the socket is closed and the tool returns a `TIMEOUT` error. Set `0` to disable the backstop. |
638
638
 
639
639
  > ⚠️ **Set `LEADBAY_REGION` explicitly.** If you don't, the server probes BOTH `api-us.leadbay.app` and `api-fr.leadbay.app` in parallel with your bearer token attached, sending the token to a backend that doesn't own your account. The `install` and `login` subcommands enforce `--region` for exactly this reason; the runtime auto-probe is a backwards-compat fallback, not a recommended setting.
640
640
 
@@ -657,6 +657,7 @@ Use `dry_run: true` to validate domain formatting and wizard reachability withou
657
657
  | `mcp tool called` | Every tool invocation | `tool`, `ok`, `duration_ms`, `format`, `bytes`, `error_code` (if failed) |
658
658
  | `mcp quota hit` | When the API returns `QUOTA_EXCEEDED` (HTTP 429/402) | `tool`, `retry_after_s`, `endpoint` |
659
659
  | `mcp topup link created` | When `leadbay_create_topup_link` returns a checkout URL | `tool` (the URL itself is **never** captured) |
660
+ | `mcp tool timeout` | When an outbound Leadbay request exceeds `LEADBAY_TIMEOUT_MS` | `tool`, `timeout_ms`, `endpoint`, `region` |
660
661
 
661
662
  After your first authenticated call, your PostHog `distinctId` is set to your Leadbay account email so MCP events consolidate with web-app events for the same person. Events also carry `$groups.organization` so org-level rollups work.
662
663
 
package/dist/bin.js CHANGED
@@ -11,20 +11,47 @@ var __export = (target, all) => {
11
11
 
12
12
  // ../core/dist/client.js
13
13
  import https from "https";
14
+ import { AsyncLocalStorage } from "async_hooks";
14
15
  import { readdirSync, readFileSync, existsSync } from "fs";
15
16
  import { join } from "path";
16
- function httpsRequest(method, url, headers, body, timeoutMs) {
17
+ function defaultTimeoutMs() {
18
+ const raw = process.env.LEADBAY_TIMEOUT_MS;
19
+ if (raw === void 0 || raw.trim() === "")
20
+ return DEFAULT_REQUEST_TIMEOUT_MS;
21
+ const n = Number(raw);
22
+ return Number.isFinite(n) ? n : DEFAULT_REQUEST_TIMEOUT_MS;
23
+ }
24
+ function runWithRequestSignal(signal, fn) {
25
+ return requestSignalStore.run(signal, fn);
26
+ }
27
+ function makeCancelledError(method, url) {
28
+ const err = new Error(`Request cancelled: ${method} ${url}`);
29
+ err.name = "AbortError";
30
+ err.code = "CANCELLED";
31
+ return err;
32
+ }
33
+ function httpsRequest(method, url, headers, body, timeoutMs, signal) {
34
+ const deadlineMs = timeoutMs ?? defaultTimeoutMs();
35
+ const abortSignal = signal ?? requestSignalStore.getStore();
36
+ const abortSafe = method.toUpperCase() === "GET";
17
37
  return new Promise((resolve, reject) => {
18
38
  const start = Date.now();
39
+ if (abortSignal?.aborted) {
40
+ reject(makeCancelledError(method, url));
41
+ return;
42
+ }
19
43
  const parsed = new URL(url);
20
44
  const reqHeaders = { ...headers };
21
45
  if (body !== void 0) {
22
46
  reqHeaders["Content-Length"] = Buffer.byteLength(body);
23
47
  }
24
48
  let deadline;
49
+ let onAbort;
25
50
  const clearDeadline = () => {
26
51
  if (deadline !== void 0)
27
52
  clearTimeout(deadline);
53
+ if (onAbort)
54
+ abortSignal?.removeEventListener("abort", onAbort);
28
55
  };
29
56
  const req = https.request({
30
57
  hostname: parsed.hostname,
@@ -45,15 +72,24 @@ function httpsRequest(method, url, headers, body, timeoutMs) {
45
72
  });
46
73
  });
47
74
  });
48
- if (timeoutMs !== void 0 && timeoutMs > 0) {
75
+ if (deadlineMs > 0) {
49
76
  deadline = setTimeout(() => {
50
77
  req.destroy?.();
51
- const err = new Error(`Request timed out after ${timeoutMs}ms: ${method} ${url}`);
78
+ const err = new Error(`Request timed out after ${deadlineMs}ms: ${method} ${url}`);
52
79
  err.code = "TIMEOUT";
80
+ err.timeout_ms = deadlineMs;
53
81
  reject(err);
54
- }, timeoutMs);
82
+ }, deadlineMs);
55
83
  deadline.unref?.();
56
84
  }
85
+ if (abortSignal && abortSafe) {
86
+ onAbort = () => {
87
+ req.destroy?.();
88
+ clearDeadline();
89
+ reject(makeCancelledError(method, url));
90
+ };
91
+ abortSignal.addEventListener("abort", onAbort, { once: true });
92
+ }
57
93
  req.on("error", (e) => {
58
94
  clearDeadline();
59
95
  reject(e);
@@ -177,7 +213,7 @@ function parseRetryAfter(value) {
177
213
  }
178
214
  return null;
179
215
  }
180
- var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
216
+ var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, DEFAULT_REQUEST_TIMEOUT_MS, requestSignalStore, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
181
217
  var init_client = __esm({
182
218
  "../core/dist/client.js"() {
183
219
  "use strict";
@@ -185,6 +221,8 @@ var init_client = __esm({
185
221
  TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
186
222
  ME_CACHE_TTL_MS = 60 * 1e3;
187
223
  MAX_CONCURRENT = 5;
224
+ DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
225
+ requestSignalStore = new AsyncLocalStorage();
188
226
  REGIONS = {
189
227
  us: "https://api-us.leadbay.app",
190
228
  fr: "https://api-fr.leadbay.app"
@@ -408,6 +446,8 @@ var init_client = __esm({
408
446
  throw this.mapErrorResponse(res.status, res.body, path, res.headers);
409
447
  }
410
448
  return JSON.parse(res.body);
449
+ } catch (e) {
450
+ throw this.mapTransportError(e, `${method} ${path}`);
411
451
  } finally {
412
452
  this.releaseSemaphore();
413
453
  }
@@ -439,6 +479,8 @@ var init_client = __esm({
439
479
  if (res.status < 200 || res.status >= 300) {
440
480
  throw this.mapErrorResponse(res.status, res.body, path, res.headers);
441
481
  }
482
+ } catch (e) {
483
+ throw this.mapTransportError(e, `${method} ${path}`);
442
484
  } finally {
443
485
  this.releaseSemaphore();
444
486
  }
@@ -476,6 +518,8 @@ var init_client = __esm({
476
518
  throw this.mapErrorResponse(res.status, res.body, path, res.headers);
477
519
  }
478
520
  return JSON.parse(res.body);
521
+ } catch (e) {
522
+ throw this.mapTransportError(e, `${method} ${path}`);
479
523
  } finally {
480
524
  this.releaseSemaphore();
481
525
  }
@@ -536,6 +580,29 @@ var init_client = __esm({
536
580
  would_call: { method, path: fullPath, body: journalBody }
537
581
  };
538
582
  }
583
+ /**
584
+ * Turn httpsRequest's raw TIMEOUT rejection into the `{error:true, code, …}`
585
+ * envelope every other failure already speaks, so the agent gets something it
586
+ * can read out to the user and act on rather than a bare Error string. Any
587
+ * other rejection (ECONNRESET, DNS, a mapped 4xx/5xx) passes through untouched
588
+ * — this is a translation, not a catch-all.
589
+ *
590
+ * The code stays "TIMEOUT" so the hosted auth probe's existing branch
591
+ * (auth-http.ts) keeps classifying it as a transient fault and moves to the
592
+ * sibling region instead of declaring a live token expired.
593
+ */
594
+ mapTransportError(e, endpoint) {
595
+ const err = e;
596
+ if (err?.code !== "TIMEOUT")
597
+ return e;
598
+ const ms = err.timeout_ms ?? defaultTimeoutMs();
599
+ const envelope = this.makeError("TIMEOUT", `Leadbay did not respond within ${ms}ms \u2014 the request was cancelled`, "The connection was accepted but no response came back, so this is a Leadbay-side stall, not a bad request. It is transient: retry the same call once. If it times out again, tell the user Leadbay is not responding right now and offer to report it with leadbay_report_friction.", endpoint);
600
+ if (envelope._meta) {
601
+ envelope._meta.timeout_ms = ms;
602
+ envelope._meta.latency_ms = ms;
603
+ }
604
+ return envelope;
605
+ }
539
606
  mapErrorResponse(status, rawBody, endpoint, headers) {
540
607
  let parsed;
541
608
  try {
@@ -657,6 +724,8 @@ var init_client = __esm({
657
724
  this.telemetryEnabledFromStamp = false;
658
725
  }
659
726
  return observed;
727
+ } catch (e) {
728
+ throw this.mapTransportError(e, "GET /users/me");
660
729
  } finally {
661
730
  this.releaseSemaphore();
662
731
  }
@@ -26464,6 +26533,7 @@ __export(dist_exports, {
26464
26533
  AgentMemorySourceSchema: () => AgentMemorySourceSchema,
26465
26534
  AgentMemoryTombstoneSchema: () => AgentMemoryTombstoneSchema,
26466
26535
  COMPOSITE_FILE_TOOL_NAMES: () => COMPOSITE_FILE_TOOL_NAMES,
26536
+ DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
26467
26537
  GETTING_STARTED_MANIFEST: () => GETTING_STARTED_MANIFEST,
26468
26538
  InMemoryBulkStore: () => InMemoryBulkStore,
26469
26539
  LeadbayClient: () => LeadbayClient,
@@ -26587,6 +26657,7 @@ __export(dist_exports, {
26587
26657
  resolveImportRows: () => resolveImportRows,
26588
26658
  resolveRegion: () => resolveRegion,
26589
26659
  reviseHintFor: () => reviseHintFor,
26660
+ runWithRequestSignal: () => runWithRequestSignal,
26590
26661
  scanPortfolioSignals: () => scanPortfolioSignals,
26591
26662
  seedCandidates: () => seedCandidates,
26592
26663
  selectLeads: () => selectLeads,
@@ -29837,6 +29908,7 @@ var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown
29837
29908
 
29838
29909
  // src/server.ts
29839
29910
  init_dist();
29911
+ init_dist();
29840
29912
 
29841
29913
  // src/telemetry.ts
29842
29914
  import { PostHog } from "posthog-node";
@@ -29850,6 +29922,7 @@ var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o45058744368
29850
29922
  // src/telemetry-events.ts
29851
29923
  var EV_TOOL_CALL = "mcp tool called";
29852
29924
  var EV_QUOTA_HIT = "mcp quota hit";
29925
+ var EV_TOOL_TIMEOUT = "mcp tool timeout";
29853
29926
  var EV_TOPUP_LINK = "mcp topup link created";
29854
29927
  var EV_STARTUP = "mcp startup";
29855
29928
  var EV_MCP_UPDATE_CHECK = "mcp update check";
@@ -29860,6 +29933,7 @@ var EV_MCP_VERSION_UPDATED = "mcp version updated";
29860
29933
  var EV_AGENT_MEMORY_CAPTURED = "agent_memory_captured";
29861
29934
  var EV_AGENT_MEMORY_RECALLED = "agent_memory_recalled";
29862
29935
  var EV_AGENT_MEMORY_PRUNED = "agent_memory_pruned";
29936
+ var DURATION_PLAUSIBILITY_CEILING_MS = 6e5;
29863
29937
  var EV_FRICTION_REPORTED = "mcp friction reported";
29864
29938
  var EV_COMPOSITE_CALL = "mcp composite call";
29865
29939
 
@@ -29873,6 +29947,8 @@ var NOOP_TELEMETRY = {
29873
29947
  },
29874
29948
  captureQuotaHit: (_props, _identity) => {
29875
29949
  },
29950
+ captureToolTimeout: (_props, _identity) => {
29951
+ },
29876
29952
  captureTopupLink: (_props, _identity) => {
29877
29953
  },
29878
29954
  captureStartup: (_props, _identity) => {
@@ -29907,6 +29983,13 @@ function parseTelemetryEnv(raw) {
29907
29983
  if (v === "false" || v === "0" || v === "no" || v === "off") return false;
29908
29984
  return true;
29909
29985
  }
29986
+ function withPlausibleDuration(props) {
29987
+ const { duration_ms, ...rest } = props;
29988
+ if (Number.isFinite(duration_ms) && duration_ms >= 0 && duration_ms <= DURATION_PLAUSIBILITY_CEILING_MS) {
29989
+ return { ...rest, duration_ms };
29990
+ }
29991
+ return { ...rest, duration_ms_raw: duration_ms, duration_implausible: true };
29992
+ }
29910
29993
  function initTelemetry(opts) {
29911
29994
  if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
29912
29995
  if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
@@ -30075,14 +30158,17 @@ function initTelemetry(opts) {
30075
30158
  return identityPromise;
30076
30159
  },
30077
30160
  captureToolCall(props, identity) {
30078
- emit(EV_TOOL_CALL, { ...props }, identity);
30161
+ emit(EV_TOOL_CALL, withPlausibleDuration(props), identity);
30079
30162
  },
30080
30163
  captureCompositeCall(props, identity) {
30081
- emit(EV_COMPOSITE_CALL, { ...props }, identity);
30164
+ emit(EV_COMPOSITE_CALL, withPlausibleDuration(props), identity);
30082
30165
  },
30083
30166
  captureQuotaHit(props, identity) {
30084
30167
  emit(EV_QUOTA_HIT, { ...props }, identity);
30085
30168
  },
30169
+ captureToolTimeout(props, identity) {
30170
+ emit(EV_TOOL_TIMEOUT, { ...props }, identity);
30171
+ },
30086
30172
  captureTopupLink(props, identity) {
30087
30173
  emit(EV_TOPUP_LINK, { ...props }, identity);
30088
30174
  },
@@ -30992,6 +31078,16 @@ function buildServer(client, opts = {}) {
30992
31078
  source: "business"
30993
31079
  };
30994
31080
  };
31081
+ const captureTimeoutAlert = (toolName, envelope, triggeredBy) => {
31082
+ const ms = envelope._meta?.timeout_ms;
31083
+ telemetry.captureToolTimeout({
31084
+ tool: toolName,
31085
+ ...typeof ms === "number" ? { timeout_ms: ms } : {},
31086
+ ...envelope._meta?.endpoint ? { endpoint: envelope._meta.endpoint } : {},
31087
+ ...envelope._meta?.region ? { region: envelope._meta.region } : {},
31088
+ ...triggeredBy !== void 0 ? { triggered_by: triggeredBy } : {}
31089
+ });
31090
+ };
30995
31091
  const captureAgentMemoryTelemetry = (toolName, result) => {
30996
31092
  if (!result || typeof result !== "object") return;
30997
31093
  const meta = result._meta ?? {};
@@ -31156,7 +31252,7 @@ ${url}
31156
31252
  isError: true
31157
31253
  };
31158
31254
  }
31159
- const result = await tool.execute(client, args, {
31255
+ const result = await runWithRequestSignal(extra.signal, () => tool.execute(client, args, {
31160
31256
  logger: opts.logger,
31161
31257
  bulkTracker: opts.bulkTracker,
31162
31258
  notificationsInbox: opts.notificationsInbox,
@@ -31189,7 +31285,7 @@ ${url}
31189
31285
  ...report.tool_called ? { tool_called: report.tool_called } : {},
31190
31286
  ...report.severity ? { severity: report.severity } : {}
31191
31287
  }) === true
31192
- });
31288
+ }));
31193
31289
  await maybeAttachUpdate(name, result);
31194
31290
  maybeAttachNotifications(result);
31195
31291
  if (result && typeof result === "object" && result.error === true) {
@@ -31205,6 +31301,9 @@ ${url}
31205
31301
  endpoint: result._meta?.endpoint
31206
31302
  });
31207
31303
  }
31304
+ if (envCode === "TIMEOUT") {
31305
+ captureTimeoutAlert(name, result, triggered_by);
31306
+ }
31208
31307
  telemetry.captureToolCall({
31209
31308
  tool: name,
31210
31309
  ok: false,
@@ -31335,6 +31434,9 @@ ${url}
31335
31434
  endpoint: err._meta?.endpoint
31336
31435
  });
31337
31436
  }
31437
+ if (!skipAnalytics && err.code === "TIMEOUT") {
31438
+ captureTimeoutAlert(name, err, triggered_by);
31439
+ }
31338
31440
  const httpStatus2 = err._meta?.http_status;
31339
31441
  if (!skipAnalytics) {
31340
31442
  telemetry.captureToolCall({
@@ -32838,7 +32940,7 @@ var OAUTH_BASE_URLS = {
32838
32940
  fr: "https://staging.api.leadbay.app"
32839
32941
  }
32840
32942
  };
32841
- var VERSION = "0.32.0";
32943
+ var VERSION = "0.32.1";
32842
32944
  var HELP = `
32843
32945
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
32844
32946
 
@@ -2792,30 +2792,59 @@ function getPrompt(name, args = {}) {
2792
2792
 
2793
2793
  // ../core/dist/client.js
2794
2794
  import https from "https";
2795
+ import { AsyncLocalStorage } from "async_hooks";
2795
2796
  import { readdirSync, readFileSync, existsSync } from "fs";
2796
2797
  import { join } from "path";
2797
2798
  var LENS_CACHE_TTL_MS = 5 * 60 * 1e3;
2798
2799
  var TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
2799
2800
  var ME_CACHE_TTL_MS = 60 * 1e3;
2800
2801
  var MAX_CONCURRENT = 5;
2802
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
2803
+ function defaultTimeoutMs() {
2804
+ const raw = process.env.LEADBAY_TIMEOUT_MS;
2805
+ if (raw === void 0 || raw.trim() === "")
2806
+ return DEFAULT_REQUEST_TIMEOUT_MS;
2807
+ const n = Number(raw);
2808
+ return Number.isFinite(n) ? n : DEFAULT_REQUEST_TIMEOUT_MS;
2809
+ }
2810
+ var requestSignalStore = new AsyncLocalStorage();
2811
+ function runWithRequestSignal(signal, fn) {
2812
+ return requestSignalStore.run(signal, fn);
2813
+ }
2814
+ function makeCancelledError(method, url) {
2815
+ const err = new Error(`Request cancelled: ${method} ${url}`);
2816
+ err.name = "AbortError";
2817
+ err.code = "CANCELLED";
2818
+ return err;
2819
+ }
2801
2820
  var REGIONS = {
2802
2821
  us: "https://api-us.leadbay.app",
2803
2822
  fr: "https://api-fr.leadbay.app"
2804
2823
  };
2805
2824
  var API_VERSION = "1.6";
2806
2825
  var API_PREFIX = `/${API_VERSION}`;
2807
- function httpsRequest(method, url, headers, body, timeoutMs) {
2826
+ function httpsRequest(method, url, headers, body, timeoutMs, signal) {
2827
+ const deadlineMs = timeoutMs ?? defaultTimeoutMs();
2828
+ const abortSignal = signal ?? requestSignalStore.getStore();
2829
+ const abortSafe = method.toUpperCase() === "GET";
2808
2830
  return new Promise((resolve, reject) => {
2809
2831
  const start = Date.now();
2832
+ if (abortSignal?.aborted) {
2833
+ reject(makeCancelledError(method, url));
2834
+ return;
2835
+ }
2810
2836
  const parsed = new URL(url);
2811
2837
  const reqHeaders = { ...headers };
2812
2838
  if (body !== void 0) {
2813
2839
  reqHeaders["Content-Length"] = Buffer.byteLength(body);
2814
2840
  }
2815
2841
  let deadline;
2842
+ let onAbort;
2816
2843
  const clearDeadline = () => {
2817
2844
  if (deadline !== void 0)
2818
2845
  clearTimeout(deadline);
2846
+ if (onAbort)
2847
+ abortSignal?.removeEventListener("abort", onAbort);
2819
2848
  };
2820
2849
  const req = https.request({
2821
2850
  hostname: parsed.hostname,
@@ -2836,15 +2865,24 @@ function httpsRequest(method, url, headers, body, timeoutMs) {
2836
2865
  });
2837
2866
  });
2838
2867
  });
2839
- if (timeoutMs !== void 0 && timeoutMs > 0) {
2868
+ if (deadlineMs > 0) {
2840
2869
  deadline = setTimeout(() => {
2841
2870
  req.destroy?.();
2842
- const err = new Error(`Request timed out after ${timeoutMs}ms: ${method} ${url}`);
2871
+ const err = new Error(`Request timed out after ${deadlineMs}ms: ${method} ${url}`);
2843
2872
  err.code = "TIMEOUT";
2873
+ err.timeout_ms = deadlineMs;
2844
2874
  reject(err);
2845
- }, timeoutMs);
2875
+ }, deadlineMs);
2846
2876
  deadline.unref?.();
2847
2877
  }
2878
+ if (abortSignal && abortSafe) {
2879
+ onAbort = () => {
2880
+ req.destroy?.();
2881
+ clearDeadline();
2882
+ reject(makeCancelledError(method, url));
2883
+ };
2884
+ abortSignal.addEventListener("abort", onAbort, { once: true });
2885
+ }
2848
2886
  req.on("error", (e) => {
2849
2887
  clearDeadline();
2850
2888
  reject(e);
@@ -3166,6 +3204,8 @@ var LeadbayClient = class _LeadbayClient {
3166
3204
  throw this.mapErrorResponse(res.status, res.body, path, res.headers);
3167
3205
  }
3168
3206
  return JSON.parse(res.body);
3207
+ } catch (e) {
3208
+ throw this.mapTransportError(e, `${method} ${path}`);
3169
3209
  } finally {
3170
3210
  this.releaseSemaphore();
3171
3211
  }
@@ -3197,6 +3237,8 @@ var LeadbayClient = class _LeadbayClient {
3197
3237
  if (res.status < 200 || res.status >= 300) {
3198
3238
  throw this.mapErrorResponse(res.status, res.body, path, res.headers);
3199
3239
  }
3240
+ } catch (e) {
3241
+ throw this.mapTransportError(e, `${method} ${path}`);
3200
3242
  } finally {
3201
3243
  this.releaseSemaphore();
3202
3244
  }
@@ -3234,6 +3276,8 @@ var LeadbayClient = class _LeadbayClient {
3234
3276
  throw this.mapErrorResponse(res.status, res.body, path, res.headers);
3235
3277
  }
3236
3278
  return JSON.parse(res.body);
3279
+ } catch (e) {
3280
+ throw this.mapTransportError(e, `${method} ${path}`);
3237
3281
  } finally {
3238
3282
  this.releaseSemaphore();
3239
3283
  }
@@ -3294,6 +3338,29 @@ var LeadbayClient = class _LeadbayClient {
3294
3338
  would_call: { method, path: fullPath, body: journalBody }
3295
3339
  };
3296
3340
  }
3341
+ /**
3342
+ * Turn httpsRequest's raw TIMEOUT rejection into the `{error:true, code, …}`
3343
+ * envelope every other failure already speaks, so the agent gets something it
3344
+ * can read out to the user and act on rather than a bare Error string. Any
3345
+ * other rejection (ECONNRESET, DNS, a mapped 4xx/5xx) passes through untouched
3346
+ * — this is a translation, not a catch-all.
3347
+ *
3348
+ * The code stays "TIMEOUT" so the hosted auth probe's existing branch
3349
+ * (auth-http.ts) keeps classifying it as a transient fault and moves to the
3350
+ * sibling region instead of declaring a live token expired.
3351
+ */
3352
+ mapTransportError(e, endpoint) {
3353
+ const err = e;
3354
+ if (err?.code !== "TIMEOUT")
3355
+ return e;
3356
+ const ms = err.timeout_ms ?? defaultTimeoutMs();
3357
+ const envelope = this.makeError("TIMEOUT", `Leadbay did not respond within ${ms}ms \u2014 the request was cancelled`, "The connection was accepted but no response came back, so this is a Leadbay-side stall, not a bad request. It is transient: retry the same call once. If it times out again, tell the user Leadbay is not responding right now and offer to report it with leadbay_report_friction.", endpoint);
3358
+ if (envelope._meta) {
3359
+ envelope._meta.timeout_ms = ms;
3360
+ envelope._meta.latency_ms = ms;
3361
+ }
3362
+ return envelope;
3363
+ }
3297
3364
  mapErrorResponse(status, rawBody, endpoint, headers) {
3298
3365
  let parsed;
3299
3366
  try {
@@ -3415,6 +3482,8 @@ var LeadbayClient = class _LeadbayClient {
3415
3482
  this.telemetryEnabledFromStamp = false;
3416
3483
  }
3417
3484
  return observed;
3485
+ } catch (e) {
3486
+ throw this.mapTransportError(e, "GET /users/me");
3418
3487
  } finally {
3419
3488
  this.releaseSemaphore();
3420
3489
  }
@@ -27663,6 +27732,7 @@ var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o45058744368
27663
27732
  // src/telemetry-events.ts
27664
27733
  var EV_TOOL_CALL = "mcp tool called";
27665
27734
  var EV_QUOTA_HIT = "mcp quota hit";
27735
+ var EV_TOOL_TIMEOUT = "mcp tool timeout";
27666
27736
  var EV_TOPUP_LINK = "mcp topup link created";
27667
27737
  var EV_STARTUP = "mcp startup";
27668
27738
  var EV_MCP_UPDATE_CHECK = "mcp update check";
@@ -27673,6 +27743,7 @@ var EV_MCP_VERSION_UPDATED = "mcp version updated";
27673
27743
  var EV_AGENT_MEMORY_CAPTURED = "agent_memory_captured";
27674
27744
  var EV_AGENT_MEMORY_RECALLED = "agent_memory_recalled";
27675
27745
  var EV_AGENT_MEMORY_PRUNED = "agent_memory_pruned";
27746
+ var DURATION_PLAUSIBILITY_CEILING_MS = 6e5;
27676
27747
  var EV_FRICTION_REPORTED = "mcp friction reported";
27677
27748
  var EV_COMPOSITE_CALL = "mcp composite call";
27678
27749
 
@@ -27686,6 +27757,8 @@ var NOOP_TELEMETRY = {
27686
27757
  },
27687
27758
  captureQuotaHit: (_props, _identity) => {
27688
27759
  },
27760
+ captureToolTimeout: (_props, _identity) => {
27761
+ },
27689
27762
  captureTopupLink: (_props, _identity) => {
27690
27763
  },
27691
27764
  captureStartup: (_props, _identity) => {
@@ -27720,6 +27793,13 @@ function parseTelemetryEnv(raw) {
27720
27793
  if (v === "false" || v === "0" || v === "no" || v === "off") return false;
27721
27794
  return true;
27722
27795
  }
27796
+ function withPlausibleDuration(props) {
27797
+ const { duration_ms, ...rest } = props;
27798
+ if (Number.isFinite(duration_ms) && duration_ms >= 0 && duration_ms <= DURATION_PLAUSIBILITY_CEILING_MS) {
27799
+ return { ...rest, duration_ms };
27800
+ }
27801
+ return { ...rest, duration_ms_raw: duration_ms, duration_implausible: true };
27802
+ }
27723
27803
  function initTelemetry(opts) {
27724
27804
  if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
27725
27805
  if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
@@ -27888,14 +27968,17 @@ function initTelemetry(opts) {
27888
27968
  return identityPromise;
27889
27969
  },
27890
27970
  captureToolCall(props, identity) {
27891
- emit(EV_TOOL_CALL, { ...props }, identity);
27971
+ emit(EV_TOOL_CALL, withPlausibleDuration(props), identity);
27892
27972
  },
27893
27973
  captureCompositeCall(props, identity) {
27894
- emit(EV_COMPOSITE_CALL, { ...props }, identity);
27974
+ emit(EV_COMPOSITE_CALL, withPlausibleDuration(props), identity);
27895
27975
  },
27896
27976
  captureQuotaHit(props, identity) {
27897
27977
  emit(EV_QUOTA_HIT, { ...props }, identity);
27898
27978
  },
27979
+ captureToolTimeout(props, identity) {
27980
+ emit(EV_TOOL_TIMEOUT, { ...props }, identity);
27981
+ },
27899
27982
  captureTopupLink(props, identity) {
27900
27983
  emit(EV_TOPUP_LINK, { ...props }, identity);
27901
27984
  },
@@ -28783,6 +28866,16 @@ function buildServer(client, opts = {}) {
28783
28866
  source: "business"
28784
28867
  };
28785
28868
  };
28869
+ const captureTimeoutAlert = (toolName, envelope, triggeredBy) => {
28870
+ const ms = envelope._meta?.timeout_ms;
28871
+ telemetry2.captureToolTimeout({
28872
+ tool: toolName,
28873
+ ...typeof ms === "number" ? { timeout_ms: ms } : {},
28874
+ ...envelope._meta?.endpoint ? { endpoint: envelope._meta.endpoint } : {},
28875
+ ...envelope._meta?.region ? { region: envelope._meta.region } : {},
28876
+ ...triggeredBy !== void 0 ? { triggered_by: triggeredBy } : {}
28877
+ });
28878
+ };
28786
28879
  const captureAgentMemoryTelemetry = (toolName, result) => {
28787
28880
  if (!result || typeof result !== "object") return;
28788
28881
  const meta = result._meta ?? {};
@@ -28947,7 +29040,7 @@ ${url}
28947
29040
  isError: true
28948
29041
  };
28949
29042
  }
28950
- const result = await tool.execute(client, args, {
29043
+ const result = await runWithRequestSignal(extra.signal, () => tool.execute(client, args, {
28951
29044
  logger: opts.logger,
28952
29045
  bulkTracker: opts.bulkTracker,
28953
29046
  notificationsInbox: opts.notificationsInbox,
@@ -28980,7 +29073,7 @@ ${url}
28980
29073
  ...report.tool_called ? { tool_called: report.tool_called } : {},
28981
29074
  ...report.severity ? { severity: report.severity } : {}
28982
29075
  }) === true
28983
- });
29076
+ }));
28984
29077
  await maybeAttachUpdate(name, result);
28985
29078
  maybeAttachNotifications(result);
28986
29079
  if (result && typeof result === "object" && result.error === true) {
@@ -28996,6 +29089,9 @@ ${url}
28996
29089
  endpoint: result._meta?.endpoint
28997
29090
  });
28998
29091
  }
29092
+ if (envCode === "TIMEOUT") {
29093
+ captureTimeoutAlert(name, result, triggered_by);
29094
+ }
28999
29095
  telemetry2.captureToolCall({
29000
29096
  tool: name,
29001
29097
  ok: false,
@@ -29126,6 +29222,9 @@ ${url}
29126
29222
  endpoint: err._meta?.endpoint
29127
29223
  });
29128
29224
  }
29225
+ if (!skipAnalytics && err.code === "TIMEOUT") {
29226
+ captureTimeoutAlert(name, err, triggered_by);
29227
+ }
29129
29228
  const httpStatus2 = err._meta?.http_status;
29130
29229
  if (!skipAnalytics) {
29131
29230
  telemetry2.captureToolCall({
@@ -29374,7 +29473,7 @@ function parseWriteEnv(env = process.env) {
29374
29473
  }
29375
29474
 
29376
29475
  // src/http-server.ts
29377
- var VERSION = true ? "0.32.0" : "0.0.0-dev";
29476
+ var VERSION = true ? "0.32.1" : "0.0.0-dev";
29378
29477
  var PORT = Number(process.env.PORT ?? 8080);
29379
29478
  var HOST = process.env.HOST ?? "0.0.0.0";
29380
29479
  var logger = {
@@ -1804,7 +1804,7 @@ var init_installer_gui = __esm({
1804
1804
  init_install_dxt();
1805
1805
  init_install_shared();
1806
1806
  init_oauth();
1807
- VERSION = true ? "0.32.0" : "0.0.0-dev";
1807
+ VERSION = true ? "0.32.1" : "0.0.0-dev";
1808
1808
  MESSAGES = {
1809
1809
  en: {
1810
1810
  installer: {
@@ -1067,7 +1067,7 @@ async function oauthLogin(opts) {
1067
1067
  }
1068
1068
 
1069
1069
  // installer/installer-gui.ts
1070
- var VERSION = true ? "0.32.0" : "0.0.0-dev";
1070
+ var VERSION = true ? "0.32.1" : "0.0.0-dev";
1071
1071
  var MESSAGES = {
1072
1072
  en: {
1073
1073
  installer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "mcpName": "io.github.leadbay/leadbay-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
6
6
  "type": "module",