@leadbay/mcp 0.30.0 → 0.31.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/dist/bin.js CHANGED
@@ -8596,6 +8596,11 @@ WHEN NOT TO USE: when the user has named a specific lens \u2014 pass \`lensId\`
8596
8596
 
8597
8597
  The active lens can change between calls (5-min cache + backend \`last_requested_lens\`). If a multi-step workflow depends on staying on one lens, **capture \`response.lens.id\` from the first response and pass it as the \`lensId\` argument on every subsequent Leadbay call** \u2014 including re-pulls, bulk qualifies, and research. (Field-name caveat: response nests it as \`lens.id\`; the parameter is \`lensId\`.) Re-pulling without \`lensId\` after a long-running tool may silently switch to a different lens and discard prior work.
8598
8598
 
8599
+ **EMPTY BATCH \u2014 route on \`empty_reason\`, never loop.** When \`leads\` is empty the response carries \`empty_reason: {code, message, retryable, criteria?, narrow_locations?}\`. \`retryable\` is the only field that decides what you do next:
8600
+
8601
+ - \`retryable: true\` (always \`code: "computing"\`) \u2014 the lens is still building. Say so, pull ONCE more in ~30s. Do not call it empty.
8602
+ - \`retryable: false\` \u2014 no amount of re-pulling, lens-switching or \`leadbay_extend_lens\` can produce leads on these criteria. **Stop calling tools.** Surface \`message\` to the user, name the criteria from \`criteria\` (and \`narrow_locations\` first when present \u2014 a city-scale geo scope is the usual culprit), and offer \`leadbay_adjust_audience\` to widen. A refill on a zero-candidate lens answers "queued", consumes no quota and delivers nothing, so retrying reads as progress while achieving none (product#3995).
8603
+
8599
8604
  ---
8600
8605
 
8601
8606
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
@@ -16481,6 +16486,107 @@ var init_prepare_outreach = __esm({
16481
16486
  }
16482
16487
  });
16483
16488
 
16489
+ // ../core/dist/composite/_empty-lens-reason.js
16490
+ function criteriaOf(filter) {
16491
+ return filter?.lens_filter?.items?.flatMap((i) => i.criteria ?? []) ?? [];
16492
+ }
16493
+ function summariseCriteria(criteria) {
16494
+ const out = {};
16495
+ for (const c of criteria) {
16496
+ if (c.type === "sector_ids") {
16497
+ const key = c.is_excluded ? "excluded_sector_ids" : "sector_ids";
16498
+ out[key] = [...out[key] ?? [], ...c.sectors ?? []];
16499
+ } else if (c.type === "location_ids") {
16500
+ const key = c.is_excluded ? "excluded_location_ids" : "location_ids";
16501
+ out[key] = [...out[key] ?? [], ...c.locations ?? []];
16502
+ } else if (c.type === "size" && !c.is_excluded) {
16503
+ out.sizes = [...out.sizes ?? [], ...c.sizes ?? []];
16504
+ }
16505
+ }
16506
+ return Object.keys(out).length > 0 ? out : void 0;
16507
+ }
16508
+ function narrowLocationsOf(filter, criteria) {
16509
+ const included = new Set(criteria.filter((c) => c.type === "location_ids" && !c.is_excluded).flatMap((c) => c.locations ?? []));
16510
+ if (included.size === 0)
16511
+ return [];
16512
+ const results = filter?.locations?.results ?? [];
16513
+ return results.filter((r) => typeof r.id === "string" && included.has(r.id) && typeof r.level === "number" && r.level >= CITY_LEVEL).map((r) => ({
16514
+ id: r.id,
16515
+ name: typeof r.name === "string" ? r.name : "",
16516
+ level: r.level
16517
+ }));
16518
+ }
16519
+ function narrowGeoSentence(narrow) {
16520
+ const names = narrow.map((n) => n.name).filter(Boolean);
16521
+ if (names.length === 0)
16522
+ return "";
16523
+ return ` Its geography is pinned to ${names.join(", ")} \u2014 a city-scale area or smaller, which on an empty lens is almost always the criterion to relax first.`;
16524
+ }
16525
+ async function diagnoseEmptyLens(client, lensId, computing) {
16526
+ if (computing.wishlist || computing.scores) {
16527
+ return {
16528
+ code: "computing",
16529
+ retryable: true,
16530
+ message: "This lens is still computing its leads. Pull again in ~30s \u2014 do NOT report it as empty yet."
16531
+ };
16532
+ }
16533
+ let row;
16534
+ try {
16535
+ const lenses = await client.request("GET", "/lenses");
16536
+ row = lenses.find((l) => String(l.id) === String(lensId));
16537
+ } catch {
16538
+ }
16539
+ let filter = null;
16540
+ try {
16541
+ filter = await client.request("GET", `/lenses/${lensId}/filter`);
16542
+ } catch {
16543
+ }
16544
+ const criteria = criteriaOf(filter);
16545
+ const summary = summariseCriteria(criteria);
16546
+ const narrow = narrowLocationsOf(filter, criteria);
16547
+ const geo = narrowGeoSentence(narrow);
16548
+ const extras = {
16549
+ ...summary ? { criteria: summary } : {},
16550
+ ...narrow.length > 0 ? { narrow_locations: narrow } : {}
16551
+ };
16552
+ if (row?.not_enough_lead_candidates) {
16553
+ return {
16554
+ code: "no_candidates",
16555
+ retryable: false,
16556
+ message: "This lens's criteria match no companies in the database, so it cannot fill." + geo + " Tell the user and offer to widen the audience (leadbay_adjust_audience) \u2014 extending or re-pulling will not help.",
16557
+ ...extras
16558
+ };
16559
+ }
16560
+ if (row?.not_enough_new_leads) {
16561
+ return {
16562
+ code: "no_new_leads",
16563
+ retryable: false,
16564
+ message: "Every company matching this lens has already been delivered \u2014 there are no NEW leads left on these criteria. Tell the user; offer to widen the audience (leadbay_adjust_audience) or work the existing leads via leadbay_pull_followups.",
16565
+ ...extras
16566
+ };
16567
+ }
16568
+ if (summary) {
16569
+ return {
16570
+ code: "audience_too_narrow",
16571
+ retryable: false,
16572
+ message: "This lens is finished computing and holds zero leads: its criteria intersect to nothing." + geo + " Tell the user which criteria are in play and offer to widen the audience (leadbay_adjust_audience). Do NOT call leadbay_extend_lens \u2014 a refill on a zero-candidate lens reports queued, consumes no quota, and delivers nothing.",
16573
+ ...extras
16574
+ };
16575
+ }
16576
+ return {
16577
+ code: "unknown",
16578
+ retryable: false,
16579
+ message: "This lens is finished computing and holds zero leads, and carries no audience criteria that would explain it. Report this to the user rather than retrying; leadbay_report_friction is the way to flag it to the Leadbay team."
16580
+ };
16581
+ }
16582
+ var CITY_LEVEL;
16583
+ var init_empty_lens_reason = __esm({
16584
+ "../core/dist/composite/_empty-lens-reason.js"() {
16585
+ "use strict";
16586
+ CITY_LEVEL = 7;
16587
+ }
16588
+ });
16589
+
16484
16590
  // ../core/dist/composite/pull-leads.js
16485
16591
  function normalizeLinkedinPage3(v) {
16486
16592
  if (v == null)
@@ -16561,6 +16667,7 @@ var init_pull_leads = __esm({
16561
16667
  "../core/dist/composite/pull-leads.js"() {
16562
16668
  "use strict";
16563
16669
  init_agent_memory();
16670
+ init_empty_lens_reason();
16564
16671
  init_tool_descriptions_generated();
16565
16672
  pullLeads = {
16566
16673
  name: "leadbay_pull_leads",
@@ -16626,6 +16733,34 @@ var init_pull_leads = __esm({
16626
16733
  type: "boolean",
16627
16734
  description: "True if scoring is still running."
16628
16735
  },
16736
+ empty_reason: {
16737
+ type: ["object", "null"],
16738
+ description: "Why this LENS holds zero leads. null whenever leads were returned, and null when this page is empty only because it is past the end of a non-empty lens. `retryable` is the field to route on: true ONLY on code=computing (pull again in ~30s). On every other code re-pulling and leadbay_extend_lens are both futile \u2014 a refill on a zero-candidate lens answers 'queued', consumes no quota and delivers nothing \u2014 so surface `message` to the user and offer leadbay_adjust_audience instead of retrying.",
16739
+ properties: {
16740
+ code: {
16741
+ type: "string",
16742
+ description: "computing | no_candidates | no_new_leads | audience_too_narrow | unknown"
16743
+ },
16744
+ message: {
16745
+ type: "string",
16746
+ description: "The line to surface to the user."
16747
+ },
16748
+ retryable: {
16749
+ type: "boolean",
16750
+ description: "True only while the lens is still computing. False means no amount of re-pulling or extending can produce leads."
16751
+ },
16752
+ criteria: {
16753
+ type: "object",
16754
+ description: "The lens criteria in play \u2014 what the user would have to relax. Present when the lens carries any."
16755
+ },
16756
+ narrow_locations: {
16757
+ type: "array",
16758
+ description: "Include-locations that resolved to a city-scale area or smaller ({id, name, level}). On an empty lens, name these first: this is the fingerprint of a whole-country location that fell through to a same-named village (product#3951).",
16759
+ items: { type: "object" }
16760
+ }
16761
+ },
16762
+ required: ["code", "message", "retryable"]
16763
+ },
16629
16764
  next_steps: {
16630
16765
  type: ["object", "null"],
16631
16766
  description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty; options[1] is the enrich offer (kind:enrich_top_leads \u2014 route it to leadbay_enrich_titles scoped to the leadIds JUST shown (pass leads[].id + the pinned lens.id) with NO titles, so it runs the no-spend discovery preview; quota is only spent after the user picks titles + confirms channels on a follow-up call). When the batch is empty but the lens is still computing (computing_wishlist/computing_scores true), this carries a 'Re-pull in ~30s' option (kind:repull_computing) plus 'Refine audience' \u2014 render the widget so the user waits rather than seeing 'no leads.' null only when the batch is empty AND nothing is computing (a genuinely empty / over-narrow lens).",
@@ -16726,6 +16861,11 @@ var init_pull_leads = __esm({
16726
16861
  computingWishlist: res.computing_wishlist,
16727
16862
  computingScores: res.computing_scores
16728
16863
  });
16864
+ const lensIsEmpty = leadCount === 0 && (res.pagination?.total ?? 0) === 0;
16865
+ const emptyReason = lensIsEmpty ? await diagnoseEmptyLens(client, lensId, {
16866
+ wishlist: res.computing_wishlist,
16867
+ scores: res.computing_scores
16868
+ }) : null;
16729
16869
  return withAgentMemoryMeta(client, {
16730
16870
  lens: { id: lensId },
16731
16871
  leads: res.items.map((lead) => ({
@@ -16737,6 +16877,7 @@ var init_pull_leads = __esm({
16737
16877
  next_page: nextPage,
16738
16878
  computing_wishlist: res.computing_wishlist,
16739
16879
  computing_scores: res.computing_scores,
16880
+ empty_reason: emptyReason,
16740
16881
  next_steps: nextSteps,
16741
16882
  _meta: {
16742
16883
  region: client.region,
@@ -31730,7 +31871,7 @@ var OAUTH_BASE_URLS = {
31730
31871
  fr: "https://staging.api.leadbay.app"
31731
31872
  }
31732
31873
  };
31733
- var VERSION = "0.30.0";
31874
+ var VERSION = "0.31.0";
31734
31875
  var HELP = `
31735
31876
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31736
31877
 
@@ -10924,6 +10924,11 @@ WHEN NOT TO USE: when the user has named a specific lens \u2014 pass \`lensId\`
10924
10924
 
10925
10925
  The active lens can change between calls (5-min cache + backend \`last_requested_lens\`). If a multi-step workflow depends on staying on one lens, **capture \`response.lens.id\` from the first response and pass it as the \`lensId\` argument on every subsequent Leadbay call** \u2014 including re-pulls, bulk qualifies, and research. (Field-name caveat: response nests it as \`lens.id\`; the parameter is \`lensId\`.) Re-pulling without \`lensId\` after a long-running tool may silently switch to a different lens and discard prior work.
10926
10926
 
10927
+ **EMPTY BATCH \u2014 route on \`empty_reason\`, never loop.** When \`leads\` is empty the response carries \`empty_reason: {code, message, retryable, criteria?, narrow_locations?}\`. \`retryable\` is the only field that decides what you do next:
10928
+
10929
+ - \`retryable: true\` (always \`code: "computing"\`) \u2014 the lens is still building. Say so, pull ONCE more in ~30s. Do not call it empty.
10930
+ - \`retryable: false\` \u2014 no amount of re-pulling, lens-switching or \`leadbay_extend_lens\` can produce leads on these criteria. **Stop calling tools.** Surface \`message\` to the user, name the criteria from \`criteria\` (and \`narrow_locations\` first when present \u2014 a city-scale geo scope is the usual culprit), and offer \`leadbay_adjust_audience\` to widen. A refill on a zero-candidate lens answers "queued", consumes no quota and delivers nothing, so retrying reads as progress while achieving none (product#3995).
10931
+
10927
10932
  ---
10928
10933
 
10929
10934
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
@@ -18328,6 +18333,101 @@ var prepareOutreach = {
18328
18333
  }
18329
18334
  };
18330
18335
 
18336
+ // ../core/dist/composite/_empty-lens-reason.js
18337
+ var CITY_LEVEL = 7;
18338
+ function criteriaOf(filter) {
18339
+ return filter?.lens_filter?.items?.flatMap((i) => i.criteria ?? []) ?? [];
18340
+ }
18341
+ function summariseCriteria(criteria) {
18342
+ const out = {};
18343
+ for (const c of criteria) {
18344
+ if (c.type === "sector_ids") {
18345
+ const key = c.is_excluded ? "excluded_sector_ids" : "sector_ids";
18346
+ out[key] = [...out[key] ?? [], ...c.sectors ?? []];
18347
+ } else if (c.type === "location_ids") {
18348
+ const key = c.is_excluded ? "excluded_location_ids" : "location_ids";
18349
+ out[key] = [...out[key] ?? [], ...c.locations ?? []];
18350
+ } else if (c.type === "size" && !c.is_excluded) {
18351
+ out.sizes = [...out.sizes ?? [], ...c.sizes ?? []];
18352
+ }
18353
+ }
18354
+ return Object.keys(out).length > 0 ? out : void 0;
18355
+ }
18356
+ function narrowLocationsOf(filter, criteria) {
18357
+ const included = new Set(criteria.filter((c) => c.type === "location_ids" && !c.is_excluded).flatMap((c) => c.locations ?? []));
18358
+ if (included.size === 0)
18359
+ return [];
18360
+ const results = filter?.locations?.results ?? [];
18361
+ return results.filter((r) => typeof r.id === "string" && included.has(r.id) && typeof r.level === "number" && r.level >= CITY_LEVEL).map((r) => ({
18362
+ id: r.id,
18363
+ name: typeof r.name === "string" ? r.name : "",
18364
+ level: r.level
18365
+ }));
18366
+ }
18367
+ function narrowGeoSentence(narrow) {
18368
+ const names = narrow.map((n) => n.name).filter(Boolean);
18369
+ if (names.length === 0)
18370
+ return "";
18371
+ return ` Its geography is pinned to ${names.join(", ")} \u2014 a city-scale area or smaller, which on an empty lens is almost always the criterion to relax first.`;
18372
+ }
18373
+ async function diagnoseEmptyLens(client, lensId, computing) {
18374
+ if (computing.wishlist || computing.scores) {
18375
+ return {
18376
+ code: "computing",
18377
+ retryable: true,
18378
+ message: "This lens is still computing its leads. Pull again in ~30s \u2014 do NOT report it as empty yet."
18379
+ };
18380
+ }
18381
+ let row;
18382
+ try {
18383
+ const lenses = await client.request("GET", "/lenses");
18384
+ row = lenses.find((l) => String(l.id) === String(lensId));
18385
+ } catch {
18386
+ }
18387
+ let filter = null;
18388
+ try {
18389
+ filter = await client.request("GET", `/lenses/${lensId}/filter`);
18390
+ } catch {
18391
+ }
18392
+ const criteria = criteriaOf(filter);
18393
+ const summary = summariseCriteria(criteria);
18394
+ const narrow = narrowLocationsOf(filter, criteria);
18395
+ const geo = narrowGeoSentence(narrow);
18396
+ const extras = {
18397
+ ...summary ? { criteria: summary } : {},
18398
+ ...narrow.length > 0 ? { narrow_locations: narrow } : {}
18399
+ };
18400
+ if (row?.not_enough_lead_candidates) {
18401
+ return {
18402
+ code: "no_candidates",
18403
+ retryable: false,
18404
+ message: "This lens's criteria match no companies in the database, so it cannot fill." + geo + " Tell the user and offer to widen the audience (leadbay_adjust_audience) \u2014 extending or re-pulling will not help.",
18405
+ ...extras
18406
+ };
18407
+ }
18408
+ if (row?.not_enough_new_leads) {
18409
+ return {
18410
+ code: "no_new_leads",
18411
+ retryable: false,
18412
+ message: "Every company matching this lens has already been delivered \u2014 there are no NEW leads left on these criteria. Tell the user; offer to widen the audience (leadbay_adjust_audience) or work the existing leads via leadbay_pull_followups.",
18413
+ ...extras
18414
+ };
18415
+ }
18416
+ if (summary) {
18417
+ return {
18418
+ code: "audience_too_narrow",
18419
+ retryable: false,
18420
+ message: "This lens is finished computing and holds zero leads: its criteria intersect to nothing." + geo + " Tell the user which criteria are in play and offer to widen the audience (leadbay_adjust_audience). Do NOT call leadbay_extend_lens \u2014 a refill on a zero-candidate lens reports queued, consumes no quota, and delivers nothing.",
18421
+ ...extras
18422
+ };
18423
+ }
18424
+ return {
18425
+ code: "unknown",
18426
+ retryable: false,
18427
+ message: "This lens is finished computing and holds zero leads, and carries no audience criteria that would explain it. Report this to the user rather than retrying; leadbay_report_friction is the way to flag it to the Leadbay team."
18428
+ };
18429
+ }
18430
+
18331
18431
  // ../core/dist/composite/pull-leads.js
18332
18432
  function normalizeLinkedinPage3(v) {
18333
18433
  if (v == null)
@@ -18467,6 +18567,34 @@ var pullLeads = {
18467
18567
  type: "boolean",
18468
18568
  description: "True if scoring is still running."
18469
18569
  },
18570
+ empty_reason: {
18571
+ type: ["object", "null"],
18572
+ description: "Why this LENS holds zero leads. null whenever leads were returned, and null when this page is empty only because it is past the end of a non-empty lens. `retryable` is the field to route on: true ONLY on code=computing (pull again in ~30s). On every other code re-pulling and leadbay_extend_lens are both futile \u2014 a refill on a zero-candidate lens answers 'queued', consumes no quota and delivers nothing \u2014 so surface `message` to the user and offer leadbay_adjust_audience instead of retrying.",
18573
+ properties: {
18574
+ code: {
18575
+ type: "string",
18576
+ description: "computing | no_candidates | no_new_leads | audience_too_narrow | unknown"
18577
+ },
18578
+ message: {
18579
+ type: "string",
18580
+ description: "The line to surface to the user."
18581
+ },
18582
+ retryable: {
18583
+ type: "boolean",
18584
+ description: "True only while the lens is still computing. False means no amount of re-pulling or extending can produce leads."
18585
+ },
18586
+ criteria: {
18587
+ type: "object",
18588
+ description: "The lens criteria in play \u2014 what the user would have to relax. Present when the lens carries any."
18589
+ },
18590
+ narrow_locations: {
18591
+ type: "array",
18592
+ description: "Include-locations that resolved to a city-scale area or smaller ({id, name, level}). On an empty lens, name these first: this is the fingerprint of a whole-country location that fell through to a same-named village (product#3951).",
18593
+ items: { type: "object" }
18594
+ }
18595
+ },
18596
+ required: ["code", "message", "retryable"]
18597
+ },
18470
18598
  next_steps: {
18471
18599
  type: ["object", "null"],
18472
18600
  description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty; options[1] is the enrich offer (kind:enrich_top_leads \u2014 route it to leadbay_enrich_titles scoped to the leadIds JUST shown (pass leads[].id + the pinned lens.id) with NO titles, so it runs the no-spend discovery preview; quota is only spent after the user picks titles + confirms channels on a follow-up call). When the batch is empty but the lens is still computing (computing_wishlist/computing_scores true), this carries a 'Re-pull in ~30s' option (kind:repull_computing) plus 'Refine audience' \u2014 render the widget so the user waits rather than seeing 'no leads.' null only when the batch is empty AND nothing is computing (a genuinely empty / over-narrow lens).",
@@ -18567,6 +18695,11 @@ var pullLeads = {
18567
18695
  computingWishlist: res.computing_wishlist,
18568
18696
  computingScores: res.computing_scores
18569
18697
  });
18698
+ const lensIsEmpty = leadCount === 0 && (res.pagination?.total ?? 0) === 0;
18699
+ const emptyReason = lensIsEmpty ? await diagnoseEmptyLens(client, lensId, {
18700
+ wishlist: res.computing_wishlist,
18701
+ scores: res.computing_scores
18702
+ }) : null;
18570
18703
  return withAgentMemoryMeta(client, {
18571
18704
  lens: { id: lensId },
18572
18705
  leads: res.items.map((lead) => ({
@@ -18578,6 +18711,7 @@ var pullLeads = {
18578
18711
  next_page: nextPage,
18579
18712
  computing_wishlist: res.computing_wishlist,
18580
18713
  computing_scores: res.computing_scores,
18714
+ empty_reason: emptyReason,
18581
18715
  next_steps: nextSteps,
18582
18716
  _meta: {
18583
18717
  region: client.region,
@@ -28307,7 +28441,7 @@ function parseWriteEnv(env = process.env) {
28307
28441
  }
28308
28442
 
28309
28443
  // src/http-server.ts
28310
- var VERSION = true ? "0.30.0" : "0.0.0-dev";
28444
+ var VERSION = true ? "0.31.0" : "0.0.0-dev";
28311
28445
  var PORT = Number(process.env.PORT ?? 8080);
28312
28446
  var HOST = process.env.HOST ?? "0.0.0.0";
28313
28447
  var logger = {
@@ -1804,7 +1804,7 @@ var init_installer_gui = __esm({
1804
1804
  init_install_dxt();
1805
1805
  init_install_shared();
1806
1806
  init_oauth();
1807
- VERSION = true ? "0.30.0" : "0.0.0-dev";
1807
+ VERSION = true ? "0.31.0" : "0.0.0-dev";
1808
1808
  MESSAGES = {
1809
1809
  en: {
1810
1810
  installer: {
@@ -1067,7 +1067,7 @@ async function oauthLogin(opts) {
1067
1067
  }
1068
1068
 
1069
1069
  // installer/installer-gui.ts
1070
- var VERSION = true ? "0.30.0" : "0.0.0-dev";
1070
+ var VERSION = true ? "0.31.0" : "0.0.0-dev";
1071
1071
  var MESSAGES = {
1072
1072
  en: {
1073
1073
  installer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.30.0",
3
+ "version": "0.31.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",