@leadbay/mcp 0.32.5 → 0.32.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.32.6 — 2026-09-01
4
+
5
+ The 401 auto-retry is GET-only — replaying a write could double-execute a
6
+ mutation that already committed — but `mapErrorResponse` wrote its hint as if
7
+ `request()` had always retried. On `leadbay_create_topup_link` (a POST) the
8
+ agent was told the call had already been retried when it had been attempted
9
+ exactly once. Covers acceptance criteria 4 and 5 of product#3998; the 401 itself
10
+ is a backend bug fixed in leadbay/backend#1989.
11
+
12
+ - The GET-only rule is extracted into `retriesOn401` so the retry path and the
13
+ error mapper read one source of truth, and the actual outcome is threaded into
14
+ `mapErrorResponse` rather than assumed.
15
+ - The not-retried hint states only the fact and never the reason. A GET with
16
+ `retryOn401:false` (the startup auth probe) is not a write, and the earlier
17
+ text told it that it was. Why the retry didn't run is not actionable for the
18
+ agent — only that this was attempt one — so the flag stays a boolean.
19
+ - `readOnlyHint: false` on both Stripe tools. Each mints a Stripe session, and
20
+ for an org with no customer yet the shared `getStripeCustomer` path creates
21
+ the customer and persists `organizations.stripe_customer_id`.
22
+ - The two Stripe tools stay in `granularReadTools`. `readOnlyHint` (a client
23
+ confirmation hint) and `includeWrite` (a capability gate over the user's
24
+ Leadbay data) are different axes, and this repo already treats them that way
25
+ — `leadbay_preview_bulk_enrichment` sits in `granularWriteTools` with
26
+ `readOnlyHint: true`. Moving them would leave a `--no-write` user who hits
27
+ quota with no top-up link at all, which is the exact failure product#3998 was
28
+ filed for.
29
+
3
30
  ## 0.32.5 — 2026-09-01
4
31
 
5
32
  `leadbay_update_contact` returned `NOT_FOUND` / 404 on 100% of the calls ever
package/dist/bin.js CHANGED
@@ -213,12 +213,13 @@ function parseRetryAfter(value) {
213
213
  }
214
214
  return null;
215
215
  }
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;
216
+ var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, retriesOn401, ME_CACHE_TTL_MS, MAX_CONCURRENT, DEFAULT_REQUEST_TIMEOUT_MS, requestSignalStore, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
217
217
  var init_client = __esm({
218
218
  "../core/dist/client.js"() {
219
219
  "use strict";
220
220
  LENS_CACHE_TTL_MS = 5 * 60 * 1e3;
221
221
  TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
222
+ retriesOn401 = (method) => method.toUpperCase() === "GET";
222
223
  ME_CACHE_TTL_MS = 60 * 1e3;
223
224
  MAX_CONCURRENT = 5;
224
225
  DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
@@ -396,15 +397,16 @@ var init_client = __esm({
396
397
  // error envelope says so.
397
398
  //
398
399
  // Arrow-function field so `this` stays bound even when the method is passed
399
- // as a bare reference (see request()'s ternary). Retries are GET-ONLY: a 401
400
- // on a write (POST/PUT/DELETE) may arrive AFTER the mutation already committed
401
- // server-side, so blindly re-sending it would double-execute the write. Reads
402
- // are idempotent, so retrying them is safe. The 250ms backoff releases the
403
- // concurrency slot first (release → sleep → re-acquire) so a wave of 401s
404
- // doesn't pin all MAX_CONCURRENT slots in setTimeout and stall the queue.
400
+ // as a bare reference (see request()'s ternary). Retries are GET-ONLY (see
401
+ // retriesOn401): a 401 on a write (POST/PUT/DELETE) may arrive AFTER the
402
+ // mutation already committed server-side, so blindly re-sending it would
403
+ // double-execute the write. Reads are idempotent, so retrying them is safe.
404
+ // The 250ms backoff releases the concurrency slot first (release → sleep →
405
+ // re-acquire) so a wave of 401s doesn't pin all MAX_CONCURRENT slots in
406
+ // setTimeout and stall the queue.
405
407
  httpsRequestWithRetry = async (method, url, headers, body, timeoutMs) => {
406
408
  const res = await httpsRequest(method, url, headers, body, timeoutMs);
407
- if (res.status === 401 && method.toUpperCase() === "GET") {
409
+ if (res.status === 401 && retriesOn401(method)) {
408
410
  this.releaseSemaphore();
409
411
  try {
410
412
  await new Promise((r) => setTimeout(r, 250));
@@ -423,6 +425,7 @@ var init_client = __esm({
423
425
  throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y -p @leadbay/mcp@latest installer", path);
424
426
  }
425
427
  const retryOn401 = opts?.retryOn401 !== false;
428
+ const retriedOn401 = retryOn401 && retriesOn401(method);
426
429
  await this.acquireSemaphore();
427
430
  try {
428
431
  const url = `${this._baseUrl}${API_PREFIX}${path}`;
@@ -443,7 +446,7 @@ var init_client = __esm({
443
446
  return null;
444
447
  }
445
448
  if (res.status < 200 || res.status >= 300) {
446
- throw this.mapErrorResponse(res.status, res.body, path, res.headers);
449
+ throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
447
450
  }
448
451
  return JSON.parse(res.body);
449
452
  } catch (e) {
@@ -453,6 +456,7 @@ var init_client = __esm({
453
456
  }
454
457
  }
455
458
  async requestVoid(method, path, body) {
459
+ const retriedOn401 = retriesOn401(method);
456
460
  if (process.env.LEADBAY_MOCK === "1") {
457
461
  await this.mockRequest(method, path, body);
458
462
  return;
@@ -477,7 +481,7 @@ var init_client = __esm({
477
481
  retry_after: parseRetryAfter(res.headers["retry-after"])
478
482
  };
479
483
  if (res.status < 200 || res.status >= 300) {
480
- throw this.mapErrorResponse(res.status, res.body, path, res.headers);
484
+ throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
481
485
  }
482
486
  } catch (e) {
483
487
  throw this.mapTransportError(e, `${method} ${path}`);
@@ -491,6 +495,7 @@ var init_client = __esm({
491
495
  // mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
492
496
  // the wizard at POST /1.6/imports.
493
497
  async requestRawBinary(method, path, contentType, body) {
498
+ const retriedOn401 = retriesOn401(method);
494
499
  if (process.env.LEADBAY_MOCK === "1") {
495
500
  return this.mockRequestBinary(method, path, contentType, body);
496
501
  }
@@ -515,7 +520,7 @@ var init_client = __esm({
515
520
  return null;
516
521
  }
517
522
  if (res.status < 200 || res.status >= 300) {
518
- throw this.mapErrorResponse(res.status, res.body, path, res.headers);
523
+ throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
519
524
  }
520
525
  return JSON.parse(res.body);
521
526
  } catch (e) {
@@ -603,7 +608,7 @@ var init_client = __esm({
603
608
  }
604
609
  return envelope;
605
610
  }
606
- mapErrorResponse(status, rawBody, endpoint, headers) {
611
+ mapErrorResponse(status, rawBody, endpoint, headers, retried) {
607
612
  let parsed;
608
613
  try {
609
614
  parsed = JSON.parse(rawBody);
@@ -612,7 +617,7 @@ var init_client = __esm({
612
617
  }
613
618
  const retryAfter = parseRetryAfter(headers["retry-after"]);
614
619
  if (status === 401) {
615
- return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", "Leadbay tokens don't expire on a timer, so this isn't a stale token. A 401 here is usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly; if it persists, offer to report it to the team.", endpoint, null, status);
620
+ return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", retried ? "Tokens don't expire on a timer, so this isn't stale. Already auto-retried once and it 401'd again \u2014 usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly, else report it." : "Tokens don't expire on a timer, so this isn't stale. This call wasn't auto-retried, so it's the first attempt \u2014 a Leadbay-side hiccup, or the user logged out. Try again once, else report it.", endpoint, null, status);
616
621
  }
617
622
  if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
618
623
  const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
@@ -715,7 +720,7 @@ var init_client = __esm({
715
720
  try {
716
721
  const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0, opts?.timeoutMs);
717
722
  if (res.status < 200 || res.status >= 300) {
718
- throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers);
723
+ throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers, retriesOn401("GET"));
719
724
  }
720
725
  const me = JSON.parse(res.body);
721
726
  const observed = me.telemetry_enabled;
@@ -14661,8 +14666,10 @@ var init_create_topup_link = __esm({
14661
14666
  createTopupLink = {
14662
14667
  name: "leadbay_create_topup_link",
14663
14668
  annotations: {
14669
+ // Not read-only: this POSTs a new Stripe Checkout Session into existence.
14670
+ // Clients read readOnlyHint to decide whether to ask the user to confirm.
14664
14671
  title: "Generate Stripe checkout URL for AI-credits top-up",
14665
- readOnlyHint: true,
14672
+ readOnlyHint: false,
14666
14673
  destructiveHint: false,
14667
14674
  idempotentHint: false,
14668
14675
  openWorldHint: true
@@ -14695,8 +14702,12 @@ var init_open_billing_portal = __esm({
14695
14702
  openBillingPortal = {
14696
14703
  name: "leadbay_open_billing_portal",
14697
14704
  annotations: {
14705
+ // Not read-only, despite being a GET: the backend mints a Stripe portal
14706
+ // session, and for an org with no customer yet it also creates the Stripe
14707
+ // customer and persists organizations.stripe_customer_id. Same
14708
+ // getStripeCustomer path as leadbay_create_topup_link.
14698
14709
  title: "Generate Stripe customer-portal URL for subscription management",
14699
- readOnlyHint: true,
14710
+ readOnlyHint: false,
14700
14711
  destructiveHint: false,
14701
14712
  idempotentHint: false,
14702
14713
  openWorldHint: true
@@ -32972,7 +32983,7 @@ var OAUTH_BASE_URLS = {
32972
32983
  fr: "https://staging.api.leadbay.app"
32973
32984
  }
32974
32985
  };
32975
- var VERSION = "0.32.5";
32986
+ var VERSION = "0.32.6";
32976
32987
  var HELP = `
32977
32988
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
32978
32989
 
@@ -2797,6 +2797,7 @@ import { readdirSync, readFileSync, existsSync } from "fs";
2797
2797
  import { join } from "path";
2798
2798
  var LENS_CACHE_TTL_MS = 5 * 60 * 1e3;
2799
2799
  var TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
2800
+ var retriesOn401 = (method) => method.toUpperCase() === "GET";
2800
2801
  var ME_CACHE_TTL_MS = 60 * 1e3;
2801
2802
  var MAX_CONCURRENT = 5;
2802
2803
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
@@ -3154,15 +3155,16 @@ var LeadbayClient = class _LeadbayClient {
3154
3155
  // error envelope says so.
3155
3156
  //
3156
3157
  // Arrow-function field so `this` stays bound even when the method is passed
3157
- // as a bare reference (see request()'s ternary). Retries are GET-ONLY: a 401
3158
- // on a write (POST/PUT/DELETE) may arrive AFTER the mutation already committed
3159
- // server-side, so blindly re-sending it would double-execute the write. Reads
3160
- // are idempotent, so retrying them is safe. The 250ms backoff releases the
3161
- // concurrency slot first (release → sleep → re-acquire) so a wave of 401s
3162
- // doesn't pin all MAX_CONCURRENT slots in setTimeout and stall the queue.
3158
+ // as a bare reference (see request()'s ternary). Retries are GET-ONLY (see
3159
+ // retriesOn401): a 401 on a write (POST/PUT/DELETE) may arrive AFTER the
3160
+ // mutation already committed server-side, so blindly re-sending it would
3161
+ // double-execute the write. Reads are idempotent, so retrying them is safe.
3162
+ // The 250ms backoff releases the concurrency slot first (release → sleep →
3163
+ // re-acquire) so a wave of 401s doesn't pin all MAX_CONCURRENT slots in
3164
+ // setTimeout and stall the queue.
3163
3165
  httpsRequestWithRetry = async (method, url, headers, body, timeoutMs) => {
3164
3166
  const res = await httpsRequest(method, url, headers, body, timeoutMs);
3165
- if (res.status === 401 && method.toUpperCase() === "GET") {
3167
+ if (res.status === 401 && retriesOn401(method)) {
3166
3168
  this.releaseSemaphore();
3167
3169
  try {
3168
3170
  await new Promise((r) => setTimeout(r, 250));
@@ -3181,6 +3183,7 @@ var LeadbayClient = class _LeadbayClient {
3181
3183
  throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y -p @leadbay/mcp@latest installer", path);
3182
3184
  }
3183
3185
  const retryOn401 = opts?.retryOn401 !== false;
3186
+ const retriedOn401 = retryOn401 && retriesOn401(method);
3184
3187
  await this.acquireSemaphore();
3185
3188
  try {
3186
3189
  const url = `${this._baseUrl}${API_PREFIX}${path}`;
@@ -3201,7 +3204,7 @@ var LeadbayClient = class _LeadbayClient {
3201
3204
  return null;
3202
3205
  }
3203
3206
  if (res.status < 200 || res.status >= 300) {
3204
- throw this.mapErrorResponse(res.status, res.body, path, res.headers);
3207
+ throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
3205
3208
  }
3206
3209
  return JSON.parse(res.body);
3207
3210
  } catch (e) {
@@ -3211,6 +3214,7 @@ var LeadbayClient = class _LeadbayClient {
3211
3214
  }
3212
3215
  }
3213
3216
  async requestVoid(method, path, body) {
3217
+ const retriedOn401 = retriesOn401(method);
3214
3218
  if (process.env.LEADBAY_MOCK === "1") {
3215
3219
  await this.mockRequest(method, path, body);
3216
3220
  return;
@@ -3235,7 +3239,7 @@ var LeadbayClient = class _LeadbayClient {
3235
3239
  retry_after: parseRetryAfter(res.headers["retry-after"])
3236
3240
  };
3237
3241
  if (res.status < 200 || res.status >= 300) {
3238
- throw this.mapErrorResponse(res.status, res.body, path, res.headers);
3242
+ throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
3239
3243
  }
3240
3244
  } catch (e) {
3241
3245
  throw this.mapTransportError(e, `${method} ${path}`);
@@ -3249,6 +3253,7 @@ var LeadbayClient = class _LeadbayClient {
3249
3253
  // mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
3250
3254
  // the wizard at POST /1.6/imports.
3251
3255
  async requestRawBinary(method, path, contentType, body) {
3256
+ const retriedOn401 = retriesOn401(method);
3252
3257
  if (process.env.LEADBAY_MOCK === "1") {
3253
3258
  return this.mockRequestBinary(method, path, contentType, body);
3254
3259
  }
@@ -3273,7 +3278,7 @@ var LeadbayClient = class _LeadbayClient {
3273
3278
  return null;
3274
3279
  }
3275
3280
  if (res.status < 200 || res.status >= 300) {
3276
- throw this.mapErrorResponse(res.status, res.body, path, res.headers);
3281
+ throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
3277
3282
  }
3278
3283
  return JSON.parse(res.body);
3279
3284
  } catch (e) {
@@ -3361,7 +3366,7 @@ var LeadbayClient = class _LeadbayClient {
3361
3366
  }
3362
3367
  return envelope;
3363
3368
  }
3364
- mapErrorResponse(status, rawBody, endpoint, headers) {
3369
+ mapErrorResponse(status, rawBody, endpoint, headers, retried) {
3365
3370
  let parsed;
3366
3371
  try {
3367
3372
  parsed = JSON.parse(rawBody);
@@ -3370,7 +3375,7 @@ var LeadbayClient = class _LeadbayClient {
3370
3375
  }
3371
3376
  const retryAfter = parseRetryAfter(headers["retry-after"]);
3372
3377
  if (status === 401) {
3373
- return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", "Leadbay tokens don't expire on a timer, so this isn't a stale token. A 401 here is usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly; if it persists, offer to report it to the team.", endpoint, null, status);
3378
+ return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", retried ? "Tokens don't expire on a timer, so this isn't stale. Already auto-retried once and it 401'd again \u2014 usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly, else report it." : "Tokens don't expire on a timer, so this isn't stale. This call wasn't auto-retried, so it's the first attempt \u2014 a Leadbay-side hiccup, or the user logged out. Try again once, else report it.", endpoint, null, status);
3374
3379
  }
3375
3380
  if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
3376
3381
  const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
@@ -3473,7 +3478,7 @@ var LeadbayClient = class _LeadbayClient {
3473
3478
  try {
3474
3479
  const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0, opts?.timeoutMs);
3475
3480
  if (res.status < 200 || res.status >= 300) {
3476
- throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers);
3481
+ throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers, retriesOn401("GET"));
3477
3482
  }
3478
3483
  const me = JSON.parse(res.body);
3479
3484
  const observed = me.telemetry_enabled;
@@ -16830,8 +16835,10 @@ function coerceCsvValue(v) {
16830
16835
  var createTopupLink = {
16831
16836
  name: "leadbay_create_topup_link",
16832
16837
  annotations: {
16838
+ // Not read-only: this POSTs a new Stripe Checkout Session into existence.
16839
+ // Clients read readOnlyHint to decide whether to ask the user to confirm.
16833
16840
  title: "Generate Stripe checkout URL for AI-credits top-up",
16834
- readOnlyHint: true,
16841
+ readOnlyHint: false,
16835
16842
  destructiveHint: false,
16836
16843
  idempotentHint: false,
16837
16844
  openWorldHint: true
@@ -16857,8 +16864,12 @@ var createTopupLink = {
16857
16864
  var openBillingPortal = {
16858
16865
  name: "leadbay_open_billing_portal",
16859
16866
  annotations: {
16867
+ // Not read-only, despite being a GET: the backend mints a Stripe portal
16868
+ // session, and for an org with no customer yet it also creates the Stripe
16869
+ // customer and persists organizations.stripe_customer_id. Same
16870
+ // getStripeCustomer path as leadbay_create_topup_link.
16860
16871
  title: "Generate Stripe customer-portal URL for subscription management",
16861
- readOnlyHint: true,
16872
+ readOnlyHint: false,
16862
16873
  destructiveHint: false,
16863
16874
  idempotentHint: false,
16864
16875
  openWorldHint: true
@@ -29558,7 +29569,7 @@ function parseWriteEnv(env = process.env) {
29558
29569
  }
29559
29570
 
29560
29571
  // src/http-server.ts
29561
- var VERSION = true ? "0.32.5" : "0.0.0-dev";
29572
+ var VERSION = true ? "0.32.6" : "0.0.0-dev";
29562
29573
  var PORT = Number(process.env.PORT ?? 8080);
29563
29574
  var HOST = process.env.HOST ?? "0.0.0.0";
29564
29575
  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.5" : "0.0.0-dev";
1807
+ VERSION = true ? "0.32.6" : "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.5" : "0.0.0-dev";
1070
+ var VERSION = true ? "0.32.6" : "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.5",
3
+ "version": "0.32.6",
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",