@leadbay/mcp 0.19.3 → 0.20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.20.0 — 2026-06-15
4
+
5
+ - **Proactive update proposal on a fresh session** (product#3742): the auto-update check already ran at boot, but the resulting proposal only reached the user if the agent happened to call `leadbay_account_status` — which a fresh session rarely does, so the "newer version available" prompt was effectively invisible. The cached `update_available` block now also rides along on `_meta.update_available` of the **first ordinary tool result** of a session while an upgrade is pending, gated once-per-version so it surfaces exactly once. `leadbay_account_status` keeps carrying it as a top-level field. The server-instruction paragraph now tells the agent to surface the `ask_user_input_v0` prompt whenever it sees the field on *any* response.
6
+ - **Installer asset is now `.dxt`, not `.mcpb`**: the release-asset picker prefers the `.dxt` bundle (falling back to `.mcpb` only when a release ships no `.dxt`). The field is renamed `mcpb_url` → `install_url` across `update_available`, the `leadbay_acknowledge_update` result, and the persisted update-state — with forward-migration of the legacy `latest_known_mcpb_url` key so existing users don't lose their cache.
7
+
3
8
  ## 0.19.3 — 2026-06-15
4
9
 
5
10
  - **New tool `leadbay_send_feedback`**: delivers a user-authored message to the same destination as the web app's "Send feedback" form — the team's Sentry feedback inbox (the website form calls `Sentry.captureFeedback`; there is no Leadbay API endpoint, so the MCP reuses its already-initialized `@sentry/node`). User-initiated ("send feedback / report a bug / tell Leadbay…"), or offered on a tool error and sent only on explicit yes. Distinct from the silent, agent-detected, PostHog-only `leadbay_report_friction`: feedback is explicit, user-authored, and reaches the team's inbox. Honest delivery — if the Sentry transport isn't available it returns `sent:false`, never a false success; Sentry is flushed after capture so the event actually ships; identity is attached when it resolves (anonymous fallback rather than dropping the message). Write-gated (`LEADBAY_MCP_WRITE=1`) since it sends data outward.
package/dist/bin.js CHANGED
@@ -16524,16 +16524,16 @@ var init_account_status = __esm({
16524
16524
  properties: {
16525
16525
  current_version: { type: "string" },
16526
16526
  latest_version: { type: "string" },
16527
- mcpb_url: {
16527
+ install_url: {
16528
16528
  type: "string",
16529
- description: "Direct download URL for the .mcpb installer asset."
16529
+ description: "Direct download URL for the installer asset (.dxt, falling back to .mcpb)."
16530
16530
  },
16531
16531
  release_url: {
16532
16532
  type: "string",
16533
16533
  description: "GitHub release page (changelog)."
16534
16534
  }
16535
16535
  },
16536
- required: ["current_version", "latest_version", "mcpb_url", "release_url"]
16536
+ required: ["current_version", "latest_version", "install_url", "release_url"]
16537
16537
  }
16538
16538
  },
16539
16539
  required: ["user", "organization"]
@@ -23584,10 +23584,13 @@ function initTelemetry(opts) {
23584
23584
 
23585
23585
  // src/update-check.ts
23586
23586
  var cachedInfo = null;
23587
- var checkInFlight = false;
23587
+ var inFlightCheck = null;
23588
23588
  function getCachedUpdateInfo() {
23589
23589
  return cachedInfo;
23590
23590
  }
23591
+ function getInFlightCheck() {
23592
+ return inFlightCheck;
23593
+ }
23591
23594
  var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
23592
23595
  var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
23593
23596
  var FETCH_TIMEOUT_MS = 5e3;
@@ -23632,25 +23635,24 @@ function compareSemver(a, b) {
23632
23635
  }
23633
23636
  return 0;
23634
23637
  }
23635
- function pickMcpbAsset(rel) {
23638
+ function pickInstallAsset(rel) {
23636
23639
  if (!Array.isArray(rel.assets)) return void 0;
23637
- const mcpb = rel.assets.find(
23638
- (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
23639
- );
23640
- if (mcpb?.browser_download_url) return mcpb.browser_download_url;
23641
23640
  const dxt = rel.assets.find(
23642
23641
  (a) => typeof a.name === "string" && a.name.endsWith(".dxt")
23643
23642
  );
23644
- return dxt?.browser_download_url;
23643
+ if (dxt?.browser_download_url) return dxt.browser_download_url;
23644
+ const mcpb = rel.assets.find(
23645
+ (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
23646
+ );
23647
+ return mcpb?.browser_download_url;
23645
23648
  }
23646
- async function checkForUpdate(opts) {
23647
- if (checkInFlight) return cachedInfo;
23648
- checkInFlight = true;
23649
- try {
23650
- return await doCheck(opts);
23651
- } finally {
23652
- checkInFlight = false;
23653
- }
23649
+ function checkForUpdate(opts) {
23650
+ if (inFlightCheck) return inFlightCheck;
23651
+ const p = doCheck(opts).finally(() => {
23652
+ if (inFlightCheck === p) inFlightCheck = null;
23653
+ });
23654
+ inFlightCheck = p;
23655
+ return p;
23654
23656
  }
23655
23657
  async function doCheck(opts) {
23656
23658
  const now = opts.now ?? Date.now;
@@ -23659,11 +23661,11 @@ async function doCheck(opts) {
23659
23661
  const currentVersion = opts.currentVersion;
23660
23662
  const state = await opts.stateStore.read();
23661
23663
  const within = now() - state.last_check_time < CHECK_THROTTLE_MS;
23662
- if (!opts.force && within && state.latest_known_version && state.latest_known_mcpb_url && state.latest_known_release_url) {
23664
+ if (!opts.force && within && state.latest_known_version && state.latest_known_install_url && state.latest_known_release_url) {
23663
23665
  const cached = buildInfoIfUpgrade(
23664
23666
  currentVersion,
23665
23667
  state.latest_known_version,
23666
- state.latest_known_mcpb_url,
23668
+ state.latest_known_install_url,
23667
23669
  state.latest_known_release_url,
23668
23670
  state.suppressed_versions,
23669
23671
  state.remind_until,
@@ -23717,18 +23719,18 @@ async function doCheck(opts) {
23717
23719
  return null;
23718
23720
  }
23719
23721
  let latestVersion;
23720
- let mcpbUrl;
23722
+ let installUrl;
23721
23723
  let releaseUrl;
23722
23724
  if (status === 200 && body) {
23723
23725
  const parsed = body.tag_name ? parseTagName(body.tag_name) : null;
23724
23726
  if (parsed) {
23725
23727
  latestVersion = parsed;
23726
- mcpbUrl = pickMcpbAsset(body);
23728
+ installUrl = pickInstallAsset(body);
23727
23729
  releaseUrl = body.html_url;
23728
23730
  }
23729
23731
  } else {
23730
23732
  latestVersion = state.latest_known_version;
23731
- mcpbUrl = state.latest_known_mcpb_url;
23733
+ installUrl = state.latest_known_install_url;
23732
23734
  releaseUrl = state.latest_known_release_url;
23733
23735
  }
23734
23736
  const persisted = await opts.stateStore.update((cur) => ({
@@ -23736,7 +23738,7 @@ async function doCheck(opts) {
23736
23738
  last_check_time: now(),
23737
23739
  etag: nextEtag,
23738
23740
  latest_known_version: latestVersion ?? cur.latest_known_version,
23739
- latest_known_mcpb_url: mcpbUrl ?? cur.latest_known_mcpb_url,
23741
+ latest_known_install_url: installUrl ?? cur.latest_known_install_url,
23740
23742
  latest_known_release_url: releaseUrl ?? cur.latest_known_release_url
23741
23743
  }));
23742
23744
  opts.telemetry.captureUpdateCheck?.({
@@ -23746,7 +23748,7 @@ async function doCheck(opts) {
23746
23748
  const info = buildInfoIfUpgrade(
23747
23749
  currentVersion,
23748
23750
  persisted.latest_known_version,
23749
- persisted.latest_known_mcpb_url,
23751
+ persisted.latest_known_install_url,
23750
23752
  persisted.latest_known_release_url,
23751
23753
  persisted.suppressed_versions,
23752
23754
  persisted.remind_until,
@@ -23755,15 +23757,15 @@ async function doCheck(opts) {
23755
23757
  cachedInfo = info;
23756
23758
  return info;
23757
23759
  }
23758
- function buildInfoIfUpgrade(currentVersion, latestVersion, mcpbUrl, releaseUrl, suppressed, remindUntil, nowMs) {
23759
- if (!latestVersion || !mcpbUrl || !releaseUrl) return null;
23760
+ function buildInfoIfUpgrade(currentVersion, latestVersion, installUrl, releaseUrl, suppressed, remindUntil, nowMs) {
23761
+ if (!latestVersion || !installUrl || !releaseUrl) return null;
23760
23762
  if (compareSemver(latestVersion, currentVersion) <= 0) return null;
23761
23763
  if (suppressed.includes(latestVersion)) return null;
23762
23764
  if (remindUntil && remindUntil > nowMs) return null;
23763
23765
  return {
23764
23766
  current_version: currentVersion,
23765
23767
  latest_version: latestVersion,
23766
- mcpb_url: mcpbUrl,
23768
+ install_url: installUrl,
23767
23769
  release_url: releaseUrl
23768
23770
  };
23769
23771
  }
@@ -23792,7 +23794,7 @@ async function recordRunningVersion(currentVersion, stateStore, telemetry) {
23792
23794
 
23793
23795
  // src/update-tool.ts
23794
23796
  var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
23795
- var DESCRIPTION = "Record the user's choice on an update prompt surfaced via `update_available` on leadbay_account_status. Pass `action: 'install' | 'remind_tomorrow' | 'skip'` and `version` (the `latest_version` from the prompt). On 'install', the server returns `{ mcpb_url, release_url }` \u2014 show the user a clickable link to mcpb_url so Claude Desktop's native installer opens it. On 'remind_tomorrow' the server suppresses the prompt for 24 hours. On 'skip' the version is suppressed permanently. Call this tool EXACTLY ONCE per prompt \u2014 do not loop, and do not call it speculatively when no update_available block is present.";
23797
+ var DESCRIPTION = "Record the user's choice on an update prompt surfaced via `update_available` on leadbay_account_status. Pass `action: 'install' | 'remind_tomorrow' | 'skip'` and `version` (the `latest_version` from the prompt). On 'install', the server returns `{ install_url, release_url }` \u2014 show the user a clickable link to install_url (a `.dxt` bundle) so Claude Desktop's native installer opens it. On 'remind_tomorrow' the server suppresses the prompt for 24 hours. On 'skip' the version is suppressed permanently. Call this tool EXACTLY ONCE per prompt \u2014 do not loop, and do not call it speculatively when no update_available block is present.";
23796
23798
  function buildAcknowledgeUpdateTool(opts) {
23797
23799
  const now = opts.now ?? Date.now;
23798
23800
  return {
@@ -23828,7 +23830,7 @@ function buildAcknowledgeUpdateTool(opts) {
23828
23830
  action: { type: "string" },
23829
23831
  version: { type: "string" },
23830
23832
  message: { type: "string" },
23831
- mcpb_url: { type: ["string", "null"] },
23833
+ install_url: { type: ["string", "null"] },
23832
23834
  release_url: { type: ["string", "null"] }
23833
23835
  },
23834
23836
  required: ["ok", "action", "version", "message"]
@@ -23862,9 +23864,9 @@ function buildAcknowledgeUpdateTool(opts) {
23862
23864
  ok: true,
23863
23865
  action,
23864
23866
  version,
23865
- mcpb_url: state.latest_known_mcpb_url ?? null,
23867
+ install_url: state.latest_known_install_url ?? null,
23866
23868
  release_url: state.latest_known_release_url ?? null,
23867
- message: state.latest_known_mcpb_url ? "Show the user the mcpb_url as a clickable link \u2014 opening it in Claude Desktop runs the native installer." : "No .mcpb URL is cached. Direct the user to the release_url to download manually."
23869
+ message: state.latest_known_install_url ? "Show the user the install_url (a .dxt bundle) as a clickable link \u2014 opening it in Claude Desktop runs the native installer." : "No installer URL is cached. Direct the user to the release_url to download manually."
23868
23870
  };
23869
23871
  }
23870
23872
  if (action === "remind_tomorrow") {
@@ -23967,7 +23969,7 @@ function buildStartHereParagraph(has) {
23967
23969
  }
23968
23970
  function buildUpdateAvailableParagraph(has) {
23969
23971
  if (!has("leadbay_acknowledge_update")) return null;
23970
- return "MCP auto-update: when `leadbay_account_status` returns an `update_available` field (`{ current_version, latest_version, mcpb_url, release_url }`), a newer MCP server release is published and the user has NOT suppressed it. Surface a prompt via `ask_user_input_v0` with EXACTLY these three options: \"Install now\", \"Remind me tomorrow\", \"Skip this version\". Map the user's choice to `leadbay_acknowledge_update({ action: 'install' | 'remind_tomorrow' | 'skip', version: latest_version })`. On 'install', the tool returns `mcpb_url` \u2014 render it as a clickable markdown link the user can open in Claude Desktop (the .mcpb extension triggers the native installer). The user does NOT need to restart anything before clicking \u2014 the new server takes effect on the next MCP session. Prompt the user ONCE per session per version \u2014 don't re-prompt within the same chat after they've acknowledged.";
23972
+ return "MCP auto-update: an `update_available` field (`{ current_version, latest_version, install_url, release_url }`) means a newer MCP server release is published and the user has NOT suppressed it. It appears in TWO places: as a top-level field on `leadbay_account_status`, AND on `_meta.update_available` of the FIRST other tool result in a session while an update is pending (so a fresh session surfaces the proposal even without an account_status call). Whenever you see it on ANY response, surface a prompt via `ask_user_input_v0` with EXACTLY these three options: \"Install now\", \"Remind me tomorrow\", \"Skip this version\". Map the user's choice to `leadbay_acknowledge_update({ action: 'install' | 'remind_tomorrow' | 'skip', version: latest_version })`. On 'install', the tool returns `install_url` \u2014 render it as a clickable markdown link the user can open in Claude Desktop (the .dxt extension triggers the native installer). The user does NOT need to restart anything before clicking \u2014 the new server takes effect on the next MCP session. Prompt the user ONCE per session per version \u2014 don't re-prompt within the same chat after they've acknowledged.";
23971
23973
  }
23972
23974
  function buildRhythmParagraph(has) {
23973
23975
  if (has("leadbay_report_outreach")) {
@@ -24262,16 +24264,47 @@ function buildServer(client, opts = {}) {
24262
24264
  );
24263
24265
  });
24264
24266
  };
24265
- const maybeAttachUpdate = (toolName, result) => {
24266
- if (toolName !== "leadbay_account_status") return;
24267
+ const UPDATE_SURFACE_WAIT_MS = 1500;
24268
+ const maybeAttachUpdate = async (toolName, result) => {
24267
24269
  if (!opts.updateStateStore) return;
24268
24270
  if (result === null || typeof result !== "object" || Array.isArray(result)) {
24269
24271
  return;
24270
24272
  }
24271
- const info = getCachedUpdateInfo();
24273
+ if (result.error === true) {
24274
+ return;
24275
+ }
24276
+ let info = getCachedUpdateInfo();
24277
+ if (!info) {
24278
+ const inflight = getInFlightCheck();
24279
+ if (inflight) {
24280
+ const settled = inflight.catch((err) => {
24281
+ opts.logger?.warn?.(
24282
+ `update_check.surface_await_failed ${err?.message ?? err}`
24283
+ );
24284
+ return null;
24285
+ });
24286
+ info = await Promise.race([
24287
+ settled,
24288
+ new Promise(
24289
+ (resolve) => setTimeout(() => resolve(null), UPDATE_SURFACE_WAIT_MS)
24290
+ )
24291
+ ]);
24292
+ info = getCachedUpdateInfo() ?? info;
24293
+ }
24294
+ }
24272
24295
  if (!info) return;
24273
- result.update_available = info;
24274
- if (!promptedVersionsThisSession.has(info.latest_version)) {
24296
+ const isAccountStatus = toolName === "leadbay_account_status";
24297
+ const alreadyPrompted = promptedVersionsThisSession.has(info.latest_version);
24298
+ if (!isAccountStatus && alreadyPrompted) return;
24299
+ if (isAccountStatus) {
24300
+ result.update_available = info;
24301
+ } else {
24302
+ const envelope = result;
24303
+ const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
24304
+ const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
24305
+ target._meta = { ...existingMeta, update_available: info };
24306
+ }
24307
+ if (!alreadyPrompted) {
24275
24308
  promptedVersionsThisSession.add(info.latest_version);
24276
24309
  telemetry.captureUpdatePrompted?.({
24277
24310
  current_version: serverVersion,
@@ -24451,7 +24484,7 @@ function buildServer(client, opts = {}) {
24451
24484
  // tool reports honestly when telemetry is off.
24452
24485
  sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
24453
24486
  });
24454
- maybeAttachUpdate(name, result);
24487
+ await maybeAttachUpdate(name, result);
24455
24488
  maybeAttachNotifications(result);
24456
24489
  if (result && typeof result === "object" && result.error === true) {
24457
24490
  const envText = formatErrorForLLM(result);
@@ -25461,8 +25494,10 @@ var UpdateStateStore = class {
25461
25494
  if (typeof r.latest_known_version === "string") {
25462
25495
  out.latest_known_version = r.latest_known_version;
25463
25496
  }
25464
- if (typeof r.latest_known_mcpb_url === "string") {
25465
- out.latest_known_mcpb_url = r.latest_known_mcpb_url;
25497
+ if (typeof r.latest_known_install_url === "string") {
25498
+ out.latest_known_install_url = r.latest_known_install_url;
25499
+ } else if (typeof r.latest_known_mcpb_url === "string") {
25500
+ out.latest_known_install_url = r.latest_known_mcpb_url;
25466
25501
  }
25467
25502
  if (typeof r.latest_known_release_url === "string") {
25468
25503
  out.latest_known_release_url = r.latest_known_release_url;
@@ -25890,7 +25925,7 @@ var OAUTH_BASE_URLS = {
25890
25925
  fr: "https://staging.api.leadbay.app"
25891
25926
  }
25892
25927
  };
25893
- var VERSION = "0.19.3";
25928
+ var VERSION = "0.20.0";
25894
25929
  var HELP = `
25895
25930
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
25896
25931
 
@@ -16779,16 +16779,16 @@ var accountStatus = {
16779
16779
  properties: {
16780
16780
  current_version: { type: "string" },
16781
16781
  latest_version: { type: "string" },
16782
- mcpb_url: {
16782
+ install_url: {
16783
16783
  type: "string",
16784
- description: "Direct download URL for the .mcpb installer asset."
16784
+ description: "Direct download URL for the installer asset (.dxt, falling back to .mcpb)."
16785
16785
  },
16786
16786
  release_url: {
16787
16787
  type: "string",
16788
16788
  description: "GitHub release page (changelog)."
16789
16789
  }
16790
16790
  },
16791
- required: ["current_version", "latest_version", "mcpb_url", "release_url"]
16791
+ required: ["current_version", "latest_version", "install_url", "release_url"]
16792
16792
  }
16793
16793
  },
16794
16794
  required: ["user", "organization"]
@@ -21262,10 +21262,13 @@ var NOOP_TELEMETRY = {
21262
21262
 
21263
21263
  // src/update-check.ts
21264
21264
  var cachedInfo = null;
21265
- var checkInFlight = false;
21265
+ var inFlightCheck = null;
21266
21266
  function getCachedUpdateInfo() {
21267
21267
  return cachedInfo;
21268
21268
  }
21269
+ function getInFlightCheck() {
21270
+ return inFlightCheck;
21271
+ }
21269
21272
  var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
21270
21273
  var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
21271
21274
  var FETCH_TIMEOUT_MS = 5e3;
@@ -21310,25 +21313,24 @@ function compareSemver(a, b) {
21310
21313
  }
21311
21314
  return 0;
21312
21315
  }
21313
- function pickMcpbAsset(rel) {
21316
+ function pickInstallAsset(rel) {
21314
21317
  if (!Array.isArray(rel.assets)) return void 0;
21315
- const mcpb = rel.assets.find(
21316
- (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
21317
- );
21318
- if (mcpb?.browser_download_url) return mcpb.browser_download_url;
21319
21318
  const dxt = rel.assets.find(
21320
21319
  (a) => typeof a.name === "string" && a.name.endsWith(".dxt")
21321
21320
  );
21322
- return dxt?.browser_download_url;
21321
+ if (dxt?.browser_download_url) return dxt.browser_download_url;
21322
+ const mcpb = rel.assets.find(
21323
+ (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
21324
+ );
21325
+ return mcpb?.browser_download_url;
21323
21326
  }
21324
- async function checkForUpdate(opts) {
21325
- if (checkInFlight) return cachedInfo;
21326
- checkInFlight = true;
21327
- try {
21328
- return await doCheck(opts);
21329
- } finally {
21330
- checkInFlight = false;
21331
- }
21327
+ function checkForUpdate(opts) {
21328
+ if (inFlightCheck) return inFlightCheck;
21329
+ const p = doCheck(opts).finally(() => {
21330
+ if (inFlightCheck === p) inFlightCheck = null;
21331
+ });
21332
+ inFlightCheck = p;
21333
+ return p;
21332
21334
  }
21333
21335
  async function doCheck(opts) {
21334
21336
  const now = opts.now ?? Date.now;
@@ -21337,11 +21339,11 @@ async function doCheck(opts) {
21337
21339
  const currentVersion = opts.currentVersion;
21338
21340
  const state = await opts.stateStore.read();
21339
21341
  const within = now() - state.last_check_time < CHECK_THROTTLE_MS;
21340
- if (!opts.force && within && state.latest_known_version && state.latest_known_mcpb_url && state.latest_known_release_url) {
21342
+ if (!opts.force && within && state.latest_known_version && state.latest_known_install_url && state.latest_known_release_url) {
21341
21343
  const cached = buildInfoIfUpgrade(
21342
21344
  currentVersion,
21343
21345
  state.latest_known_version,
21344
- state.latest_known_mcpb_url,
21346
+ state.latest_known_install_url,
21345
21347
  state.latest_known_release_url,
21346
21348
  state.suppressed_versions,
21347
21349
  state.remind_until,
@@ -21395,18 +21397,18 @@ async function doCheck(opts) {
21395
21397
  return null;
21396
21398
  }
21397
21399
  let latestVersion;
21398
- let mcpbUrl;
21400
+ let installUrl;
21399
21401
  let releaseUrl;
21400
21402
  if (status === 200 && body) {
21401
21403
  const parsed = body.tag_name ? parseTagName(body.tag_name) : null;
21402
21404
  if (parsed) {
21403
21405
  latestVersion = parsed;
21404
- mcpbUrl = pickMcpbAsset(body);
21406
+ installUrl = pickInstallAsset(body);
21405
21407
  releaseUrl = body.html_url;
21406
21408
  }
21407
21409
  } else {
21408
21410
  latestVersion = state.latest_known_version;
21409
- mcpbUrl = state.latest_known_mcpb_url;
21411
+ installUrl = state.latest_known_install_url;
21410
21412
  releaseUrl = state.latest_known_release_url;
21411
21413
  }
21412
21414
  const persisted = await opts.stateStore.update((cur) => ({
@@ -21414,7 +21416,7 @@ async function doCheck(opts) {
21414
21416
  last_check_time: now(),
21415
21417
  etag: nextEtag,
21416
21418
  latest_known_version: latestVersion ?? cur.latest_known_version,
21417
- latest_known_mcpb_url: mcpbUrl ?? cur.latest_known_mcpb_url,
21419
+ latest_known_install_url: installUrl ?? cur.latest_known_install_url,
21418
21420
  latest_known_release_url: releaseUrl ?? cur.latest_known_release_url
21419
21421
  }));
21420
21422
  opts.telemetry.captureUpdateCheck?.({
@@ -21424,7 +21426,7 @@ async function doCheck(opts) {
21424
21426
  const info = buildInfoIfUpgrade(
21425
21427
  currentVersion,
21426
21428
  persisted.latest_known_version,
21427
- persisted.latest_known_mcpb_url,
21429
+ persisted.latest_known_install_url,
21428
21430
  persisted.latest_known_release_url,
21429
21431
  persisted.suppressed_versions,
21430
21432
  persisted.remind_until,
@@ -21433,22 +21435,22 @@ async function doCheck(opts) {
21433
21435
  cachedInfo = info;
21434
21436
  return info;
21435
21437
  }
21436
- function buildInfoIfUpgrade(currentVersion, latestVersion, mcpbUrl, releaseUrl, suppressed, remindUntil, nowMs) {
21437
- if (!latestVersion || !mcpbUrl || !releaseUrl) return null;
21438
+ function buildInfoIfUpgrade(currentVersion, latestVersion, installUrl, releaseUrl, suppressed, remindUntil, nowMs) {
21439
+ if (!latestVersion || !installUrl || !releaseUrl) return null;
21438
21440
  if (compareSemver(latestVersion, currentVersion) <= 0) return null;
21439
21441
  if (suppressed.includes(latestVersion)) return null;
21440
21442
  if (remindUntil && remindUntil > nowMs) return null;
21441
21443
  return {
21442
21444
  current_version: currentVersion,
21443
21445
  latest_version: latestVersion,
21444
- mcpb_url: mcpbUrl,
21446
+ install_url: installUrl,
21445
21447
  release_url: releaseUrl
21446
21448
  };
21447
21449
  }
21448
21450
 
21449
21451
  // src/update-tool.ts
21450
21452
  var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
21451
- var DESCRIPTION = "Record the user's choice on an update prompt surfaced via `update_available` on leadbay_account_status. Pass `action: 'install' | 'remind_tomorrow' | 'skip'` and `version` (the `latest_version` from the prompt). On 'install', the server returns `{ mcpb_url, release_url }` \u2014 show the user a clickable link to mcpb_url so Claude Desktop's native installer opens it. On 'remind_tomorrow' the server suppresses the prompt for 24 hours. On 'skip' the version is suppressed permanently. Call this tool EXACTLY ONCE per prompt \u2014 do not loop, and do not call it speculatively when no update_available block is present.";
21453
+ var DESCRIPTION = "Record the user's choice on an update prompt surfaced via `update_available` on leadbay_account_status. Pass `action: 'install' | 'remind_tomorrow' | 'skip'` and `version` (the `latest_version` from the prompt). On 'install', the server returns `{ install_url, release_url }` \u2014 show the user a clickable link to install_url (a `.dxt` bundle) so Claude Desktop's native installer opens it. On 'remind_tomorrow' the server suppresses the prompt for 24 hours. On 'skip' the version is suppressed permanently. Call this tool EXACTLY ONCE per prompt \u2014 do not loop, and do not call it speculatively when no update_available block is present.";
21452
21454
  function buildAcknowledgeUpdateTool(opts) {
21453
21455
  const now = opts.now ?? Date.now;
21454
21456
  return {
@@ -21484,7 +21486,7 @@ function buildAcknowledgeUpdateTool(opts) {
21484
21486
  action: { type: "string" },
21485
21487
  version: { type: "string" },
21486
21488
  message: { type: "string" },
21487
- mcpb_url: { type: ["string", "null"] },
21489
+ install_url: { type: ["string", "null"] },
21488
21490
  release_url: { type: ["string", "null"] }
21489
21491
  },
21490
21492
  required: ["ok", "action", "version", "message"]
@@ -21518,9 +21520,9 @@ function buildAcknowledgeUpdateTool(opts) {
21518
21520
  ok: true,
21519
21521
  action,
21520
21522
  version,
21521
- mcpb_url: state.latest_known_mcpb_url ?? null,
21523
+ install_url: state.latest_known_install_url ?? null,
21522
21524
  release_url: state.latest_known_release_url ?? null,
21523
- message: state.latest_known_mcpb_url ? "Show the user the mcpb_url as a clickable link \u2014 opening it in Claude Desktop runs the native installer." : "No .mcpb URL is cached. Direct the user to the release_url to download manually."
21525
+ message: state.latest_known_install_url ? "Show the user the install_url (a .dxt bundle) as a clickable link \u2014 opening it in Claude Desktop runs the native installer." : "No installer URL is cached. Direct the user to the release_url to download manually."
21524
21526
  };
21525
21527
  }
21526
21528
  if (action === "remind_tomorrow") {
@@ -21623,7 +21625,7 @@ function buildStartHereParagraph(has) {
21623
21625
  }
21624
21626
  function buildUpdateAvailableParagraph(has) {
21625
21627
  if (!has("leadbay_acknowledge_update")) return null;
21626
- return "MCP auto-update: when `leadbay_account_status` returns an `update_available` field (`{ current_version, latest_version, mcpb_url, release_url }`), a newer MCP server release is published and the user has NOT suppressed it. Surface a prompt via `ask_user_input_v0` with EXACTLY these three options: \"Install now\", \"Remind me tomorrow\", \"Skip this version\". Map the user's choice to `leadbay_acknowledge_update({ action: 'install' | 'remind_tomorrow' | 'skip', version: latest_version })`. On 'install', the tool returns `mcpb_url` \u2014 render it as a clickable markdown link the user can open in Claude Desktop (the .mcpb extension triggers the native installer). The user does NOT need to restart anything before clicking \u2014 the new server takes effect on the next MCP session. Prompt the user ONCE per session per version \u2014 don't re-prompt within the same chat after they've acknowledged.";
21628
+ return "MCP auto-update: an `update_available` field (`{ current_version, latest_version, install_url, release_url }`) means a newer MCP server release is published and the user has NOT suppressed it. It appears in TWO places: as a top-level field on `leadbay_account_status`, AND on `_meta.update_available` of the FIRST other tool result in a session while an update is pending (so a fresh session surfaces the proposal even without an account_status call). Whenever you see it on ANY response, surface a prompt via `ask_user_input_v0` with EXACTLY these three options: \"Install now\", \"Remind me tomorrow\", \"Skip this version\". Map the user's choice to `leadbay_acknowledge_update({ action: 'install' | 'remind_tomorrow' | 'skip', version: latest_version })`. On 'install', the tool returns `install_url` \u2014 render it as a clickable markdown link the user can open in Claude Desktop (the .dxt extension triggers the native installer). The user does NOT need to restart anything before clicking \u2014 the new server takes effect on the next MCP session. Prompt the user ONCE per session per version \u2014 don't re-prompt within the same chat after they've acknowledged.";
21627
21629
  }
21628
21630
  function buildRhythmParagraph(has) {
21629
21631
  if (has("leadbay_report_outreach")) {
@@ -21918,16 +21920,47 @@ function buildServer(client, opts = {}) {
21918
21920
  );
21919
21921
  });
21920
21922
  };
21921
- const maybeAttachUpdate = (toolName, result) => {
21922
- if (toolName !== "leadbay_account_status") return;
21923
+ const UPDATE_SURFACE_WAIT_MS = 1500;
21924
+ const maybeAttachUpdate = async (toolName, result) => {
21923
21925
  if (!opts.updateStateStore) return;
21924
21926
  if (result === null || typeof result !== "object" || Array.isArray(result)) {
21925
21927
  return;
21926
21928
  }
21927
- const info = getCachedUpdateInfo();
21929
+ if (result.error === true) {
21930
+ return;
21931
+ }
21932
+ let info = getCachedUpdateInfo();
21933
+ if (!info) {
21934
+ const inflight = getInFlightCheck();
21935
+ if (inflight) {
21936
+ const settled = inflight.catch((err) => {
21937
+ opts.logger?.warn?.(
21938
+ `update_check.surface_await_failed ${err?.message ?? err}`
21939
+ );
21940
+ return null;
21941
+ });
21942
+ info = await Promise.race([
21943
+ settled,
21944
+ new Promise(
21945
+ (resolve) => setTimeout(() => resolve(null), UPDATE_SURFACE_WAIT_MS)
21946
+ )
21947
+ ]);
21948
+ info = getCachedUpdateInfo() ?? info;
21949
+ }
21950
+ }
21928
21951
  if (!info) return;
21929
- result.update_available = info;
21930
- if (!promptedVersionsThisSession.has(info.latest_version)) {
21952
+ const isAccountStatus = toolName === "leadbay_account_status";
21953
+ const alreadyPrompted = promptedVersionsThisSession.has(info.latest_version);
21954
+ if (!isAccountStatus && alreadyPrompted) return;
21955
+ if (isAccountStatus) {
21956
+ result.update_available = info;
21957
+ } else {
21958
+ const envelope = result;
21959
+ const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
21960
+ const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
21961
+ target._meta = { ...existingMeta, update_available: info };
21962
+ }
21963
+ if (!alreadyPrompted) {
21931
21964
  promptedVersionsThisSession.add(info.latest_version);
21932
21965
  telemetry.captureUpdatePrompted?.({
21933
21966
  current_version: serverVersion,
@@ -22107,7 +22140,7 @@ function buildServer(client, opts = {}) {
22107
22140
  // tool reports honestly when telemetry is off.
22108
22141
  sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
22109
22142
  });
22110
- maybeAttachUpdate(name, result);
22143
+ await maybeAttachUpdate(name, result);
22111
22144
  maybeAttachNotifications(result);
22112
22145
  if (result && typeof result === "object" && result.error === true) {
22113
22146
  const envText = formatErrorForLLM(result);
@@ -22403,7 +22436,7 @@ function parseWriteEnv(env = process.env) {
22403
22436
  }
22404
22437
 
22405
22438
  // src/http-server.ts
22406
- var VERSION = true ? "0.19.3" : "0.0.0-dev";
22439
+ var VERSION = true ? "0.20.0" : "0.0.0-dev";
22407
22440
  var PORT = Number(process.env.PORT ?? 8080);
22408
22441
  var HOST = process.env.HOST ?? "0.0.0.0";
22409
22442
  var sseSessions = /* @__PURE__ */ new Map();
@@ -1466,7 +1466,7 @@ var init_installer_gui = __esm({
1466
1466
  init_install_dxt();
1467
1467
  init_install_shared();
1468
1468
  init_oauth();
1469
- VERSION = "0.19.3";
1469
+ VERSION = "0.20.0";
1470
1470
  PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
1471
1471
  sessions = /* @__PURE__ */ new Map();
1472
1472
  OAUTH_BASE_URLS = {
@@ -873,7 +873,7 @@ async function oauthLogin(opts) {
873
873
  }
874
874
 
875
875
  // installer/installer-gui.ts
876
- var VERSION = "0.19.3";
876
+ var VERSION = "0.20.0";
877
877
  var PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
878
878
  var sessions = /* @__PURE__ */ new Map();
879
879
  var OAUTH_BASE_URLS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.19.3",
3
+ "version": "0.20.0",
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",