@leadbay/mcp 0.23.8 → 0.23.10

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,19 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.23.10 — 2026-07-01
4
+
5
+ A freshly-created lens no longer reads as "empty" (product#3833).
6
+
7
+ - **`leadbay_new_lens`** — creating a lens with criteria queues an asynchronous backend wishlist rebuild, so an immediate `leadbay_pull_leads` can read empty for a few seconds while it computes. The `created` result now carries `computing_wishlist: true` and a "leads stream in — pull in ~30s" message so the agent waits instead of reporting an empty lens. A criteria-less clone inherits the base lens's leads immediately (`computing_wishlist: false`).
8
+ - **`leadbay_pull_leads`** — on an EMPTY page where the backend reports the lens is still computing (`computing_wishlist`/`computing_scores`), `next_steps` now carries a single "Re-pull in ~30s" option (`kind: repull_computing`) instead of `null`, so the agent renders a wait-and-retry widget rather than "no leads." An empty page with nothing computing still returns `null` (a genuinely empty / over-narrow lens — no fabricated leads).
9
+ - Confirmed against `leadbay/backend` (`POST /lenses/:id/filter` → `queueRefreshJob` → `RefreshLens`) and a live probe on `api-us`: a bare clone shows leads instantly; a filtered clone shows `computing_wishlist: true` then settles within ~1–30s.
10
+
11
+ ## 0.23.9 — 2026-06-27
12
+
13
+ MCP data plane now targets backend **/1.6** (was /1.5).
14
+
15
+ - **`client`** — every request path is built from a single `API_VERSION = "1.6"` constant (exported as `API_PREFIX`). `LeadbayClient` (request / requestVoid / requestRawBinary + mock-mode paths) and `bin.ts`'s login probe all reference it, so future backend version bumps are a one-line change. Verified live on `api-us` before shipping: `/1.6` mirrors `/1.5` across the full route surface probed (monitor, lenses, auth/login, kpi, geo) — identical status on every call. Tool-description prose and route-doc comments updated to /1.6 to match.
16
+
3
17
  ## 0.23.8 — 2026-06-26
4
18
 
5
19
  Installer no longer hangs in Claude Cowork / headless terminals (product#3805).
package/README.md CHANGED
@@ -143,11 +143,11 @@ To rotate the local MCP credential, re-run `npx -y @leadbay/mcp install --oauth`
143
143
  ### Install via the Claude Code plugin marketplace
144
144
 
145
145
  ```text
146
- /plugin marketplace add leadbay/leadclaw
146
+ /plugin marketplace add leadbay/mcp
147
147
  ```
148
148
 
149
149
  ```text
150
- /plugin install leadbay@leadbay-leadclaw
150
+ /plugin install leadbay@leadbay-mcp
151
151
  ```
152
152
 
153
153
  Claude Code prompts for Leadbay auth/config through the plugin's `userConfig`. This is equivalent to the npm/CLI install above.
@@ -157,13 +157,13 @@ The plugin install gives you **two surfaces in one shot**:
157
157
  1. **The MCP server** — registered via the plugin's `mcpServers.leadbay` block (boots `@leadbay/mcp` over stdio). This exposes the `leadbay_*` tools to the agent.
158
158
  2. **Six auto-discovered Claude Code skills** under `skills/` — `leadbay_daily_check_in`, `leadbay_research_a_domain`, `leadbay_import_file`, `leadbay_log_outreach`, `leadbay_qualify_top_n`, `leadbay_refine_audience`. These auto-trigger on natural-language matches against each skill's description ("get me leadbay leads today", "research acme.com", etc.) and dispatch to the MCP tools above by name. Each `SKILL.md` is generated by `@leadbay/promptforge` from the same `.md.tmpl` source as the MCP prompt, so the two surfaces never drift.
159
159
 
160
- You can verify the skills installed by running `/skill list` after install. To uninstall everything, `/plugin uninstall leadbay@leadbay-leadclaw` removes the MCP server registration **and** the skills together.
160
+ You can verify the skills installed by running `/skill list` after install. To uninstall everything, `/plugin uninstall leadbay@leadbay-mcp` removes the MCP server registration **and** the skills together.
161
161
 
162
162
  ### Claude Desktop
163
163
 
164
164
  The guided installer writes the local `mcpServers.leadbay` entry in `claude_desktop_config.json` with `npx -y @leadbay/mcp@latest`, like the Linux flow. Restart Claude Desktop after install.
165
165
 
166
- The `.dxt` / `.mcpb` bundle from [Releases](https://github.com/leadbay/leadclaw/releases/latest) remains available as an alternative install path.
166
+ The `.dxt` / `.mcpb` bundle from [Releases](https://github.com/leadbay/mcp/releases/latest) remains available as an alternative install path.
167
167
 
168
168
  ### `npm install -g` says "EACCES" / "permission denied"
169
169
 
@@ -551,7 +551,7 @@ The user's literal text replaces `verification.ref` in the outreach record, and
551
551
 
552
552
  ## 7. Upgrade & rotation
553
553
 
554
- **Upgrade**: the guided installer pins `@leadbay/mcp@latest`, so each MCP session already picks up the newest published version — just restart the client. (If you hand-pinned a specific version in your config, change it to `@latest` or the version you want, then restart.) **0.3.0 enables composite write tools by default** — see [MIGRATION.md](./MIGRATION.md). See also the [changelog](https://github.com/leadbay/leadclaw/releases).
554
+ **Upgrade**: the guided installer pins `@leadbay/mcp@latest`, so each MCP session already picks up the newest published version — just restart the client. (If you hand-pinned a specific version in your config, change it to `@latest` or the version you want, then restart.) **0.3.0 enables composite write tools by default** — see [MIGRATION.md](./MIGRATION.md). See also the [changelog](https://github.com/leadbay/mcp/releases).
555
555
 
556
556
  **Rotate local credential**: re-run `npx -y @leadbay/mcp@latest install --oauth` (or `login --oauth`) — the new credential replaces the old one in your MCP client config.
557
557
 
package/dist/bin.js CHANGED
@@ -68,7 +68,7 @@ async function resolveRegion(email, password, startWith = "us") {
68
68
  const baseUrl = REGIONS[region];
69
69
  const body = JSON.stringify({ email, password });
70
70
  try {
71
- const res = await httpsRequest("POST", `${baseUrl}/1.5/auth/login`, { "Content-Type": "application/json" }, body);
71
+ const res = await httpsRequest("POST", `${baseUrl}${API_PREFIX}/auth/login`, { "Content-Type": "application/json" }, body);
72
72
  if (res.status === 200) {
73
73
  const parsed = JSON.parse(res.body);
74
74
  if (parsed?.token) {
@@ -159,7 +159,7 @@ function parseRetryAfter(value) {
159
159
  }
160
160
  return null;
161
161
  }
162
- var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, REGIONS, _mockFixtures, _mockJournal, LeadbayClient;
162
+ var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
163
163
  var init_client = __esm({
164
164
  "../core/dist/client.js"() {
165
165
  "use strict";
@@ -171,6 +171,8 @@ var init_client = __esm({
171
171
  us: "https://api-us.leadbay.app",
172
172
  fr: "https://api-fr.leadbay.app"
173
173
  };
174
+ API_VERSION = "1.6";
175
+ API_PREFIX = `/${API_VERSION}`;
174
176
  _mockFixtures = null;
175
177
  _mockJournal = [];
176
178
  LeadbayClient = class {
@@ -313,7 +315,7 @@ var init_client = __esm({
313
315
  const retryOn401 = opts?.retryOn401 !== false;
314
316
  await this.acquireSemaphore();
315
317
  try {
316
- const url = `${this._baseUrl}/1.5${path}`;
318
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
317
319
  const headers = {
318
320
  Authorization: `Bearer ${this.token}`
319
321
  };
@@ -348,7 +350,7 @@ var init_client = __esm({
348
350
  }
349
351
  await this.acquireSemaphore();
350
352
  try {
351
- const url = `${this._baseUrl}/1.5${path}`;
353
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
352
354
  const headers = {
353
355
  Authorization: `Bearer ${this.token}`
354
356
  };
@@ -373,7 +375,7 @@ var init_client = __esm({
373
375
  // serialized body (string for text payloads such as CSV; Buffer for binary
374
376
  // uploads). Auth, semaphore, error mapping, _lastMeta, and mock-mode all
375
377
  // mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
376
- // the wizard at POST /1.5/imports.
378
+ // the wizard at POST /1.6/imports.
377
379
  async requestRawBinary(method, path, contentType, body) {
378
380
  if (process.env.LEADBAY_MOCK === "1") {
379
381
  return this.mockRequestBinary(method, path, contentType, body);
@@ -383,7 +385,7 @@ var init_client = __esm({
383
385
  }
384
386
  await this.acquireSemaphore();
385
387
  try {
386
- const url = `${this._baseUrl}/1.5${path}`;
388
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
387
389
  const headers = {
388
390
  Authorization: `Bearer ${this.token}`,
389
391
  "Content-Type": contentType
@@ -407,7 +409,7 @@ var init_client = __esm({
407
409
  }
408
410
  }
409
411
  mockRequest(method, path, body) {
410
- const fullPath = `/1.5${path}`;
412
+ const fullPath = `${API_PREFIX}${path}`;
411
413
  this._lastMeta = {
412
414
  region: this._region,
413
415
  endpoint: `${method} ${path}`,
@@ -430,7 +432,7 @@ var init_client = __esm({
430
432
  };
431
433
  }
432
434
  mockRequestBinary(method, path, contentType, body) {
433
- const fullPath = `/1.5${path}`;
435
+ const fullPath = `${API_PREFIX}${path}`;
434
436
  this._lastMeta = {
435
437
  region: this._region,
436
438
  endpoint: `${method} ${path}`,
@@ -5695,7 +5697,7 @@ WHEN TO USE: at the start of a session to know what the agent can/can't do, afte
5695
5697
 
5696
5698
  WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
5697
5699
  `;
5698
- leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
5700
+ leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.6/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
5699
5701
 
5700
5702
  **When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
5701
5703
 
@@ -6305,7 +6307,7 @@ WHEN NOT TO USE: from agent flow \u2014 leadbay_adjust_audience handles the draf
6305
6307
 
6306
6308
  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\`.
6307
6309
  `;
6308
- leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.5/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
6310
+ leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.6/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
6309
6311
 
6310
6312
  **When a quota window is hit, offer top-up as the FIRST option.** A top-up clears the throttle immediately (no need to wait for the daily/weekly/monthly window reset). The flow:
6311
6313
 
@@ -7409,8 +7411,9 @@ On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
7409
7411
  created (name + resolved sectors/sizes as chips) and ASK the user to confirm
7410
7412
  via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
7411
7413
  "yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
7412
- On \`ambiguous_sectors\` / \`ambiguous_locations\`: surface the candidates to
7413
- pick from.
7414
+ (if \`computing_wishlist:true\`, add that leads stream in \u2014 pull in ~30s, don't
7415
+ report "empty"). On \`ambiguous_sectors\` / \`ambiguous_locations\`: surface the
7416
+ candidates to pick from.
7414
7417
 
7415
7418
  ---
7416
7419
 
@@ -7424,6 +7427,8 @@ Create a brand-new lens (saved audience) and apply its sector/size criteria. Clo
7424
7427
 
7425
7428
  **Does not switch the active lens.** The new lens is created but the user stays on their current one. Offer \`leadbay_my_lenses(switchToLensId=<new id>)\` as a next step if they want to start pulling from it.
7426
7429
 
7430
+ **Leads compute asynchronously.** When the lens has criteria, applying the filter kicks off a backend wishlist rebuild \u2014 the \`created\` result carries \`computing_wishlist:true\`. An immediate \`leadbay_pull_leads\` may read empty for a few seconds while it computes; that lens is *warming up*, not empty. Wait ~30s before the first pull (or offer the user a re-pull). A criteria-less clone inherits the base lens's leads right away (\`computing_wishlist:false\`).
7431
+
7427
7432
  WHEN TO USE: when the user wants a NEW lens. Canonical phrasings: "create a lens called X", "make a new audience for Y", "set up a lens for <sector>".
7428
7433
 
7429
7434
  WHEN NOT TO USE: to EDIT an existing lens \u2014 use \`leadbay_adjust_audience\` (pass \`lensName\` to target one by name). Not for listing/switching (\`leadbay_my_lenses\`) or topping up (\`leadbay_extend_lens\`).
@@ -7477,19 +7482,25 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
7477
7482
  Pick the rows that fit. On \`created\`, the switch + pull rows are the natural
7478
7483
  follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-call.
7479
7484
 
7480
- | Observation | Suggest | Calls |
7481
- |-----------------------------------|------------------------------------------|--------------------------------------------------------|
7482
- | \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
7483
- | \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
7484
- | Lens created | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
7485
- | Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
7486
- | Lens created | "Leave it; keep my current lens active" | (no call) |
7487
- | \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
7488
-
7489
- If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
7490
- invent a tool that doesn't exist.
7485
+ **\`created\` with \`computing_wishlist: true\`** \u2014 the new lens's leads are being
7486
+ (re)computed asynchronously. Do NOT fire an immediate \`leadbay_pull_leads\` and
7487
+ report "empty" \u2014 the lens is warming up, not empty. Tell the user the lens was
7488
+ created and its leads are streaming in, and offer to pull in ~30s.
7489
+
7490
+ | Observation | Suggest | Calls |
7491
+ |-----------------------------------------|------------------------------------------|--------------------------------------------------------|
7492
+ | \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
7493
+ | \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
7494
+ | Lens created, \`computing_wishlist=true\` | "Give it ~30s, then pull leads (the wishlist is still computing)" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` after ~30s |
7495
+ | Lens created (no criteria) | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
7496
+ | Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
7497
+ | Lens created | "Leave it; keep my current lens active" | (no call) |
7498
+ | \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
7499
+
7500
+ If nothing fits, default to "switch to the new lens and pull leads in ~30s
7501
+ (the wishlist may still be computing)" \u2014 never invent a tool that doesn't exist.
7491
7502
  `;
7492
- leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
7503
+ leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.6/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
7493
7504
 
7494
7505
  Sibling of \`leadbay_create_topup_link\`. Use cases differ:
7495
7506
 
@@ -7767,7 +7778,7 @@ table. Detail + status priority below.
7767
7778
 
7768
7779
  Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
7769
7780
 
7770
- Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
7781
+ Backend: wraps \`GET /1.6/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.6/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
7771
7782
 
7772
7783
  **Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field\`(\`_comparison\`), \`yc\`, \`liked\`, \`last_action\` (MonitorActionType enum), \`last_action_date\` (with \`last_days\`).
7773
7784
 
@@ -7949,7 +7960,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7949
7960
 
7950
7961
  ## RENDER (quick)
7951
7962
 
7952
- 3-col markdown table sorted by \`score\` desc \u2014 DO NOT print the numeric
7963
+ 3-col markdown table in the order the tool returns them (the Discover-tab
7964
+ order \u2014 do NOT re-sort by score) \u2014 DO NOT print the numeric
7953
7965
  score. Col 1 = inline-code 10-segment bar (\`\u25B0\` firmographic, \`\u2756\` AI
7954
7966
  booster cap at the right end of the filled run, \`\u25B1\` empty;
7955
7967
  filled=round(score/10), ai=round(avg_boost/3.3)) + \`<br>\` + linked
@@ -7976,7 +7988,7 @@ The active lens can change between calls (5-min cache + backend \`last_requested
7976
7988
 
7977
7989
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
7978
7990
 
7979
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
7991
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
7980
7992
 
7981
7993
  ## Score-bar (10-segment, inline-code wrapped)
7982
7994
 
@@ -8082,6 +8094,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
8082
8094
  | Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
8083
8095
  | Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
8084
8096
  | \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
8097
+ | Batch is EMPTY and \`computing_wishlist\`/\`computing_scores == true\` (e.g. a just-created lens) | render the \`next_steps\` widget \u2014 it carries "Re-pull in ~30s" (first) + "Refine audience". Do NOT report "no leads": the lens is warming up, not empty | leadbay_pull_leads (retry with same lensId after ~30s) |
8085
8098
  | User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
8086
8099
  | Phase 4 research was run (\`research_lead_by_id\` called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) \u2014 ONE call per contact (the tool takes a single leadId + contactId, never a list) |
8087
8100
  If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
@@ -11411,7 +11424,7 @@ async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCoun
11411
11424
  }
11412
11425
  }
11413
11426
  if (!exhaustedPagination) {
11414
- throw client.makeError("IMPORT_PAGINATION_RUNAWAY", `Records pagination exceeded ${maxPagesPerPoll} pages`, `importId=${importId}. Please file a bug at https://github.com/leadbay/leadclaw/issues.`, `GET /imports/${importId}/records`);
11427
+ throw client.makeError("IMPORT_PAGINATION_RUNAWAY", `Records pagination exceeded ${maxPagesPerPoll} pages`, `importId=${importId}. Please file a bug at https://github.com/leadbay/mcp/issues.`, `GET /imports/${importId}/records`);
11415
11428
  }
11416
11429
  const snapshot = { total, transient };
11417
11430
  const settled = transient === 0;
@@ -14472,9 +14485,27 @@ function summarise(responses) {
14472
14485
  return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt };
14473
14486
  }
14474
14487
  function buildPullLeadsNextSteps(args) {
14475
- const { leadCount, hasMore, nextPage } = args;
14476
- if (leadCount <= 0)
14488
+ const { leadCount, hasMore, nextPage, computingWishlist, computingScores } = args;
14489
+ if (leadCount <= 0) {
14490
+ if (computingWishlist || computingScores) {
14491
+ return {
14492
+ question: "This lens is still warming up. What next?",
14493
+ options: [
14494
+ {
14495
+ label: "Re-pull in ~30s",
14496
+ description: "Leads are still being computed for this lens \u2014 wait ~30s, then pull again to see them.",
14497
+ kind: "repull_computing"
14498
+ },
14499
+ {
14500
+ label: "Refine audience",
14501
+ description: "Adjust the lens audience / filters (sector, size, prompt) \u2014 useful if it's coming back sparse.",
14502
+ kind: "refine_audience"
14503
+ }
14504
+ ]
14505
+ };
14506
+ }
14477
14507
  return null;
14508
+ }
14478
14509
  const options = [];
14479
14510
  options.push({
14480
14511
  label: "Triage board",
@@ -14572,7 +14603,7 @@ var init_pull_leads = __esm({
14572
14603
  },
14573
14604
  next_steps: {
14574
14605
  type: ["object", "null"],
14575
- 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. null only when the batch is empty.",
14606
+ 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. 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).",
14576
14607
  properties: {
14577
14608
  question: { type: "string" },
14578
14609
  options: {
@@ -14663,7 +14694,13 @@ var init_pull_leads = __esm({
14663
14694
  const hasMore = currentPage < totalPages - 1;
14664
14695
  const nextPage = hasMore ? currentPage + 1 : null;
14665
14696
  const leadCount = res.items.length;
14666
- const nextSteps = buildPullLeadsNextSteps({ leadCount, hasMore, nextPage });
14697
+ const nextSteps = buildPullLeadsNextSteps({
14698
+ leadCount,
14699
+ hasMore,
14700
+ nextPage,
14701
+ computingWishlist: res.computing_wishlist,
14702
+ computingScores: res.computing_scores
14703
+ });
14667
14704
  return withAgentMemoryMeta(client, {
14668
14705
  lens: { id: lensId },
14669
14706
  leads: res.items.map((lead) => ({
@@ -21816,6 +21853,10 @@ var init_new_lens = __esm({
21816
21853
  items: { type: "object" }
21817
21854
  },
21818
21855
  filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
21856
+ computing_wishlist: {
21857
+ type: "boolean",
21858
+ description: "On 'created': true when the lens has criteria and its wishlist is (re)computing asynchronously \u2014 an immediate leadbay_pull_leads may read empty; wait ~30s. False for a criteria-less clone (it inherits the base lens's leads immediately)."
21859
+ },
21819
21860
  message: { type: "string" },
21820
21861
  _meta: { type: "object" }
21821
21862
  },
@@ -21922,7 +21963,13 @@ var init_new_lens = __esm({
21922
21963
  status: "created",
21923
21964
  lens: { id: created.id, name: created.name },
21924
21965
  filter_applied: merged,
21925
- message: `Created "${created.name}".`,
21966
+ // Applying the filter queued a backend wishlist refresh (#3833). The lens
21967
+ // leads are (re)computed asynchronously, so an immediate pull_leads can
21968
+ // read empty for a few seconds. Signal that here (flag-independent — the
21969
+ // backend's own computing_wishlist can lag right after create) so the
21970
+ // agent waits ~30s instead of reporting an empty lens.
21971
+ computing_wishlist: hasCriteria,
21972
+ message: hasCriteria ? `Created "${created.name}". Leads stream in asynchronously \u2014 call leadbay_pull_leads in ~30s to see them (an immediate pull may show an empty lens while the wishlist computes).` : `Created "${created.name}".`,
21926
21973
  _meta: { region: client.region }
21927
21974
  };
21928
21975
  }
@@ -22669,6 +22716,8 @@ var init_artifact_kit = __esm({
22669
22716
  // ../core/dist/index.js
22670
22717
  var dist_exports = {};
22671
22718
  __export(dist_exports, {
22719
+ API_PREFIX: () => API_PREFIX,
22720
+ API_VERSION: () => API_VERSION,
22672
22721
  AgentMemoryCaptureInputSchema: () => AgentMemoryCaptureInputSchema,
22673
22722
  AgentMemoryEntrySchema: () => AgentMemoryEntrySchema,
22674
22723
  AgentMemoryInjectionError: () => AgentMemoryInjectionError,
@@ -23219,7 +23268,7 @@ Call \`leadbay_pull_leads\` on the resolved lens. **Capture \`response.lens.id\`
23219
23268
 
23220
23269
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
23221
23270
 
23222
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
23271
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
23223
23272
 
23224
23273
  ## Score-bar (10-segment, inline-code wrapped)
23225
23274
 
@@ -23418,7 +23467,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
23418
23467
 
23419
23468
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
23420
23469
 
23421
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
23470
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
23422
23471
 
23423
23472
  ## Score-bar (10-segment, inline-code wrapped)
23424
23473
 
@@ -23548,6 +23597,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
23548
23597
  | Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
23549
23598
  | Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
23550
23599
  | \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
23600
+ | Batch is EMPTY and \`computing_wishlist\`/\`computing_scores == true\` (e.g. a just-created lens) | render the \`next_steps\` widget \u2014 it carries "Re-pull in ~30s" (first) + "Refine audience". Do NOT report "no leads": the lens is warming up, not empty | leadbay_pull_leads (retry with same lensId after ~30s) |
23551
23601
  | User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
23552
23602
  | Phase 4 research was run (\`research_lead_by_id\` called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) \u2014 ONE call per contact (the tool takes a single leadId + contactId, never a list) |
23553
23603
  If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
@@ -24062,7 +24112,7 @@ When \`bulk_qualify_leads\` returns, surface results in two parts.
24062
24112
 
24063
24113
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
24064
24114
 
24065
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
24115
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
24066
24116
 
24067
24117
  ## Score-bar (10-segment, inline-code wrapped)
24068
24118
 
@@ -25170,7 +25220,7 @@ function getCachedUpdateInfo() {
25170
25220
  function getInFlightCheck() {
25171
25221
  return inFlightCheck;
25172
25222
  }
25173
- var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
25223
+ var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/mcp/releases/latest";
25174
25224
  var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
25175
25225
  var FETCH_TIMEOUT_MS = 5e3;
25176
25226
  var USER_AGENT = "leadbay-mcp-update-check";
@@ -27717,7 +27767,7 @@ var OAUTH_BASE_URLS = {
27717
27767
  fr: "https://staging.api.leadbay.app"
27718
27768
  }
27719
27769
  };
27720
- var VERSION = "0.23.8";
27770
+ var VERSION = "0.23.10";
27721
27771
  var HELP = `
27722
27772
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
27723
27773
 
@@ -27784,7 +27834,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
27784
27834
  }
27785
27835
 
27786
27836
  DOCS
27787
- https://github.com/leadbay/leadclaw#readme
27837
+ https://github.com/leadbay/mcp#readme
27788
27838
  `.trim();
27789
27839
  function makeStderrLogger(level) {
27790
27840
  const rank = { debug: 0, info: 1, error: 2 };
@@ -28518,7 +28568,7 @@ async function loginAt(baseUrl, email, password) {
28518
28568
  const https2 = await import("https");
28519
28569
  return await new Promise((resolve, reject) => {
28520
28570
  const body = JSON.stringify({ email, password });
28521
- const u = new URL(baseUrl + "/1.5/auth/login");
28571
+ const u = new URL(baseUrl + `${API_PREFIX}/auth/login`);
28522
28572
  const r = https2.request(
28523
28573
  {
28524
28574
  hostname: u.hostname,
@@ -88,7 +88,7 @@ Call \`leadbay_pull_leads\` on the resolved lens. **Capture \`response.lens.id\`
88
88
 
89
89
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
90
90
 
91
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
91
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
92
92
 
93
93
  ## Score-bar (10-segment, inline-code wrapped)
94
94
 
@@ -287,7 +287,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
287
287
 
288
288
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
289
289
 
290
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
290
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
291
291
 
292
292
  ## Score-bar (10-segment, inline-code wrapped)
293
293
 
@@ -417,6 +417,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
417
417
  | Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
418
418
  | Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
419
419
  | \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
420
+ | Batch is EMPTY and \`computing_wishlist\`/\`computing_scores == true\` (e.g. a just-created lens) | render the \`next_steps\` widget \u2014 it carries "Re-pull in ~30s" (first) + "Refine audience". Do NOT report "no leads": the lens is warming up, not empty | leadbay_pull_leads (retry with same lensId after ~30s) |
420
421
  | User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
421
422
  | Phase 4 research was run (\`research_lead_by_id\` called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) \u2014 ONE call per contact (the tool takes a single leadId + contactId, never a list) |
422
423
  If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
@@ -931,7 +932,7 @@ When \`bulk_qualify_leads\` returns, surface results in two parts.
931
932
 
932
933
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
933
934
 
934
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
935
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
935
936
 
936
937
  ## Score-bar (10-segment, inline-code wrapped)
937
938
 
@@ -1592,6 +1593,8 @@ var REGIONS = {
1592
1593
  us: "https://api-us.leadbay.app",
1593
1594
  fr: "https://api-fr.leadbay.app"
1594
1595
  };
1596
+ var API_VERSION = "1.6";
1597
+ var API_PREFIX = `/${API_VERSION}`;
1595
1598
  function httpsRequest(method, url, headers, body) {
1596
1599
  return new Promise((resolve, reject) => {
1597
1600
  const start = Date.now();
@@ -1647,7 +1650,7 @@ async function resolveRegion(email, password, startWith = "us") {
1647
1650
  const baseUrl = REGIONS[region];
1648
1651
  const body = JSON.stringify({ email, password });
1649
1652
  try {
1650
- const res = await httpsRequest("POST", `${baseUrl}/1.5/auth/login`, { "Content-Type": "application/json" }, body);
1653
+ const res = await httpsRequest("POST", `${baseUrl}${API_PREFIX}/auth/login`, { "Content-Type": "application/json" }, body);
1651
1654
  if (res.status === 200) {
1652
1655
  const parsed = JSON.parse(res.body);
1653
1656
  if (parsed?.token) {
@@ -1861,7 +1864,7 @@ var LeadbayClient = class {
1861
1864
  const retryOn401 = opts?.retryOn401 !== false;
1862
1865
  await this.acquireSemaphore();
1863
1866
  try {
1864
- const url = `${this._baseUrl}/1.5${path}`;
1867
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
1865
1868
  const headers = {
1866
1869
  Authorization: `Bearer ${this.token}`
1867
1870
  };
@@ -1896,7 +1899,7 @@ var LeadbayClient = class {
1896
1899
  }
1897
1900
  await this.acquireSemaphore();
1898
1901
  try {
1899
- const url = `${this._baseUrl}/1.5${path}`;
1902
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
1900
1903
  const headers = {
1901
1904
  Authorization: `Bearer ${this.token}`
1902
1905
  };
@@ -1921,7 +1924,7 @@ var LeadbayClient = class {
1921
1924
  // serialized body (string for text payloads such as CSV; Buffer for binary
1922
1925
  // uploads). Auth, semaphore, error mapping, _lastMeta, and mock-mode all
1923
1926
  // mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
1924
- // the wizard at POST /1.5/imports.
1927
+ // the wizard at POST /1.6/imports.
1925
1928
  async requestRawBinary(method, path, contentType, body) {
1926
1929
  if (process.env.LEADBAY_MOCK === "1") {
1927
1930
  return this.mockRequestBinary(method, path, contentType, body);
@@ -1931,7 +1934,7 @@ var LeadbayClient = class {
1931
1934
  }
1932
1935
  await this.acquireSemaphore();
1933
1936
  try {
1934
- const url = `${this._baseUrl}/1.5${path}`;
1937
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
1935
1938
  const headers = {
1936
1939
  Authorization: `Bearer ${this.token}`,
1937
1940
  "Content-Type": contentType
@@ -1955,7 +1958,7 @@ var LeadbayClient = class {
1955
1958
  }
1956
1959
  }
1957
1960
  mockRequest(method, path, body) {
1958
- const fullPath = `/1.5${path}`;
1961
+ const fullPath = `${API_PREFIX}${path}`;
1959
1962
  this._lastMeta = {
1960
1963
  region: this._region,
1961
1964
  endpoint: `${method} ${path}`,
@@ -1978,7 +1981,7 @@ var LeadbayClient = class {
1978
1981
  };
1979
1982
  }
1980
1983
  mockRequestBinary(method, path, contentType, body) {
1981
- const fullPath = `/1.5${path}`;
1984
+ const fullPath = `${API_PREFIX}${path}`;
1982
1985
  this._lastMeta = {
1983
1986
  region: this._region,
1984
1987
  endpoint: `${method} ${path}`,
@@ -6824,7 +6827,7 @@ WHEN TO USE: at the start of a session to know what the agent can/can't do, afte
6824
6827
 
6825
6828
  WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
6826
6829
  `;
6827
- var leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
6830
+ var leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.6/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
6828
6831
 
6829
6832
  **When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
6830
6833
 
@@ -7434,7 +7437,7 @@ WHEN NOT TO USE: from agent flow \u2014 leadbay_adjust_audience handles the draf
7434
7437
 
7435
7438
  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\`.
7436
7439
  `;
7437
- var leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.5/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
7440
+ var leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.6/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
7438
7441
 
7439
7442
  **When a quota window is hit, offer top-up as the FIRST option.** A top-up clears the throttle immediately (no need to wait for the daily/weekly/monthly window reset). The flow:
7440
7443
 
@@ -8538,8 +8541,9 @@ On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
8538
8541
  created (name + resolved sectors/sizes as chips) and ASK the user to confirm
8539
8542
  via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
8540
8543
  "yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
8541
- On \`ambiguous_sectors\` / \`ambiguous_locations\`: surface the candidates to
8542
- pick from.
8544
+ (if \`computing_wishlist:true\`, add that leads stream in \u2014 pull in ~30s, don't
8545
+ report "empty"). On \`ambiguous_sectors\` / \`ambiguous_locations\`: surface the
8546
+ candidates to pick from.
8543
8547
 
8544
8548
  ---
8545
8549
 
@@ -8553,6 +8557,8 @@ Create a brand-new lens (saved audience) and apply its sector/size criteria. Clo
8553
8557
 
8554
8558
  **Does not switch the active lens.** The new lens is created but the user stays on their current one. Offer \`leadbay_my_lenses(switchToLensId=<new id>)\` as a next step if they want to start pulling from it.
8555
8559
 
8560
+ **Leads compute asynchronously.** When the lens has criteria, applying the filter kicks off a backend wishlist rebuild \u2014 the \`created\` result carries \`computing_wishlist:true\`. An immediate \`leadbay_pull_leads\` may read empty for a few seconds while it computes; that lens is *warming up*, not empty. Wait ~30s before the first pull (or offer the user a re-pull). A criteria-less clone inherits the base lens's leads right away (\`computing_wishlist:false\`).
8561
+
8556
8562
  WHEN TO USE: when the user wants a NEW lens. Canonical phrasings: "create a lens called X", "make a new audience for Y", "set up a lens for <sector>".
8557
8563
 
8558
8564
  WHEN NOT TO USE: to EDIT an existing lens \u2014 use \`leadbay_adjust_audience\` (pass \`lensName\` to target one by name). Not for listing/switching (\`leadbay_my_lenses\`) or topping up (\`leadbay_extend_lens\`).
@@ -8606,19 +8612,25 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
8606
8612
  Pick the rows that fit. On \`created\`, the switch + pull rows are the natural
8607
8613
  follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-call.
8608
8614
 
8609
- | Observation | Suggest | Calls |
8610
- |-----------------------------------|------------------------------------------|--------------------------------------------------------|
8611
- | \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
8612
- | \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
8613
- | Lens created | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
8614
- | Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
8615
- | Lens created | "Leave it; keep my current lens active" | (no call) |
8616
- | \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
8617
-
8618
- If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
8619
- invent a tool that doesn't exist.
8615
+ **\`created\` with \`computing_wishlist: true\`** \u2014 the new lens's leads are being
8616
+ (re)computed asynchronously. Do NOT fire an immediate \`leadbay_pull_leads\` and
8617
+ report "empty" \u2014 the lens is warming up, not empty. Tell the user the lens was
8618
+ created and its leads are streaming in, and offer to pull in ~30s.
8619
+
8620
+ | Observation | Suggest | Calls |
8621
+ |-----------------------------------------|------------------------------------------|--------------------------------------------------------|
8622
+ | \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
8623
+ | \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
8624
+ | Lens created, \`computing_wishlist=true\` | "Give it ~30s, then pull leads (the wishlist is still computing)" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` after ~30s |
8625
+ | Lens created (no criteria) | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
8626
+ | Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
8627
+ | Lens created | "Leave it; keep my current lens active" | (no call) |
8628
+ | \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
8629
+
8630
+ If nothing fits, default to "switch to the new lens and pull leads in ~30s
8631
+ (the wishlist may still be computing)" \u2014 never invent a tool that doesn't exist.
8620
8632
  `;
8621
- var leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
8633
+ var leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.6/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
8622
8634
 
8623
8635
  Sibling of \`leadbay_create_topup_link\`. Use cases differ:
8624
8636
 
@@ -8896,7 +8908,7 @@ table. Detail + status priority below.
8896
8908
 
8897
8909
  Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
8898
8910
 
8899
- Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
8911
+ Backend: wraps \`GET /1.6/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.6/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
8900
8912
 
8901
8913
  **Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field\`(\`_comparison\`), \`yc\`, \`liked\`, \`last_action\` (MonitorActionType enum), \`last_action_date\` (with \`last_days\`).
8902
8914
 
@@ -9078,7 +9090,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
9078
9090
 
9079
9091
  ## RENDER (quick)
9080
9092
 
9081
- 3-col markdown table sorted by \`score\` desc \u2014 DO NOT print the numeric
9093
+ 3-col markdown table in the order the tool returns them (the Discover-tab
9094
+ order \u2014 do NOT re-sort by score) \u2014 DO NOT print the numeric
9082
9095
  score. Col 1 = inline-code 10-segment bar (\`\u25B0\` firmographic, \`\u2756\` AI
9083
9096
  booster cap at the right end of the filled run, \`\u25B1\` empty;
9084
9097
  filled=round(score/10), ai=round(avg_boost/3.3)) + \`<br>\` + linked
@@ -9105,7 +9118,7 @@ The active lens can change between calls (5-min cache + backend \`last_requested
9105
9118
 
9106
9119
  ## RENDERING \u2014 markdown table, three columns, score-bar driven
9107
9120
 
9108
- Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
9121
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
9109
9122
 
9110
9123
  ## Score-bar (10-segment, inline-code wrapped)
9111
9124
 
@@ -9211,6 +9224,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
9211
9224
  | Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
9212
9225
  | Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
9213
9226
  | \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
9227
+ | Batch is EMPTY and \`computing_wishlist\`/\`computing_scores == true\` (e.g. a just-created lens) | render the \`next_steps\` widget \u2014 it carries "Re-pull in ~30s" (first) + "Refine audience". Do NOT report "no leads": the lens is warming up, not empty | leadbay_pull_leads (retry with same lensId after ~30s) |
9214
9228
  | User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
9215
9229
  | Phase 4 research was run (\`research_lead_by_id\` called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) \u2014 ONE call per contact (the tool takes a single leadId + contactId, never a list) |
9216
9230
  If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
@@ -12419,7 +12433,7 @@ async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCoun
12419
12433
  }
12420
12434
  }
12421
12435
  if (!exhaustedPagination) {
12422
- throw client.makeError("IMPORT_PAGINATION_RUNAWAY", `Records pagination exceeded ${maxPagesPerPoll} pages`, `importId=${importId}. Please file a bug at https://github.com/leadbay/leadclaw/issues.`, `GET /imports/${importId}/records`);
12436
+ throw client.makeError("IMPORT_PAGINATION_RUNAWAY", `Records pagination exceeded ${maxPagesPerPoll} pages`, `importId=${importId}. Please file a bug at https://github.com/leadbay/mcp/issues.`, `GET /imports/${importId}/records`);
12423
12437
  }
12424
12438
  const snapshot = { total, transient };
12425
12439
  const settled = transient === 0;
@@ -15149,9 +15163,27 @@ function summarise(responses) {
15149
15163
  return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt };
15150
15164
  }
15151
15165
  function buildPullLeadsNextSteps(args) {
15152
- const { leadCount, hasMore, nextPage } = args;
15153
- if (leadCount <= 0)
15166
+ const { leadCount, hasMore, nextPage, computingWishlist, computingScores } = args;
15167
+ if (leadCount <= 0) {
15168
+ if (computingWishlist || computingScores) {
15169
+ return {
15170
+ question: "This lens is still warming up. What next?",
15171
+ options: [
15172
+ {
15173
+ label: "Re-pull in ~30s",
15174
+ description: "Leads are still being computed for this lens \u2014 wait ~30s, then pull again to see them.",
15175
+ kind: "repull_computing"
15176
+ },
15177
+ {
15178
+ label: "Refine audience",
15179
+ description: "Adjust the lens audience / filters (sector, size, prompt) \u2014 useful if it's coming back sparse.",
15180
+ kind: "refine_audience"
15181
+ }
15182
+ ]
15183
+ };
15184
+ }
15154
15185
  return null;
15186
+ }
15155
15187
  const options = [];
15156
15188
  options.push({
15157
15189
  label: "Triage board",
@@ -15243,7 +15275,7 @@ var pullLeads = {
15243
15275
  },
15244
15276
  next_steps: {
15245
15277
  type: ["object", "null"],
15246
- 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. null only when the batch is empty.",
15278
+ 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. 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).",
15247
15279
  properties: {
15248
15280
  question: { type: "string" },
15249
15281
  options: {
@@ -15334,7 +15366,13 @@ var pullLeads = {
15334
15366
  const hasMore = currentPage < totalPages - 1;
15335
15367
  const nextPage = hasMore ? currentPage + 1 : null;
15336
15368
  const leadCount = res.items.length;
15337
- const nextSteps = buildPullLeadsNextSteps({ leadCount, hasMore, nextPage });
15369
+ const nextSteps = buildPullLeadsNextSteps({
15370
+ leadCount,
15371
+ hasMore,
15372
+ nextPage,
15373
+ computingWishlist: res.computing_wishlist,
15374
+ computingScores: res.computing_scores
15375
+ });
15338
15376
  return withAgentMemoryMeta(client, {
15339
15377
  lens: { id: lensId },
15340
15378
  leads: res.items.map((lead) => ({
@@ -21608,6 +21646,10 @@ var newLens = {
21608
21646
  items: { type: "object" }
21609
21647
  },
21610
21648
  filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
21649
+ computing_wishlist: {
21650
+ type: "boolean",
21651
+ description: "On 'created': true when the lens has criteria and its wishlist is (re)computing asynchronously \u2014 an immediate leadbay_pull_leads may read empty; wait ~30s. False for a criteria-less clone (it inherits the base lens's leads immediately)."
21652
+ },
21611
21653
  message: { type: "string" },
21612
21654
  _meta: { type: "object" }
21613
21655
  },
@@ -21714,7 +21756,13 @@ var newLens = {
21714
21756
  status: "created",
21715
21757
  lens: { id: created.id, name: created.name },
21716
21758
  filter_applied: merged,
21717
- message: `Created "${created.name}".`,
21759
+ // Applying the filter queued a backend wishlist refresh (#3833). The lens
21760
+ // leads are (re)computed asynchronously, so an immediate pull_leads can
21761
+ // read empty for a few seconds. Signal that here (flag-independent — the
21762
+ // backend's own computing_wishlist can lag right after create) so the
21763
+ // agent waits ~30s instead of reporting an empty lens.
21764
+ computing_wishlist: hasCriteria,
21765
+ message: hasCriteria ? `Created "${created.name}". Leads stream in asynchronously \u2014 call leadbay_pull_leads in ~30s to see them (an immediate pull may show an empty lens while the wishlist computes).` : `Created "${created.name}".`,
21718
21766
  _meta: { region: client.region }
21719
21767
  };
21720
21768
  }
@@ -22769,7 +22817,7 @@ function getCachedUpdateInfo() {
22769
22817
  function getInFlightCheck() {
22770
22818
  return inFlightCheck;
22771
22819
  }
22772
- var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
22820
+ var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/mcp/releases/latest";
22773
22821
  var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
22774
22822
  var FETCH_TIMEOUT_MS = 5e3;
22775
22823
  var USER_AGENT = "leadbay-mcp-update-check";
@@ -24014,7 +24062,7 @@ function parseWriteEnv(env = process.env) {
24014
24062
  }
24015
24063
 
24016
24064
  // src/http-server.ts
24017
- var VERSION = true ? "0.23.8" : "0.0.0-dev";
24065
+ var VERSION = true ? "0.23.10" : "0.0.0-dev";
24018
24066
  var PORT = Number(process.env.PORT ?? 8080);
24019
24067
  var HOST = process.env.HOST ?? "0.0.0.0";
24020
24068
  var sseSessions = /* @__PURE__ */ new Map();
@@ -24056,6 +24104,24 @@ function applyCors(c) {
24056
24104
  c.header("Access-Control-Allow-Origin", "*");
24057
24105
  c.header("Access-Control-Expose-Headers", "WWW-Authenticate");
24058
24106
  }
24107
+ var DEFAULT_ALLOWED_ORIGINS = [
24108
+ "https://claude.ai",
24109
+ "https://claude.com",
24110
+ "https://chatgpt.com"
24111
+ ];
24112
+ function allowedOrigins() {
24113
+ const extra = (process.env.LEADBAY_MCP_ALLOWED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
24114
+ return /* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...extra]);
24115
+ }
24116
+ function rejectForeignOrigin(c) {
24117
+ const origin = c.req.header("origin");
24118
+ if (!origin) return null;
24119
+ const allowed = allowedOrigins();
24120
+ allowed.add(requestOrigin(c));
24121
+ if (allowed.has(origin)) return null;
24122
+ applyCors(c);
24123
+ return c.json({ error: "forbidden origin" }, 403);
24124
+ }
24059
24125
  function servePrm(c, resourcePath) {
24060
24126
  applyCors(c);
24061
24127
  c.header("Cache-Control", "public, max-age=3600");
@@ -24094,6 +24160,8 @@ app.use("/mcp", MCP_BODY_LIMIT);
24094
24160
  app.use("/fr/mcp", MCP_BODY_LIMIT);
24095
24161
  app.use("/messages", MCP_BODY_LIMIT);
24096
24162
  async function handleStreamable(c, resourcePath) {
24163
+ const foreign = rejectForeignOrigin(c);
24164
+ if (foreign) return foreign;
24097
24165
  const token = extractBearer(c.req.header("authorization"));
24098
24166
  const resolved = await resolveClientFromToken(token);
24099
24167
  if (resolved.authState === "missing" || resolved.authState === "expired") {
@@ -24139,6 +24207,8 @@ async function handleStreamable(c, resourcePath) {
24139
24207
  app.all("/mcp", (c) => handleStreamable(c, "/mcp"));
24140
24208
  app.all("/fr/mcp", (c) => handleStreamable(c, "/fr/mcp"));
24141
24209
  async function handleSse(c, resourcePath) {
24210
+ const foreign = rejectForeignOrigin(c);
24211
+ if (foreign) return foreign;
24142
24212
  const token = extractBearer(c.req.header("authorization"));
24143
24213
  const resolved = await resolveClientFromToken(token);
24144
24214
  if (resolved.authState === "missing" || resolved.authState === "expired") {
@@ -24160,6 +24230,8 @@ async function handleSse(c, resourcePath) {
24160
24230
  app.get("/sse", (c) => handleSse(c, "/sse"));
24161
24231
  app.get("/fr/sse", (c) => handleSse(c, "/fr/sse"));
24162
24232
  app.post("/messages", async (c) => {
24233
+ const foreign = rejectForeignOrigin(c);
24234
+ if (foreign) return foreign;
24163
24235
  const sessionId = c.req.query("sessionId");
24164
24236
  if (!sessionId) {
24165
24237
  return c.json({ error: "missing sessionId" }, 400);
@@ -1764,7 +1764,7 @@ var init_installer_gui = __esm({
1764
1764
  init_install_dxt();
1765
1765
  init_install_shared();
1766
1766
  init_oauth();
1767
- VERSION = true ? "0.23.8" : "0.0.0-dev";
1767
+ VERSION = true ? "0.23.10" : "0.0.0-dev";
1768
1768
  MESSAGES = {
1769
1769
  en: {
1770
1770
  installer: {
@@ -1027,7 +1027,7 @@ async function oauthLogin(opts) {
1027
1027
  }
1028
1028
 
1029
1029
  // installer/installer-gui.ts
1030
- var VERSION = true ? "0.23.8" : "0.0.0-dev";
1030
+ var VERSION = true ? "0.23.10" : "0.0.0-dev";
1031
1031
  var MESSAGES = {
1032
1032
  en: {
1033
1033
  installer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.23.8",
3
+ "version": "0.23.10",
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",
@@ -61,13 +61,13 @@
61
61
  ],
62
62
  "repository": {
63
63
  "type": "git",
64
- "url": "git+https://github.com/leadbay/leadclaw.git",
64
+ "url": "git+https://github.com/leadbay/mcp.git",
65
65
  "directory": "packages/mcp"
66
66
  },
67
67
  "bugs": {
68
- "url": "https://github.com/leadbay/leadclaw/issues"
68
+ "url": "https://github.com/leadbay/mcp/issues"
69
69
  },
70
- "homepage": "https://github.com/leadbay/leadclaw/tree/main/packages/mcp#readme",
70
+ "homepage": "https://github.com/leadbay/mcp/tree/main/packages/mcp#readme",
71
71
  "author": "Leadbay",
72
72
  "license": "MIT"
73
73
  }