@leadbay/mcp 0.19.2 → 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,14 @@
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
+
8
+ ## 0.19.3 — 2026-06-15
9
+
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.
11
+
3
12
  ## 0.19.2 — 2026-06-10
4
13
 
5
14
  - **Stop paging Sentry on a missing `_triggered_by`**: a composite tool called without `_triggered_by` is a recoverable agent mistake — the host just re-calls with the field set. The guard used to `throw` an `{error:true, code:"LAST_PROMPT_REQUIRED"}` envelope into the shared catch, where `isLeadbayBusinessError` matched it and fired `captureException`, auto-opening a top-priority Sentry/GitHub bug (product#3718) on every dropped field. The guard now returns the `isError` envelope directly. Behavior toward the LLM is unchanged (same text, same `isError`, same recovery hint), and PostHog visibility is preserved (`captureToolCall` + `captureCompositeCall` still fire `ok:false` / `LAST_PROMPT_REQUIRED`, so the mandate-ignore rate stays observable); only `captureException` is dropped.
package/dist/bin.js CHANGED
@@ -277,13 +277,40 @@ var init_client = __esm({
277
277
  if (next)
278
278
  next();
279
279
  }
280
- async request(method, path, body) {
280
+ // Leadbay tokens don't expire, so a 401 is almost always a transient
281
+ // server-side blip. Retry the request ONCE before surfacing it — a single
282
+ // retry clears the vast majority of these without the agent ever seeing an
283
+ // error. If the retry also 401s, it's a real Leadbay-side problem and the
284
+ // error envelope says so.
285
+ //
286
+ // Arrow-function field so `this` stays bound even when the method is passed
287
+ // as a bare reference (see request()'s ternary). Retries are GET-ONLY: a 401
288
+ // on a write (POST/PUT/DELETE) may arrive AFTER the mutation already committed
289
+ // server-side, so blindly re-sending it would double-execute the write. Reads
290
+ // are idempotent, so retrying them is safe. The 250ms backoff releases the
291
+ // concurrency slot first (release → sleep → re-acquire) so a wave of 401s
292
+ // doesn't pin all MAX_CONCURRENT slots in setTimeout and stall the queue.
293
+ httpsRequestWithRetry = async (method, url, headers, body) => {
294
+ const res = await httpsRequest(method, url, headers, body);
295
+ if (res.status === 401 && method.toUpperCase() === "GET") {
296
+ this.releaseSemaphore();
297
+ try {
298
+ await new Promise((r) => setTimeout(r, 250));
299
+ } finally {
300
+ await this.acquireSemaphore();
301
+ }
302
+ return httpsRequest(method, url, headers, body);
303
+ }
304
+ return res;
305
+ };
306
+ async request(method, path, body, opts) {
281
307
  if (process.env.LEADBAY_MOCK === "1") {
282
308
  return this.mockRequest(method, path, body);
283
309
  }
284
310
  if (!this.token) {
285
311
  throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y @leadbay/mcp install --email <you> --region <us|fr>", path);
286
312
  }
313
+ const retryOn401 = opts?.retryOn401 !== false;
287
314
  await this.acquireSemaphore();
288
315
  try {
289
316
  const url = `${this._baseUrl}/1.5${path}`;
@@ -293,7 +320,7 @@ var init_client = __esm({
293
320
  if (body) {
294
321
  headers["Content-Type"] = "application/json";
295
322
  }
296
- const res = await httpsRequest(method, url, headers, body ? JSON.stringify(body) : void 0);
323
+ const res = await (retryOn401 ? this.httpsRequestWithRetry : httpsRequest)(method, url, headers, body ? JSON.stringify(body) : void 0);
297
324
  this._lastMeta = {
298
325
  region: this._region,
299
326
  endpoint: `${method} ${path}`,
@@ -328,7 +355,7 @@ var init_client = __esm({
328
355
  if (body) {
329
356
  headers["Content-Type"] = "application/json";
330
357
  }
331
- const res = await httpsRequest(method, url, headers, body ? JSON.stringify(body) : void 0);
358
+ const res = await this.httpsRequestWithRetry(method, url, headers, body ? JSON.stringify(body) : void 0);
332
359
  this._lastMeta = {
333
360
  region: this._region,
334
361
  endpoint: `${method} ${path}`,
@@ -361,7 +388,7 @@ var init_client = __esm({
361
388
  Authorization: `Bearer ${this.token}`,
362
389
  "Content-Type": contentType
363
390
  };
364
- const res = await httpsRequest(method, url, headers, body);
391
+ const res = await this.httpsRequestWithRetry(method, url, headers, body);
365
392
  this._lastMeta = {
366
393
  region: this._region,
367
394
  endpoint: `${method} ${path}`,
@@ -444,7 +471,7 @@ var init_client = __esm({
444
471
  }
445
472
  const retryAfter = parseRetryAfter(headers["retry-after"]);
446
473
  if (status === 401) {
447
- return this.makeError("AUTH_EXPIRED", "Authentication token expired or invalid", "Your LEADBAY_TOKEN is no longer valid. Regenerate it: npx -y @leadbay/mcp login --email <you> --region <us|fr>, then restart your MCP client.", endpoint, null, status);
474
+ 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);
448
475
  }
449
476
  if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
450
477
  const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
@@ -5440,7 +5467,7 @@ var init_notifications = __esm({
5440
5467
  });
5441
5468
 
5442
5469
  // ../core/dist/tool-descriptions.generated.js
5443
- var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_contact, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_lens, leadbay_update_lens_filter;
5470
+ var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_contact, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_lens, leadbay_update_lens_filter;
5444
5471
  var init_tool_descriptions_generated = __esm({
5445
5472
  "../core/dist/tool-descriptions.generated.js"() {
5446
5473
  "use strict";
@@ -8722,6 +8749,88 @@ WHEN TO USE: low-level. The user's selection is a per-token global state \u2014
8722
8749
  WHEN NOT TO USE: in normal flow \u2014 leadbay_enrich_titles wraps select \u2192 action \u2192 clear in one call with proper Mutex protection. Calling this directly without acquiring the selection lock can clobber concurrent composite calls.
8723
8750
 
8724
8751
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
8752
+ `;
8753
+ leadbay_send_feedback = `## WHEN TO USE
8754
+
8755
+ Trigger phrases: "send feedback", "I want to report a bug", "tell the Leadbay team", "let Leadbay know", "give feedback", "report this to support", "I have a feature request".
8756
+
8757
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
8758
+
8759
+ Do NOT use for: "no, I meant / still nothing / ugh" \u2192 \`leadbay_report_friction\`; "log the email I sent" \u2192 \`leadbay_report_outreach\`.
8760
+
8761
+ Prefer when: the user explicitly wants the Leadbay TEAM to receive a message they authored \u2014 or accepts your offer to report an error. For silent, agent-detected friction signals use leadbay_report_friction instead.
8762
+
8763
+ Examples that SHOULD invoke this tool:
8764
+ - "Send feedback to the team: the lead scores feel off this week."
8765
+ - "Can you report a bug? Pulling leads in Lyon returns nothing."
8766
+ - "Tell Leadbay I'd love a way to schedule my morning check-in."
8767
+
8768
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
8769
+ - "No, I meant Wisconsin not Wyoming."
8770
+ - "I emailed Acme \u2014 log that outreach."
8771
+ - "Thumbs down on this lead."
8772
+
8773
+ ## RENDER (quick)
8774
+
8775
+ Confirm the exact wording with the user BEFORE calling (this is sent to
8776
+ the team). After sending, show a one-line confirmation from the result's
8777
+ \`message\` (e.g. "\u2713 Sent to the Leadbay team"). If \`sent\` is false, tell
8778
+ the user it could NOT be delivered \u2014 never imply it was sent.
8779
+
8780
+ ---
8781
+
8782
+ Deliver a user-authored message to the Leadbay team's feedback inbox \u2014 the same
8783
+ destination as the web app's feedback form. **You do not write the feedback;
8784
+ the user does.** Capture their words, confirm the phrasing, then send.
8785
+
8786
+ ## Parameters
8787
+ - \`message\` (required) \u2014 the user's feedback, in their own words. Confirm it
8788
+ with the user before sending. Cap 4000 chars.
8789
+ - \`associated_error_id\` (optional) \u2014 a Sentry event id to attach the feedback
8790
+ to (e.g. the id surfaced by an error the user just hit), so the team sees the
8791
+ feedback on that exact issue.
8792
+
8793
+ ## When a tool errors \u2014 OFFER, don't auto-send
8794
+ When a Leadbay tool returns an error and the user might want the team to know,
8795
+ you may OFFER: *"Want me to send feedback about this to the Leadbay team?"*
8796
+ - Send ONLY if the user says yes AND gives (or approves) a message.
8797
+ - Never send feedback the user didn't author or approve.
8798
+ - If an error event id is available, pass it as \`associated_error_id\`.
8799
+
8800
+ ## Result
8801
+ - \`sent: true\` \u2192 it reached the team. Show the confirmation from \`message\`.
8802
+ - \`sent: false\` \u2192 delivery wasn't possible (feedback not available on this
8803
+ client). Tell the user it was NOT sent. Do not claim success.
8804
+
8805
+ This is the only "talk to the Leadbay team" tool. It does not mutate any
8806
+ Leadbay data. For silent friction signals you detect yourself, use
8807
+ \`leadbay_report_friction\` instead.
8808
+
8809
+ ## NEXT STEPS \u2014 after sending feedback
8810
+
8811
+ **ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
8812
+
8813
+ **If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
8814
+
8815
+ **One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
8816
+ - Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
8817
+ - Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
8818
+
8819
+ Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
8820
+ - \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
8821
+ - \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
8822
+
8823
+ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
8824
+
8825
+ ---
8826
+
8827
+
8828
+
8829
+ | Observation | Suggest | Calls |
8830
+ |----------------------------------------------|------------------------------------------------------|-----------------------------------------|
8831
+ | \`sent == true\` | "Anything else you'd like to flag to the team?" | leadbay_send_feedback(message) |
8832
+ | \`sent == false\` | "It didn't go through \u2014 want to try sending again?" | leadbay_send_feedback(message) |
8833
+ | Feedback was about an error the user hit | "Want me to retry the action that failed?" | (re-call the tool that errored) |
8725
8834
  `;
8726
8835
  leadbay_set_active_lens = `Mark a lens as last-used. Subsequent \`/me\` reads return it as \`last_requested_lens\`, so all composite tools default to it.
8727
8836
 
@@ -16415,16 +16524,16 @@ var init_account_status = __esm({
16415
16524
  properties: {
16416
16525
  current_version: { type: "string" },
16417
16526
  latest_version: { type: "string" },
16418
- mcpb_url: {
16527
+ install_url: {
16419
16528
  type: "string",
16420
- description: "Direct download URL for the .mcpb installer asset."
16529
+ description: "Direct download URL for the installer asset (.dxt, falling back to .mcpb)."
16421
16530
  },
16422
16531
  release_url: {
16423
16532
  type: "string",
16424
16533
  description: "GitHub release page (changelog)."
16425
16534
  }
16426
16535
  },
16427
- required: ["current_version", "latest_version", "mcpb_url", "release_url"]
16536
+ required: ["current_version", "latest_version", "install_url", "release_url"]
16428
16537
  }
16429
16538
  },
16430
16539
  required: ["user", "organization"]
@@ -21240,6 +21349,90 @@ var init_report_friction = __esm({
21240
21349
  }
21241
21350
  });
21242
21351
 
21352
+ // ../core/dist/tools/send-feedback.js
21353
+ var MESSAGE_MAX, sendFeedback;
21354
+ var init_send_feedback = __esm({
21355
+ "../core/dist/tools/send-feedback.js"() {
21356
+ "use strict";
21357
+ init_tool_descriptions_generated();
21358
+ MESSAGE_MAX = 4e3;
21359
+ sendFeedback = {
21360
+ name: "leadbay_send_feedback",
21361
+ annotations: {
21362
+ title: "Send feedback to the Leadbay team",
21363
+ readOnlyHint: false,
21364
+ destructiveHint: false,
21365
+ idempotentHint: false,
21366
+ openWorldHint: true
21367
+ },
21368
+ description: leadbay_send_feedback,
21369
+ // Write-gated: it sends data outward to the Leadbay team. Registered in
21370
+ // compositeWriteTools (default-on since 0.3.0).
21371
+ write: true,
21372
+ inputSchema: {
21373
+ type: "object",
21374
+ properties: {
21375
+ message: {
21376
+ type: "string",
21377
+ description: "The user's feedback, in their own words. Confirm the wording with the user BEFORE calling \u2014 this is sent to the Leadbay team. Cap 4000 chars."
21378
+ },
21379
+ associated_error_id: {
21380
+ type: "string",
21381
+ description: "Optional: a Sentry event id to attach this feedback to (e.g. the id from an error the user just hit), so the team sees the feedback on that exact issue."
21382
+ }
21383
+ },
21384
+ required: ["message"],
21385
+ additionalProperties: false
21386
+ },
21387
+ outputSchema: {
21388
+ type: "object",
21389
+ description: "Whether the feedback reached the Leadbay team. `sent: true` means it landed in the team's inbox.",
21390
+ properties: {
21391
+ sent: { type: "boolean" },
21392
+ message: { type: "string" },
21393
+ _meta: {
21394
+ type: "object",
21395
+ properties: { region: { type: "string" } }
21396
+ }
21397
+ }
21398
+ },
21399
+ execute: async (client, params, ctx) => {
21400
+ const text = typeof params.message === "string" ? params.message.trim() : "";
21401
+ if (!text) {
21402
+ return {
21403
+ error: true,
21404
+ code: "BAD_INPUT",
21405
+ message: "message is required \u2014 pass the user's feedback text.",
21406
+ hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
21407
+ };
21408
+ }
21409
+ const message = text.length > MESSAGE_MAX ? `${text.slice(0, MESSAGE_MAX - 1)}\u2026` : text;
21410
+ if (!ctx?.sendFeedback) {
21411
+ return {
21412
+ sent: false,
21413
+ message: "Feedback could not be sent from this client (feedback delivery isn't available here). Let the user know it wasn't delivered.",
21414
+ _meta: { region: client.region }
21415
+ };
21416
+ }
21417
+ const errorId = typeof params.associated_error_id === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(params.associated_error_id) ? params.associated_error_id : void 0;
21418
+ const sent = await ctx.sendFeedback(message, {
21419
+ ...errorId ? { associatedEventId: errorId } : {}
21420
+ });
21421
+ return {
21422
+ sent,
21423
+ message: sent ? "Sent to the Leadbay team \u2014 thanks for the feedback." : (
21424
+ // `sent:false` means the bounded flush didn't confirm within the
21425
+ // window — the envelope may still drain on shutdown. Don't assert it
21426
+ // failed (that trains users to re-send and spam the inbox).
21427
+ "Delivery not confirmed \u2014 it may still reach the Leadbay team. Avoid re-sending unless the user wants to."
21428
+ ),
21429
+ _meta: { region: client.region }
21430
+ };
21431
+ }
21432
+ };
21433
+ }
21434
+ });
21435
+
21243
21436
  // ../core/dist/index.js
21244
21437
  var dist_exports = {};
21245
21438
  __export(dist_exports, {
@@ -21370,6 +21563,7 @@ __export(dist_exports, {
21370
21563
  scanPortfolioSignals: () => scanPortfolioSignals,
21371
21564
  seedCandidates: () => seedCandidates,
21372
21565
  selectLeads: () => selectLeads,
21566
+ sendFeedback: () => sendFeedback,
21373
21567
  setActiveLens: () => setActiveLens,
21374
21568
  setEpilogueStatus: () => setEpilogueStatus,
21375
21569
  setPushback: () => setPushback,
@@ -21481,6 +21675,7 @@ var init_dist = __esm({
21481
21675
  init_answer_clarification();
21482
21676
  init_report_outreach();
21483
21677
  init_report_friction();
21678
+ init_send_feedback();
21484
21679
  init_bulk_store();
21485
21680
  agentMemoryTools = [
21486
21681
  agentMemoryRecall,
@@ -21612,6 +21807,13 @@ var init_dist = __esm({
21612
21807
  refinePrompt,
21613
21808
  answerClarification,
21614
21809
  reportOutreach,
21810
+ // sendFeedback is granular-shaped (a single call to the telemetry seam →
21811
+ // Sentry.captureFeedback, same inbox as the web app's feedback form), so it
21812
+ // lives in tools/, NOT composite/. Registered here (not advanced-gated) so
21813
+ // users can send feedback in-conversation without LEADBAY_MCP_ADVANCED.
21814
+ // Write-gated since it sends data outward. Not in COMPOSITE_FILE_TOOL_NAMES,
21815
+ // so _triggered_by is optional (no orchestration / mandatory-intent-trace).
21816
+ sendFeedback,
21615
21817
  importLeads,
21616
21818
  importAndQualify,
21617
21819
  // Contact management (product#3703) — each is a single-call relay, so
@@ -23081,6 +23283,7 @@ var NOOP_TELEMETRY = {
23081
23283
  },
23082
23284
  captureException: () => {
23083
23285
  },
23286
+ captureFeedback: async () => false,
23084
23287
  captureUpdateCheck: () => {
23085
23288
  },
23086
23289
  captureUpdatePrompted: () => {
@@ -23335,6 +23538,41 @@ function initTelemetry(opts) {
23335
23538
  logger?.warn?.(`sentry captureException failed: ${e?.message ?? e}`);
23336
23539
  }
23337
23540
  },
23541
+ async captureFeedback(message, opts2) {
23542
+ if (!sentryReady) return false;
23543
+ const trimmed = (message ?? "").trim();
23544
+ if (!trimmed) return false;
23545
+ if (identityPromise) {
23546
+ let waitTimer;
23547
+ try {
23548
+ await Promise.race([
23549
+ identityPromise,
23550
+ new Promise((resolve) => {
23551
+ waitTimer = setTimeout(resolve, 2e3);
23552
+ })
23553
+ ]);
23554
+ } catch {
23555
+ } finally {
23556
+ if (waitTimer) clearTimeout(waitTimer);
23557
+ }
23558
+ }
23559
+ try {
23560
+ Sentry.captureFeedback({
23561
+ message: trimmed,
23562
+ ...me?.name ? { name: me.name } : {},
23563
+ ...me?.email ? { email: me.email } : {},
23564
+ ...opts2?.associatedEventId ? { associatedEventId: opts2.associatedEventId } : {}
23565
+ });
23566
+ const flushed = await Sentry.flush(4e3);
23567
+ if (!flushed) {
23568
+ logger?.warn?.("sentry feedback flush timed out (event may be buffered)");
23569
+ }
23570
+ return flushed;
23571
+ } catch (e) {
23572
+ logger?.warn?.(`sentry captureFeedback failed: ${e?.message ?? e}`);
23573
+ return false;
23574
+ }
23575
+ },
23338
23576
  async shutdown() {
23339
23577
  const tasks = [];
23340
23578
  if (posthog) tasks.push(posthog.shutdown(2e3).catch(() => void 0));
@@ -23346,10 +23584,13 @@ function initTelemetry(opts) {
23346
23584
 
23347
23585
  // src/update-check.ts
23348
23586
  var cachedInfo = null;
23349
- var checkInFlight = false;
23587
+ var inFlightCheck = null;
23350
23588
  function getCachedUpdateInfo() {
23351
23589
  return cachedInfo;
23352
23590
  }
23591
+ function getInFlightCheck() {
23592
+ return inFlightCheck;
23593
+ }
23353
23594
  var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
23354
23595
  var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
23355
23596
  var FETCH_TIMEOUT_MS = 5e3;
@@ -23394,25 +23635,24 @@ function compareSemver(a, b) {
23394
23635
  }
23395
23636
  return 0;
23396
23637
  }
23397
- function pickMcpbAsset(rel) {
23638
+ function pickInstallAsset(rel) {
23398
23639
  if (!Array.isArray(rel.assets)) return void 0;
23399
- const mcpb = rel.assets.find(
23400
- (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
23401
- );
23402
- if (mcpb?.browser_download_url) return mcpb.browser_download_url;
23403
23640
  const dxt = rel.assets.find(
23404
23641
  (a) => typeof a.name === "string" && a.name.endsWith(".dxt")
23405
23642
  );
23406
- 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;
23407
23648
  }
23408
- async function checkForUpdate(opts) {
23409
- if (checkInFlight) return cachedInfo;
23410
- checkInFlight = true;
23411
- try {
23412
- return await doCheck(opts);
23413
- } finally {
23414
- checkInFlight = false;
23415
- }
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;
23416
23656
  }
23417
23657
  async function doCheck(opts) {
23418
23658
  const now = opts.now ?? Date.now;
@@ -23421,11 +23661,11 @@ async function doCheck(opts) {
23421
23661
  const currentVersion = opts.currentVersion;
23422
23662
  const state = await opts.stateStore.read();
23423
23663
  const within = now() - state.last_check_time < CHECK_THROTTLE_MS;
23424
- 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) {
23425
23665
  const cached = buildInfoIfUpgrade(
23426
23666
  currentVersion,
23427
23667
  state.latest_known_version,
23428
- state.latest_known_mcpb_url,
23668
+ state.latest_known_install_url,
23429
23669
  state.latest_known_release_url,
23430
23670
  state.suppressed_versions,
23431
23671
  state.remind_until,
@@ -23479,18 +23719,18 @@ async function doCheck(opts) {
23479
23719
  return null;
23480
23720
  }
23481
23721
  let latestVersion;
23482
- let mcpbUrl;
23722
+ let installUrl;
23483
23723
  let releaseUrl;
23484
23724
  if (status === 200 && body) {
23485
23725
  const parsed = body.tag_name ? parseTagName(body.tag_name) : null;
23486
23726
  if (parsed) {
23487
23727
  latestVersion = parsed;
23488
- mcpbUrl = pickMcpbAsset(body);
23728
+ installUrl = pickInstallAsset(body);
23489
23729
  releaseUrl = body.html_url;
23490
23730
  }
23491
23731
  } else {
23492
23732
  latestVersion = state.latest_known_version;
23493
- mcpbUrl = state.latest_known_mcpb_url;
23733
+ installUrl = state.latest_known_install_url;
23494
23734
  releaseUrl = state.latest_known_release_url;
23495
23735
  }
23496
23736
  const persisted = await opts.stateStore.update((cur) => ({
@@ -23498,7 +23738,7 @@ async function doCheck(opts) {
23498
23738
  last_check_time: now(),
23499
23739
  etag: nextEtag,
23500
23740
  latest_known_version: latestVersion ?? cur.latest_known_version,
23501
- latest_known_mcpb_url: mcpbUrl ?? cur.latest_known_mcpb_url,
23741
+ latest_known_install_url: installUrl ?? cur.latest_known_install_url,
23502
23742
  latest_known_release_url: releaseUrl ?? cur.latest_known_release_url
23503
23743
  }));
23504
23744
  opts.telemetry.captureUpdateCheck?.({
@@ -23508,7 +23748,7 @@ async function doCheck(opts) {
23508
23748
  const info = buildInfoIfUpgrade(
23509
23749
  currentVersion,
23510
23750
  persisted.latest_known_version,
23511
- persisted.latest_known_mcpb_url,
23751
+ persisted.latest_known_install_url,
23512
23752
  persisted.latest_known_release_url,
23513
23753
  persisted.suppressed_versions,
23514
23754
  persisted.remind_until,
@@ -23517,15 +23757,15 @@ async function doCheck(opts) {
23517
23757
  cachedInfo = info;
23518
23758
  return info;
23519
23759
  }
23520
- function buildInfoIfUpgrade(currentVersion, latestVersion, mcpbUrl, releaseUrl, suppressed, remindUntil, nowMs) {
23521
- if (!latestVersion || !mcpbUrl || !releaseUrl) return null;
23760
+ function buildInfoIfUpgrade(currentVersion, latestVersion, installUrl, releaseUrl, suppressed, remindUntil, nowMs) {
23761
+ if (!latestVersion || !installUrl || !releaseUrl) return null;
23522
23762
  if (compareSemver(latestVersion, currentVersion) <= 0) return null;
23523
23763
  if (suppressed.includes(latestVersion)) return null;
23524
23764
  if (remindUntil && remindUntil > nowMs) return null;
23525
23765
  return {
23526
23766
  current_version: currentVersion,
23527
23767
  latest_version: latestVersion,
23528
- mcpb_url: mcpbUrl,
23768
+ install_url: installUrl,
23529
23769
  release_url: releaseUrl
23530
23770
  };
23531
23771
  }
@@ -23554,7 +23794,7 @@ async function recordRunningVersion(currentVersion, stateStore, telemetry) {
23554
23794
 
23555
23795
  // src/update-tool.ts
23556
23796
  var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
23557
- 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.";
23558
23798
  function buildAcknowledgeUpdateTool(opts) {
23559
23799
  const now = opts.now ?? Date.now;
23560
23800
  return {
@@ -23590,7 +23830,7 @@ function buildAcknowledgeUpdateTool(opts) {
23590
23830
  action: { type: "string" },
23591
23831
  version: { type: "string" },
23592
23832
  message: { type: "string" },
23593
- mcpb_url: { type: ["string", "null"] },
23833
+ install_url: { type: ["string", "null"] },
23594
23834
  release_url: { type: ["string", "null"] }
23595
23835
  },
23596
23836
  required: ["ok", "action", "version", "message"]
@@ -23624,9 +23864,9 @@ function buildAcknowledgeUpdateTool(opts) {
23624
23864
  ok: true,
23625
23865
  action,
23626
23866
  version,
23627
- mcpb_url: state.latest_known_mcpb_url ?? null,
23867
+ install_url: state.latest_known_install_url ?? null,
23628
23868
  release_url: state.latest_known_release_url ?? null,
23629
- 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."
23630
23870
  };
23631
23871
  }
23632
23872
  if (action === "remind_tomorrow") {
@@ -23729,7 +23969,7 @@ function buildStartHereParagraph(has) {
23729
23969
  }
23730
23970
  function buildUpdateAvailableParagraph(has) {
23731
23971
  if (!has("leadbay_acknowledge_update")) return null;
23732
- 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.";
23733
23973
  }
23734
23974
  function buildRhythmParagraph(has) {
23735
23975
  if (has("leadbay_report_outreach")) {
@@ -24024,16 +24264,47 @@ function buildServer(client, opts = {}) {
24024
24264
  );
24025
24265
  });
24026
24266
  };
24027
- const maybeAttachUpdate = (toolName, result) => {
24028
- if (toolName !== "leadbay_account_status") return;
24267
+ const UPDATE_SURFACE_WAIT_MS = 1500;
24268
+ const maybeAttachUpdate = async (toolName, result) => {
24029
24269
  if (!opts.updateStateStore) return;
24030
24270
  if (result === null || typeof result !== "object" || Array.isArray(result)) {
24031
24271
  return;
24032
24272
  }
24033
- 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
+ }
24034
24295
  if (!info) return;
24035
- result.update_available = info;
24036
- 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) {
24037
24308
  promptedVersionsThisSession.add(info.latest_version);
24038
24309
  telemetry.captureUpdatePrompted?.({
24039
24310
  current_version: serverVersion,
@@ -24207,9 +24478,13 @@ function buildServer(client, opts = {}) {
24207
24478
  notificationsInbox: opts.notificationsInbox,
24208
24479
  signal: extra.signal,
24209
24480
  progress,
24210
- elicit
24481
+ elicit,
24482
+ // Route leadbay_send_feedback to Sentry's feedback inbox (same place
24483
+ // the web app's form lands). NOOP_TELEMETRY returns false, so the
24484
+ // tool reports honestly when telemetry is off.
24485
+ sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
24211
24486
  });
24212
- maybeAttachUpdate(name, result);
24487
+ await maybeAttachUpdate(name, result);
24213
24488
  maybeAttachNotifications(result);
24214
24489
  if (result && typeof result === "object" && result.error === true) {
24215
24490
  const envText = formatErrorForLLM(result);
@@ -25219,8 +25494,10 @@ var UpdateStateStore = class {
25219
25494
  if (typeof r.latest_known_version === "string") {
25220
25495
  out.latest_known_version = r.latest_known_version;
25221
25496
  }
25222
- if (typeof r.latest_known_mcpb_url === "string") {
25223
- 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;
25224
25501
  }
25225
25502
  if (typeof r.latest_known_release_url === "string") {
25226
25503
  out.latest_known_release_url = r.latest_known_release_url;
@@ -25648,7 +25925,7 @@ var OAUTH_BASE_URLS = {
25648
25925
  fr: "https://staging.api.leadbay.app"
25649
25926
  }
25650
25927
  };
25651
- var VERSION = "0.19.2";
25928
+ var VERSION = "0.20.0";
25652
25929
  var HELP = `
25653
25930
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
25654
25931
 
@@ -25923,7 +26200,7 @@ async function resolveClientFromEnv(logger) {
25923
26200
  logger.info?.("Auto-detecting region via /users/me on us and fr...");
25924
26201
  const probe = async (region) => {
25925
26202
  const c = createClient({ token, region });
25926
- await c.request("GET", "/users/me");
26203
+ await c.request("GET", "/users/me", void 0, { retryOn401: false });
25927
26204
  return c;
25928
26205
  };
25929
26206
  try {
@@ -1519,13 +1519,40 @@ var LeadbayClient = class {
1519
1519
  if (next)
1520
1520
  next();
1521
1521
  }
1522
- async request(method, path, body) {
1522
+ // Leadbay tokens don't expire, so a 401 is almost always a transient
1523
+ // server-side blip. Retry the request ONCE before surfacing it — a single
1524
+ // retry clears the vast majority of these without the agent ever seeing an
1525
+ // error. If the retry also 401s, it's a real Leadbay-side problem and the
1526
+ // error envelope says so.
1527
+ //
1528
+ // Arrow-function field so `this` stays bound even when the method is passed
1529
+ // as a bare reference (see request()'s ternary). Retries are GET-ONLY: a 401
1530
+ // on a write (POST/PUT/DELETE) may arrive AFTER the mutation already committed
1531
+ // server-side, so blindly re-sending it would double-execute the write. Reads
1532
+ // are idempotent, so retrying them is safe. The 250ms backoff releases the
1533
+ // concurrency slot first (release → sleep → re-acquire) so a wave of 401s
1534
+ // doesn't pin all MAX_CONCURRENT slots in setTimeout and stall the queue.
1535
+ httpsRequestWithRetry = async (method, url, headers, body) => {
1536
+ const res = await httpsRequest(method, url, headers, body);
1537
+ if (res.status === 401 && method.toUpperCase() === "GET") {
1538
+ this.releaseSemaphore();
1539
+ try {
1540
+ await new Promise((r) => setTimeout(r, 250));
1541
+ } finally {
1542
+ await this.acquireSemaphore();
1543
+ }
1544
+ return httpsRequest(method, url, headers, body);
1545
+ }
1546
+ return res;
1547
+ };
1548
+ async request(method, path, body, opts) {
1523
1549
  if (process.env.LEADBAY_MOCK === "1") {
1524
1550
  return this.mockRequest(method, path, body);
1525
1551
  }
1526
1552
  if (!this.token) {
1527
1553
  throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y @leadbay/mcp install --email <you> --region <us|fr>", path);
1528
1554
  }
1555
+ const retryOn401 = opts?.retryOn401 !== false;
1529
1556
  await this.acquireSemaphore();
1530
1557
  try {
1531
1558
  const url = `${this._baseUrl}/1.5${path}`;
@@ -1535,7 +1562,7 @@ var LeadbayClient = class {
1535
1562
  if (body) {
1536
1563
  headers["Content-Type"] = "application/json";
1537
1564
  }
1538
- const res = await httpsRequest(method, url, headers, body ? JSON.stringify(body) : void 0);
1565
+ const res = await (retryOn401 ? this.httpsRequestWithRetry : httpsRequest)(method, url, headers, body ? JSON.stringify(body) : void 0);
1539
1566
  this._lastMeta = {
1540
1567
  region: this._region,
1541
1568
  endpoint: `${method} ${path}`,
@@ -1570,7 +1597,7 @@ var LeadbayClient = class {
1570
1597
  if (body) {
1571
1598
  headers["Content-Type"] = "application/json";
1572
1599
  }
1573
- const res = await httpsRequest(method, url, headers, body ? JSON.stringify(body) : void 0);
1600
+ const res = await this.httpsRequestWithRetry(method, url, headers, body ? JSON.stringify(body) : void 0);
1574
1601
  this._lastMeta = {
1575
1602
  region: this._region,
1576
1603
  endpoint: `${method} ${path}`,
@@ -1603,7 +1630,7 @@ var LeadbayClient = class {
1603
1630
  Authorization: `Bearer ${this.token}`,
1604
1631
  "Content-Type": contentType
1605
1632
  };
1606
- const res = await httpsRequest(method, url, headers, body);
1633
+ const res = await this.httpsRequestWithRetry(method, url, headers, body);
1607
1634
  this._lastMeta = {
1608
1635
  region: this._region,
1609
1636
  endpoint: `${method} ${path}`,
@@ -1686,7 +1713,7 @@ var LeadbayClient = class {
1686
1713
  }
1687
1714
  const retryAfter = parseRetryAfter(headers["retry-after"]);
1688
1715
  if (status === 401) {
1689
- return this.makeError("AUTH_EXPIRED", "Authentication token expired or invalid", "Your LEADBAY_TOKEN is no longer valid. Regenerate it: npx -y @leadbay/mcp login --email <you> --region <us|fr>, then restart your MCP client.", endpoint, null, status);
1716
+ 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);
1690
1717
  }
1691
1718
  if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
1692
1719
  const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
@@ -9546,6 +9573,88 @@ WHEN NOT TO USE: in normal flow \u2014 leadbay_enrich_titles wraps select \u2192
9546
9573
 
9547
9574
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
9548
9575
  `;
9576
+ var leadbay_send_feedback = `## WHEN TO USE
9577
+
9578
+ Trigger phrases: "send feedback", "I want to report a bug", "tell the Leadbay team", "let Leadbay know", "give feedback", "report this to support", "I have a feature request".
9579
+
9580
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
9581
+
9582
+ Do NOT use for: "no, I meant / still nothing / ugh" \u2192 \`leadbay_report_friction\`; "log the email I sent" \u2192 \`leadbay_report_outreach\`.
9583
+
9584
+ Prefer when: the user explicitly wants the Leadbay TEAM to receive a message they authored \u2014 or accepts your offer to report an error. For silent, agent-detected friction signals use leadbay_report_friction instead.
9585
+
9586
+ Examples that SHOULD invoke this tool:
9587
+ - "Send feedback to the team: the lead scores feel off this week."
9588
+ - "Can you report a bug? Pulling leads in Lyon returns nothing."
9589
+ - "Tell Leadbay I'd love a way to schedule my morning check-in."
9590
+
9591
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
9592
+ - "No, I meant Wisconsin not Wyoming."
9593
+ - "I emailed Acme \u2014 log that outreach."
9594
+ - "Thumbs down on this lead."
9595
+
9596
+ ## RENDER (quick)
9597
+
9598
+ Confirm the exact wording with the user BEFORE calling (this is sent to
9599
+ the team). After sending, show a one-line confirmation from the result's
9600
+ \`message\` (e.g. "\u2713 Sent to the Leadbay team"). If \`sent\` is false, tell
9601
+ the user it could NOT be delivered \u2014 never imply it was sent.
9602
+
9603
+ ---
9604
+
9605
+ Deliver a user-authored message to the Leadbay team's feedback inbox \u2014 the same
9606
+ destination as the web app's feedback form. **You do not write the feedback;
9607
+ the user does.** Capture their words, confirm the phrasing, then send.
9608
+
9609
+ ## Parameters
9610
+ - \`message\` (required) \u2014 the user's feedback, in their own words. Confirm it
9611
+ with the user before sending. Cap 4000 chars.
9612
+ - \`associated_error_id\` (optional) \u2014 a Sentry event id to attach the feedback
9613
+ to (e.g. the id surfaced by an error the user just hit), so the team sees the
9614
+ feedback on that exact issue.
9615
+
9616
+ ## When a tool errors \u2014 OFFER, don't auto-send
9617
+ When a Leadbay tool returns an error and the user might want the team to know,
9618
+ you may OFFER: *"Want me to send feedback about this to the Leadbay team?"*
9619
+ - Send ONLY if the user says yes AND gives (or approves) a message.
9620
+ - Never send feedback the user didn't author or approve.
9621
+ - If an error event id is available, pass it as \`associated_error_id\`.
9622
+
9623
+ ## Result
9624
+ - \`sent: true\` \u2192 it reached the team. Show the confirmation from \`message\`.
9625
+ - \`sent: false\` \u2192 delivery wasn't possible (feedback not available on this
9626
+ client). Tell the user it was NOT sent. Do not claim success.
9627
+
9628
+ This is the only "talk to the Leadbay team" tool. It does not mutate any
9629
+ Leadbay data. For silent friction signals you detect yourself, use
9630
+ \`leadbay_report_friction\` instead.
9631
+
9632
+ ## NEXT STEPS \u2014 after sending feedback
9633
+
9634
+ **ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
9635
+
9636
+ **If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
9637
+
9638
+ **One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
9639
+ - Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
9640
+ - Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
9641
+
9642
+ Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
9643
+ - \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
9644
+ - \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
9645
+
9646
+ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
9647
+
9648
+ ---
9649
+
9650
+
9651
+
9652
+ | Observation | Suggest | Calls |
9653
+ |----------------------------------------------|------------------------------------------------------|-----------------------------------------|
9654
+ | \`sent == true\` | "Anything else you'd like to flag to the team?" | leadbay_send_feedback(message) |
9655
+ | \`sent == false\` | "It didn't go through \u2014 want to try sending again?" | leadbay_send_feedback(message) |
9656
+ | Feedback was about an error the user hit | "Want me to retry the action that failed?" | (re-call the tool that errored) |
9657
+ `;
9549
9658
  var leadbay_set_active_lens = `Mark a lens as last-used. Subsequent \`/me\` reads return it as \`last_requested_lens\`, so all composite tools default to it.
9550
9659
 
9551
9660
  WHEN TO USE: after the user explicitly switched contexts (e.g. created a new lens via leadbay_create_lens).
@@ -16670,16 +16779,16 @@ var accountStatus = {
16670
16779
  properties: {
16671
16780
  current_version: { type: "string" },
16672
16781
  latest_version: { type: "string" },
16673
- mcpb_url: {
16782
+ install_url: {
16674
16783
  type: "string",
16675
- description: "Direct download URL for the .mcpb installer asset."
16784
+ description: "Direct download URL for the installer asset (.dxt, falling back to .mcpb)."
16676
16785
  },
16677
16786
  release_url: {
16678
16787
  type: "string",
16679
16788
  description: "GitHub release page (changelog)."
16680
16789
  }
16681
16790
  },
16682
- required: ["current_version", "latest_version", "mcpb_url", "release_url"]
16791
+ required: ["current_version", "latest_version", "install_url", "release_url"]
16683
16792
  }
16684
16793
  },
16685
16794
  required: ["user", "organization"]
@@ -20749,6 +20858,83 @@ var reportFriction = {
20749
20858
  }
20750
20859
  };
20751
20860
 
20861
+ // ../core/dist/tools/send-feedback.js
20862
+ var MESSAGE_MAX = 4e3;
20863
+ var sendFeedback = {
20864
+ name: "leadbay_send_feedback",
20865
+ annotations: {
20866
+ title: "Send feedback to the Leadbay team",
20867
+ readOnlyHint: false,
20868
+ destructiveHint: false,
20869
+ idempotentHint: false,
20870
+ openWorldHint: true
20871
+ },
20872
+ description: leadbay_send_feedback,
20873
+ // Write-gated: it sends data outward to the Leadbay team. Registered in
20874
+ // compositeWriteTools (default-on since 0.3.0).
20875
+ write: true,
20876
+ inputSchema: {
20877
+ type: "object",
20878
+ properties: {
20879
+ message: {
20880
+ type: "string",
20881
+ description: "The user's feedback, in their own words. Confirm the wording with the user BEFORE calling \u2014 this is sent to the Leadbay team. Cap 4000 chars."
20882
+ },
20883
+ associated_error_id: {
20884
+ type: "string",
20885
+ description: "Optional: a Sentry event id to attach this feedback to (e.g. the id from an error the user just hit), so the team sees the feedback on that exact issue."
20886
+ }
20887
+ },
20888
+ required: ["message"],
20889
+ additionalProperties: false
20890
+ },
20891
+ outputSchema: {
20892
+ type: "object",
20893
+ description: "Whether the feedback reached the Leadbay team. `sent: true` means it landed in the team's inbox.",
20894
+ properties: {
20895
+ sent: { type: "boolean" },
20896
+ message: { type: "string" },
20897
+ _meta: {
20898
+ type: "object",
20899
+ properties: { region: { type: "string" } }
20900
+ }
20901
+ }
20902
+ },
20903
+ execute: async (client, params, ctx) => {
20904
+ const text = typeof params.message === "string" ? params.message.trim() : "";
20905
+ if (!text) {
20906
+ return {
20907
+ error: true,
20908
+ code: "BAD_INPUT",
20909
+ message: "message is required \u2014 pass the user's feedback text.",
20910
+ hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
20911
+ };
20912
+ }
20913
+ const message = text.length > MESSAGE_MAX ? `${text.slice(0, MESSAGE_MAX - 1)}\u2026` : text;
20914
+ if (!ctx?.sendFeedback) {
20915
+ return {
20916
+ sent: false,
20917
+ message: "Feedback could not be sent from this client (feedback delivery isn't available here). Let the user know it wasn't delivered.",
20918
+ _meta: { region: client.region }
20919
+ };
20920
+ }
20921
+ const errorId = typeof params.associated_error_id === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(params.associated_error_id) ? params.associated_error_id : void 0;
20922
+ const sent = await ctx.sendFeedback(message, {
20923
+ ...errorId ? { associatedEventId: errorId } : {}
20924
+ });
20925
+ return {
20926
+ sent,
20927
+ message: sent ? "Sent to the Leadbay team \u2014 thanks for the feedback." : (
20928
+ // `sent:false` means the bounded flush didn't confirm within the
20929
+ // window — the envelope may still drain on shutdown. Don't assert it
20930
+ // failed (that trains users to re-send and spam the inbox).
20931
+ "Delivery not confirmed \u2014 it may still reach the Leadbay team. Avoid re-sending unless the user wants to."
20932
+ ),
20933
+ _meta: { region: client.region }
20934
+ };
20935
+ }
20936
+ };
20937
+
20752
20938
  // ../core/dist/index.js
20753
20939
  var agentMemoryTools = [
20754
20940
  agentMemoryRecall,
@@ -20880,6 +21066,13 @@ var compositeWriteTools = [
20880
21066
  refinePrompt,
20881
21067
  answerClarification,
20882
21068
  reportOutreach,
21069
+ // sendFeedback is granular-shaped (a single call to the telemetry seam →
21070
+ // Sentry.captureFeedback, same inbox as the web app's feedback form), so it
21071
+ // lives in tools/, NOT composite/. Registered here (not advanced-gated) so
21072
+ // users can send feedback in-conversation without LEADBAY_MCP_ADVANCED.
21073
+ // Write-gated since it sends data outward. Not in COMPOSITE_FILE_TOOL_NAMES,
21074
+ // so _triggered_by is optional (no orchestration / mandatory-intent-trace).
21075
+ sendFeedback,
20883
21076
  importLeads,
20884
21077
  importAndQualify,
20885
21078
  // Contact management (product#3703) — each is a single-call relay, so
@@ -21052,6 +21245,7 @@ var NOOP_TELEMETRY = {
21052
21245
  },
21053
21246
  captureException: () => {
21054
21247
  },
21248
+ captureFeedback: async () => false,
21055
21249
  captureUpdateCheck: () => {
21056
21250
  },
21057
21251
  captureUpdatePrompted: () => {
@@ -21068,10 +21262,13 @@ var NOOP_TELEMETRY = {
21068
21262
 
21069
21263
  // src/update-check.ts
21070
21264
  var cachedInfo = null;
21071
- var checkInFlight = false;
21265
+ var inFlightCheck = null;
21072
21266
  function getCachedUpdateInfo() {
21073
21267
  return cachedInfo;
21074
21268
  }
21269
+ function getInFlightCheck() {
21270
+ return inFlightCheck;
21271
+ }
21075
21272
  var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
21076
21273
  var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
21077
21274
  var FETCH_TIMEOUT_MS = 5e3;
@@ -21116,25 +21313,24 @@ function compareSemver(a, b) {
21116
21313
  }
21117
21314
  return 0;
21118
21315
  }
21119
- function pickMcpbAsset(rel) {
21316
+ function pickInstallAsset(rel) {
21120
21317
  if (!Array.isArray(rel.assets)) return void 0;
21121
- const mcpb = rel.assets.find(
21122
- (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
21123
- );
21124
- if (mcpb?.browser_download_url) return mcpb.browser_download_url;
21125
21318
  const dxt = rel.assets.find(
21126
21319
  (a) => typeof a.name === "string" && a.name.endsWith(".dxt")
21127
21320
  );
21128
- 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;
21129
21326
  }
21130
- async function checkForUpdate(opts) {
21131
- if (checkInFlight) return cachedInfo;
21132
- checkInFlight = true;
21133
- try {
21134
- return await doCheck(opts);
21135
- } finally {
21136
- checkInFlight = false;
21137
- }
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;
21138
21334
  }
21139
21335
  async function doCheck(opts) {
21140
21336
  const now = opts.now ?? Date.now;
@@ -21143,11 +21339,11 @@ async function doCheck(opts) {
21143
21339
  const currentVersion = opts.currentVersion;
21144
21340
  const state = await opts.stateStore.read();
21145
21341
  const within = now() - state.last_check_time < CHECK_THROTTLE_MS;
21146
- 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) {
21147
21343
  const cached = buildInfoIfUpgrade(
21148
21344
  currentVersion,
21149
21345
  state.latest_known_version,
21150
- state.latest_known_mcpb_url,
21346
+ state.latest_known_install_url,
21151
21347
  state.latest_known_release_url,
21152
21348
  state.suppressed_versions,
21153
21349
  state.remind_until,
@@ -21201,18 +21397,18 @@ async function doCheck(opts) {
21201
21397
  return null;
21202
21398
  }
21203
21399
  let latestVersion;
21204
- let mcpbUrl;
21400
+ let installUrl;
21205
21401
  let releaseUrl;
21206
21402
  if (status === 200 && body) {
21207
21403
  const parsed = body.tag_name ? parseTagName(body.tag_name) : null;
21208
21404
  if (parsed) {
21209
21405
  latestVersion = parsed;
21210
- mcpbUrl = pickMcpbAsset(body);
21406
+ installUrl = pickInstallAsset(body);
21211
21407
  releaseUrl = body.html_url;
21212
21408
  }
21213
21409
  } else {
21214
21410
  latestVersion = state.latest_known_version;
21215
- mcpbUrl = state.latest_known_mcpb_url;
21411
+ installUrl = state.latest_known_install_url;
21216
21412
  releaseUrl = state.latest_known_release_url;
21217
21413
  }
21218
21414
  const persisted = await opts.stateStore.update((cur) => ({
@@ -21220,7 +21416,7 @@ async function doCheck(opts) {
21220
21416
  last_check_time: now(),
21221
21417
  etag: nextEtag,
21222
21418
  latest_known_version: latestVersion ?? cur.latest_known_version,
21223
- latest_known_mcpb_url: mcpbUrl ?? cur.latest_known_mcpb_url,
21419
+ latest_known_install_url: installUrl ?? cur.latest_known_install_url,
21224
21420
  latest_known_release_url: releaseUrl ?? cur.latest_known_release_url
21225
21421
  }));
21226
21422
  opts.telemetry.captureUpdateCheck?.({
@@ -21230,7 +21426,7 @@ async function doCheck(opts) {
21230
21426
  const info = buildInfoIfUpgrade(
21231
21427
  currentVersion,
21232
21428
  persisted.latest_known_version,
21233
- persisted.latest_known_mcpb_url,
21429
+ persisted.latest_known_install_url,
21234
21430
  persisted.latest_known_release_url,
21235
21431
  persisted.suppressed_versions,
21236
21432
  persisted.remind_until,
@@ -21239,22 +21435,22 @@ async function doCheck(opts) {
21239
21435
  cachedInfo = info;
21240
21436
  return info;
21241
21437
  }
21242
- function buildInfoIfUpgrade(currentVersion, latestVersion, mcpbUrl, releaseUrl, suppressed, remindUntil, nowMs) {
21243
- if (!latestVersion || !mcpbUrl || !releaseUrl) return null;
21438
+ function buildInfoIfUpgrade(currentVersion, latestVersion, installUrl, releaseUrl, suppressed, remindUntil, nowMs) {
21439
+ if (!latestVersion || !installUrl || !releaseUrl) return null;
21244
21440
  if (compareSemver(latestVersion, currentVersion) <= 0) return null;
21245
21441
  if (suppressed.includes(latestVersion)) return null;
21246
21442
  if (remindUntil && remindUntil > nowMs) return null;
21247
21443
  return {
21248
21444
  current_version: currentVersion,
21249
21445
  latest_version: latestVersion,
21250
- mcpb_url: mcpbUrl,
21446
+ install_url: installUrl,
21251
21447
  release_url: releaseUrl
21252
21448
  };
21253
21449
  }
21254
21450
 
21255
21451
  // src/update-tool.ts
21256
21452
  var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
21257
- 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.";
21258
21454
  function buildAcknowledgeUpdateTool(opts) {
21259
21455
  const now = opts.now ?? Date.now;
21260
21456
  return {
@@ -21290,7 +21486,7 @@ function buildAcknowledgeUpdateTool(opts) {
21290
21486
  action: { type: "string" },
21291
21487
  version: { type: "string" },
21292
21488
  message: { type: "string" },
21293
- mcpb_url: { type: ["string", "null"] },
21489
+ install_url: { type: ["string", "null"] },
21294
21490
  release_url: { type: ["string", "null"] }
21295
21491
  },
21296
21492
  required: ["ok", "action", "version", "message"]
@@ -21324,9 +21520,9 @@ function buildAcknowledgeUpdateTool(opts) {
21324
21520
  ok: true,
21325
21521
  action,
21326
21522
  version,
21327
- mcpb_url: state.latest_known_mcpb_url ?? null,
21523
+ install_url: state.latest_known_install_url ?? null,
21328
21524
  release_url: state.latest_known_release_url ?? null,
21329
- 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."
21330
21526
  };
21331
21527
  }
21332
21528
  if (action === "remind_tomorrow") {
@@ -21429,7 +21625,7 @@ function buildStartHereParagraph(has) {
21429
21625
  }
21430
21626
  function buildUpdateAvailableParagraph(has) {
21431
21627
  if (!has("leadbay_acknowledge_update")) return null;
21432
- 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.";
21433
21629
  }
21434
21630
  function buildRhythmParagraph(has) {
21435
21631
  if (has("leadbay_report_outreach")) {
@@ -21724,16 +21920,47 @@ function buildServer(client, opts = {}) {
21724
21920
  );
21725
21921
  });
21726
21922
  };
21727
- const maybeAttachUpdate = (toolName, result) => {
21728
- if (toolName !== "leadbay_account_status") return;
21923
+ const UPDATE_SURFACE_WAIT_MS = 1500;
21924
+ const maybeAttachUpdate = async (toolName, result) => {
21729
21925
  if (!opts.updateStateStore) return;
21730
21926
  if (result === null || typeof result !== "object" || Array.isArray(result)) {
21731
21927
  return;
21732
21928
  }
21733
- 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
+ }
21734
21951
  if (!info) return;
21735
- result.update_available = info;
21736
- 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) {
21737
21964
  promptedVersionsThisSession.add(info.latest_version);
21738
21965
  telemetry.captureUpdatePrompted?.({
21739
21966
  current_version: serverVersion,
@@ -21907,9 +22134,13 @@ function buildServer(client, opts = {}) {
21907
22134
  notificationsInbox: opts.notificationsInbox,
21908
22135
  signal: extra.signal,
21909
22136
  progress,
21910
- elicit
22137
+ elicit,
22138
+ // Route leadbay_send_feedback to Sentry's feedback inbox (same place
22139
+ // the web app's form lands). NOOP_TELEMETRY returns false, so the
22140
+ // tool reports honestly when telemetry is off.
22141
+ sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
21911
22142
  });
21912
- maybeAttachUpdate(name, result);
22143
+ await maybeAttachUpdate(name, result);
21913
22144
  maybeAttachNotifications(result);
21914
22145
  if (result && typeof result === "object" && result.error === true) {
21915
22146
  const envText = formatErrorForLLM(result);
@@ -22205,7 +22436,7 @@ function parseWriteEnv(env = process.env) {
22205
22436
  }
22206
22437
 
22207
22438
  // src/http-server.ts
22208
- var VERSION = true ? "0.19.2" : "0.0.0-dev";
22439
+ var VERSION = true ? "0.20.0" : "0.0.0-dev";
22209
22440
  var PORT = Number(process.env.PORT ?? 8080);
22210
22441
  var HOST = process.env.HOST ?? "0.0.0.0";
22211
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.2";
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.2";
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.2",
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",