@leadbay/mcp 0.19.2 → 0.19.3

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,9 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.19.3 — 2026-06-15
4
+
5
+ - **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.
6
+
3
7
  ## 0.19.2 — 2026-06-10
4
8
 
5
9
  - **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
 
@@ -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));
@@ -24207,7 +24445,11 @@ function buildServer(client, opts = {}) {
24207
24445
  notificationsInbox: opts.notificationsInbox,
24208
24446
  signal: extra.signal,
24209
24447
  progress,
24210
- elicit
24448
+ elicit,
24449
+ // Route leadbay_send_feedback to Sentry's feedback inbox (same place
24450
+ // the web app's form lands). NOOP_TELEMETRY returns false, so the
24451
+ // tool reports honestly when telemetry is off.
24452
+ sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
24211
24453
  });
24212
24454
  maybeAttachUpdate(name, result);
24213
24455
  maybeAttachNotifications(result);
@@ -25648,7 +25890,7 @@ var OAUTH_BASE_URLS = {
25648
25890
  fr: "https://staging.api.leadbay.app"
25649
25891
  }
25650
25892
  };
25651
- var VERSION = "0.19.2";
25893
+ var VERSION = "0.19.3";
25652
25894
  var HELP = `
25653
25895
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
25654
25896
 
@@ -25923,7 +26165,7 @@ async function resolveClientFromEnv(logger) {
25923
26165
  logger.info?.("Auto-detecting region via /users/me on us and fr...");
25924
26166
  const probe = async (region) => {
25925
26167
  const c = createClient({ token, region });
25926
- await c.request("GET", "/users/me");
26168
+ await c.request("GET", "/users/me", void 0, { retryOn401: false });
25927
26169
  return c;
25928
26170
  };
25929
26171
  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).
@@ -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: () => {
@@ -21907,7 +22101,11 @@ function buildServer(client, opts = {}) {
21907
22101
  notificationsInbox: opts.notificationsInbox,
21908
22102
  signal: extra.signal,
21909
22103
  progress,
21910
- elicit
22104
+ elicit,
22105
+ // Route leadbay_send_feedback to Sentry's feedback inbox (same place
22106
+ // the web app's form lands). NOOP_TELEMETRY returns false, so the
22107
+ // tool reports honestly when telemetry is off.
22108
+ sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
21911
22109
  });
21912
22110
  maybeAttachUpdate(name, result);
21913
22111
  maybeAttachNotifications(result);
@@ -22205,7 +22403,7 @@ function parseWriteEnv(env = process.env) {
22205
22403
  }
22206
22404
 
22207
22405
  // src/http-server.ts
22208
- var VERSION = true ? "0.19.2" : "0.0.0-dev";
22406
+ var VERSION = true ? "0.19.3" : "0.0.0-dev";
22209
22407
  var PORT = Number(process.env.PORT ?? 8080);
22210
22408
  var HOST = process.env.HOST ?? "0.0.0.0";
22211
22409
  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.19.3";
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.19.3";
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.19.3",
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",