@leadbay/mcp 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.6.0 — UNRELEASED
4
+
5
+ Massive spec-coverage upgrade. Each tool declares **annotations** (`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`); 17 composites + 12 highest-leverage granulars declare typed `outputSchema` + emit `structuredContent`. New surfaces: `prompts/*` (5 canned slash-commands), `resources/*` (`lead://`, `lens://`, `org://taste-profile`), `notifications/progress` (per-lead streaming on every long-running composite), `notifications/cancelled` → `ToolContext.signal` (bulk-store entries marked `cancelled` so subsequent status polls return `BULK_CANCELLED`), `elicitation/create` (refine_prompt clarification + report_outreach user_confirmed anti-poisoning).
6
+
7
+ **Hardening**: every `inputSchema` declares `additionalProperties: false`. Runtime conformance test pins `structuredContent` against `outputSchema` for every declarer (drift-catcher). Static-scan audit of every error-hint string asserts each names a recovery action. `report_outreach.verification` rejects extra keys at runtime (closes the SDK's nested-additionalProperties limitation). `score_0_to_10` deprecated alias of `boost_score` removed in 0.7.0.
8
+
9
+ **Token economy**: pagination payloads carry `has_more` + `next_page`. `research_lead` truncates large payloads with a `truncation_hint`. Per-tool opt-in `response_format: "json" | "markdown"` lets chat-rendering agents pick the cheaper render (research_lead first; pattern reusable). `LEADBAY_DEBUG=1` enables a per-tools/call observability line on stderr.
10
+
11
+ **DXT → MCPB**: bundle now publishes both `leadbay-X.Y.Z.dxt` (legacy) and `leadbay-X.Y.Z.mcpb` (new Claude Desktop format) for one cycle. Manifest `dxt_version` field stays for backwards-compat with current installers; field rename will follow Anthropic's spec when finalised. The two filenames have identical content; downstream installers can match either glob.
12
+
13
+ **Versioning + docs**: 0.4.0 / 0.6.0 bumps; README §3a "Spec primitives in action" adds wire-level JSON-RPC transcripts for every primitive; MIGRATION.md 0.5 → 0.6 walkthrough.
14
+
15
+ ## 0.5.0 — 2026-05-04
16
+
17
+ ### `leadbay_import_and_qualify` — new composite
18
+
19
+ End-to-end import + AI qualification in one call. Wraps `leadbay_import_leads` (chunking, mapping preflight, custom-field validation), then fans out `web_fetch` on every imported leadId, polls until each lead's qualification answers populate, and returns the results. When the wall-clock budget overflows, returns a `qualify_id` UUID handle for resumable retrieval via the new `leadbay_qualify_status` tool.
20
+
21
+ **Inputs** (mostly mirrors `leadbay_import_leads`):
22
+
23
+ - `domains` OR `records` — same shape as import_leads.
24
+ - `mappings.fields` and `mappings.custom_fields` — same as 0.3.0 import_leads. Custom fields surface as first-class via `leadbay_list_mappable_fields` (also new).
25
+ - `dry_run: "preview"` — special mode: uploads the CSV in dry-run and returns the wizard's per-column AI mapping hints + sample rows + custom-field candidates from the org catalog matched against unmapped column names by exact / case-insensitive / fuzzy-substring. NO ai_rescore quota consumed.
26
+ - `total_budget_ms` (default 900_000 = 15 min), `per_lead_budget_ms` (default 90_000), `per_phase_budget_ms` (default 300_000).
27
+ - `skip_already_qualified` (default `true`) — skips `web_fetch` launch on leads with a non-null `ai_agent_lead_score`. Saves quota.
28
+
29
+ **Outputs** (full mode):
30
+
31
+ ```
32
+ {
33
+ kind: "result",
34
+ qualify_id: "<UUIDv4>" | null,
35
+ import_ids: [...],
36
+ imported: [{ leadId, domain?, name, rowId? }],
37
+ not_imported: [...],
38
+ qualified: [{ lead_id, qualifications: [{ question, score, response, computed_at }], qualification_summary, signals_count, ... }],
39
+ still_running: [{ lead_id }],
40
+ failed: [...],
41
+ quota_exceeded: bool,
42
+ skipped_already_qualified: [...],
43
+ reused?: bool, seconds_since_original?: number,
44
+ cancelled?: bool, budget_exhausted?: bool,
45
+ region, _meta
46
+ }
47
+ ```
48
+
49
+ `qualify_id` is persisted to `~/.leadbay/bulks.json` (30-day TTL, 5-min idempotency window) — same store as `leadbay_enrich_titles` but with a `kind: "qualify"` discriminator. Re-calling with the same records+mapping within 5 min returns the same handle (`reused: true`).
50
+
51
+ ### `leadbay_qualify_status` — new resumable retrieval
52
+
53
+ ```
54
+ leadbay_qualify_status({ qualify_id })
55
+ → { qualify_id, status, lead_ids, qualified, still_running, ... }
56
+ ```
57
+
58
+ Refreshes the per-lead state (`/web_fetch` + `/ai_agent_responses`) at call time. No backend mutation. Survives MCP restart.
59
+
60
+ ### `leadbay_list_mappable_fields` — new discovery
61
+
62
+ ```
63
+ leadbay_list_mappable_fields()
64
+ → { standard_fields: [{name, description, mapping_value}], custom_fields: [{id, name, type, description, mapping_value}], region, _meta }
65
+ ```
66
+
67
+ Lists every CRM field the agent can target in `mappings.fields`. Standard fields come from a static catalog with human descriptions; custom fields come from `GET /crm/custom_fields`. The `mapping_value` field is what the agent passes verbatim (e.g., `"CUSTOM.8"`).
68
+
69
+ ### `leadbay_import_leads` 0.3.0 — `mappings.custom_fields` shorthand
70
+
71
+ In 0.2.0 the only way to map a custom field was `mappings.fields[col] = "CUSTOM.<id>"` (raw wire format). 0.3.0 adds `mappings.custom_fields[col] = <id>` (numeric) or `<name>` (string), resolved against `/crm/custom_fields` before the wizard sees it. New error codes: `IMPORT_CUSTOM_FIELD_UNKNOWN`, `IMPORT_INVALID_CUSTOM_MAPPING`, `IMPORT_CUSTOM_FIELD_NAME_AMBIGUOUS`, `IMPORT_CUSTOM_FIELD_CATALOG_REQUIRED`, `IMPORT_MAPPING_DUPLICATE_CUSTOM`. Catalog GET is suppressed when the mapping references no custom fields (saves a round trip).
72
+
73
+ ### Bulk store schema widening
74
+
75
+ `BulkRecord` now has a `kind: "enrich" | "qualify"` discriminator. Old enrich rows (no `kind` field) default to `"enrich"` on read. Existing `leadbay_bulk_enrich_status` callers see no change. Cross-kind id queries are surfaced with the new `BULK_WRONG_KIND` error code that points the caller at the right tool.
76
+
77
+ ### `not_in_lens` partition — terminate the silent infinite-poll
78
+
79
+ Both `leadbay_import_and_qualify` and `leadbay_qualify_status` now surface a `not_in_lens: string[]` array — lead ids that exist in the org (the wizard imported them) but are NOT admitted to the active lens. The backend's `queueAiRescoreForLead` is a no-op for these leads — they will never appear in `qualified[]`. Surfacing them in a distinct partition means the agent's poll loop terminates.
80
+
81
+ Discovered by iter-17 live e2e: imported 4 leads (Apple, Stripe, Datadog, GitHub) into a lens whose scoring rules only admitted Datadog. Datadog got `ai_agent_lead_score: -13` + 3/3 qualifications cleanly; Apple and Stripe sat in `still_running[]` indefinitely with no signal to stop polling. Now they land in `not_in_lens` with an actionable agent prompt: "either change the active lens or accept the lead won't be qualified."
82
+
83
+ `qualify_status` re-checks lens membership at each call — a lead added to the lens after the original `import_and_qualify` automatically migrates from `not_in_lens` back into the regular qualify pipeline on the next status call.
84
+
85
+ ### Aligned with backend `/imports/{id}/leads` (PR #1801)
86
+
87
+ `leadbay_import_and_qualify` now sources the qualify-phase lead set from `GET /1.5/imports/{importId}/leads` (added backend-side 2026-05-06). This is the spec-prescribed source of truth — distinct lead ids the import touched (matched-existing AND newly-created). Replaces the per-record reconciliation pagination for the qualify input. Falls back gracefully to the per-record set when the endpoint is unavailable (older backend / 400 in_progress race).
88
+
89
+ Verified live: import 970bd47a-… (apple.com matched, salesforce.com uncrawled) → /leads returned `{lead_ids: [0a788a89-...]}` cleanly; qualify_id 08ad4555-… retrieved end-to-end.
90
+
91
+ ### Monitor-membership disclosure
92
+
93
+ Both `leadbay_import_leads` and `leadbay_import_and_qualify` now flag in their tool descriptions that imported leads are NOT auto-promoted to the user's Monitor tab. Lens-scoring rules decide — only above-threshold leads get `in_monitor: true`. This was a real surprise discovered in production (journal entry `leadbay-monitor-lens-filter`, captured 2026-05-05). Surfacing it in the description prevents the agent from telling users "I imported your leads, check Monitor" — answer is the CRM-imports list, not Monitor.
94
+
95
+ ### Stable qualification ordering + human_summary
96
+
97
+ Both `leadbay_import_and_qualify` and `leadbay_qualify_status` now sort `qualifications[]` by the org's `ai_agent_questions` catalog order — the same question appears at the same index across calls so LLM agents can position-index reliably. Catalog comes from `client.resolveTasteProfile()` (cached 10min). Falls back to alphabetical when the catalog is empty.
98
+
99
+ Each `qualified[]` entry also carries an optional `human_summary` string of the form `answered X/Y — <signal> on '<question>'[, <signal> on '<question>']` where `<signal>` is `strong positive` (score=20), `positive` (10), `neutral` (0), or `negative` (-10). Top-2 by absolute score. Saves the agent from reading every per-question response when it just needs the gist.
100
+
3
101
  ## 0.4.0 — 2026-05-04
4
102
 
5
103
  ### `leadbay_import_leads` 0.2.0 — custom field mapping
package/MIGRATION.md CHANGED
@@ -1,3 +1,99 @@
1
+ # Migration: leadbay-mcp 0.5.x → 0.6.0 (UNRELEASED)
2
+
3
+ The "MCP best-practice" upgrade. Five behaviour additions and ONE
4
+ softer-than-it-looks behavior callout. All changes are MCP-spec
5
+ aligned (2025-11-25).
6
+
7
+ ## 1. Tool annotations on every tool
8
+
9
+ Every tool now declares `annotations: { readOnlyHint, destructiveHint,
10
+ idempotentHint, openWorldHint, title }` in the `tools/list` payload.
11
+ Capable clients (Claude Desktop, Cursor) use these hints to decide
12
+ whether to auto-approve a call or prompt for confirmation. No agent-
13
+ side change required.
14
+
15
+ ## 2. `additionalProperties: false` on every tool's inputSchema
16
+
17
+ **Behavior callout**: any client that was passing extra unrecognized
18
+ fields will now get a schema rejection. This was a deliberate
19
+ hardening — extra fields were never documented, and the rejection
20
+ closes a prompt-injection surface (the eval doc explicitly endorsed
21
+ this). To migrate: drop the unknown field. The error message names
22
+ the rejected field.
23
+
24
+ If you discover an unknown field in your call shape, double-check the
25
+ relevant tool's documented `inputSchema`; if you believe a field
26
+ should exist, file an issue.
27
+
28
+ ## 3. `outputSchema` + `structuredContent` on top-5 composites
29
+
30
+ `leadbay_pull_leads`, `leadbay_research_lead`, `leadbay_account_status`,
31
+ `leadbay_bulk_qualify_leads`, `leadbay_report_outreach` now declare
32
+ `outputSchema` AND emit `structuredContent` on success alongside the
33
+ existing text content. Clients that read structuredContent get the
34
+ typed payload without re-parsing through the LLM. Backwards-compat:
35
+ text content is unchanged.
36
+
37
+ ## 4. `boost_score` on `research_lead.qualification[]`
38
+
39
+ The qualification entry was previously labelled `score_0_to_10`. The
40
+ actual scale is the discrete `-10|0|10|20` boost (per
41
+ `AiAgentResponse.score`), NOT a 0-10 average. 0.6.0 introduces:
42
+
43
+ ```json
44
+ {
45
+ "question": "...",
46
+ "boost_score": 10,
47
+ "score_scale": "-10|0|10|20",
48
+ "score_0_to_10": 10, // DEPRECATED — same value as boost_score
49
+ "response": "...",
50
+ "computed_at": "..."
51
+ }
52
+ ```
53
+
54
+ `score_0_to_10` is kept as a deprecated alias for one minor version
55
+ and removed in 0.7.0. Switch to `boost_score` now.
56
+
57
+ ## 5. New spec primitives
58
+
59
+ The server now advertises and serves:
60
+
61
+ - **`prompts/*`** — 5 canned slash-commands (`leadbay_daily_check_in`,
62
+ `leadbay_research_a_domain`, `leadbay_refine_audience`,
63
+ `leadbay_log_outreach`, `leadbay_qualify_top_n`). Use these from
64
+ the client UI; agents that read prompts compose workflows
65
+ automatically.
66
+ - **`resources/*`** — three URI schemes addressable by client:
67
+ `lead://{uuid}/profile`, `lens://{id}/definition`,
68
+ `org://taste-profile`. Cache-friendly for capable clients.
69
+ - **`notifications/progress`** — long-running composites (today:
70
+ `leadbay_bulk_qualify_leads`) stream per-lead progress updates
71
+ when the client opts in via `_meta.progressToken`.
72
+ - **`notifications/cancelled`** → `ToolContext.signal` — clients
73
+ that send cancellation now actually stop in-flight work in
74
+ composites that observe `ctx.signal` (`leadbay_import_and_qualify`,
75
+ `leadbay_import_leads`, etc.).
76
+
77
+ Clients without these capabilities negotiate away — backwards-compat
78
+ is preserved.
79
+
80
+ ## 6. Pagination metadata: `has_more` + `next_page`
81
+
82
+ `leadbay_pull_leads` and `leadbay_discover_leads` payloads now
83
+ include `has_more: boolean` and `next_page: number | null` next to
84
+ the existing `pagination` object. Spec-aligned; agents no longer
85
+ need to compute `page < pages - 1`.
86
+
87
+ ## 7. Truncation steering on `research_lead`
88
+
89
+ When the response would exceed ~25k characters, the composite now
90
+ emits `truncated: true` plus `truncation_hint: "Pass concise:true to
91
+ filter to hot signals only."`. Load-bearing fields (qualification,
92
+ firmographics, contacts) are never trimmed; only signals.entries get
93
+ trimmed first.
94
+
95
+ ---
96
+
1
97
  # Migration: leadbay-mcp 0.2.x → 0.3.0
2
98
 
3
99
  This release fixes [product#3504](https://github.com/leadbay/product/issues/3504): the default-installed MCP server's system prompt told the agent to call tools that the server didn't actually expose. Three behavior changes you need to know about.
@@ -210,3 +306,143 @@ Default is read-only (exposeWrite=false, exposeGranular=false).
210
306
  to detect when `enrichment.done` flips.
211
307
  - A `DELETE /lenses/{draftId}` endpoint — not testable in our tenant; treated
212
308
  as best-effort with `orphan_draft_id` surfaced on cleanup failure.
309
+
310
+ ---
311
+
312
+ # Migration: leadbay-mcp 0.4.x → 0.5.0
313
+
314
+ The 0.5.0 release adds three new tools and extends `leadbay_import_leads` to support custom fields. This is purely additive — existing 0.4.x callers see no behavior change.
315
+
316
+ ## What's new
317
+
318
+ | Tool | Purpose |
319
+ |---|---|
320
+ | `leadbay_list_mappable_fields` | Discovery: lists every CRM field (standard + this org's custom fields) the agent can target in `mappings.fields`. |
321
+ | `leadbay_import_and_qualify` | The mission verb: imports leads + triggers AI qualification + returns per-question answers in one call. Resumable via `qualify_id`. |
322
+ | `leadbay_qualify_status` | Retrieves a previously-launched qualification by `qualify_id` (handle persists 30d). |
323
+ | `leadbay_import_leads` 0.3.0 | Adds `mappings.custom_fields` ergonomic shorthand alongside the raw `mappings.fields[col] = "CUSTOM.<id>"` wire format. |
324
+
325
+ ## Worked example: discover → import → qualify
326
+
327
+ ```jsonc
328
+ // 1. Discover what fields are mappable on this org.
329
+ leadbay_list_mappable_fields()
330
+ // → {
331
+ // "standard_fields": [{name:"LEAD_NAME", description:"...", mapping_value:"LEAD_NAME"}, ...],
332
+ // "custom_fields": [{id:"8", name:"priority_test", type:"TEXT", mapping_value:"CUSTOM.8"}, ...],
333
+ // "_meta": {region: "us", endpoint: "GET /crm/custom_fields", latency_ms: 78}
334
+ // }
335
+
336
+ // 2. (Optional) preview the wizard's mapping suggestions for a sample.
337
+ leadbay_import_and_qualify({
338
+ records: [{Brand: "Apple", Site: "apple.com", Priority: "high"}],
339
+ dry_run: "preview"
340
+ })
341
+ // → {
342
+ // kind: "preview",
343
+ // mapping_hints: [{column: "Site", suggested_field: "LEAD_WEBSITE", ai_confidence: 95}],
344
+ // custom_field_candidates: [{column: "Priority", candidates: [{id:"8", mapping_value:"CUSTOM.8", reason:"fuzzy_substring_match"}]}],
345
+ // sample_rows: [{Brand: "Apple", Site: "apple.com", Priority: "high"}],
346
+ // import_id: "<uuid>"
347
+ // }
348
+
349
+ // 3. Run the full flow with the chosen mapping.
350
+ leadbay_import_and_qualify({
351
+ records: [{Brand: "Apple", Site: "apple.com", Priority: "high"}],
352
+ mappings: {
353
+ fields: {Brand: "LEAD_NAME", Site: "LEAD_WEBSITE"},
354
+ custom_fields: {Priority: "priority_test"} // resolved against /crm/custom_fields
355
+ }
356
+ })
357
+ // → {
358
+ // kind: "result",
359
+ // qualify_id: "<uuidv4>", // resumable handle
360
+ // import_ids: ["<uuid>"],
361
+ // imported: [{leadId: "...", domain: "apple.com", name: "APPLE", rowId: "..."}],
362
+ // not_imported: [],
363
+ // qualified: [{
364
+ // lead_id: "...",
365
+ // qualifications: [{question: "Is the company...", score: 20, response: "yes...", computed_at: "..."}, ...],
366
+ // qualification_summary: {answered: 3, total: 3, avg_qualification_boost: 13.3},
367
+ // human_summary: "answered 3/3 — strong positive on 'Is the company...', positive on '...'",
368
+ // signals_count: 12
369
+ // }],
370
+ // still_running: [],
371
+ // chosen_budgets: {strategy: "small", total_budget_ms: 180000, wall_clock_estimate_ms: 60000, ...}
372
+ // }
373
+
374
+ // 4. If still_running was non-empty, retrieve later via qualify_id.
375
+ leadbay_qualify_status({qualify_id: "<uuid>"})
376
+ // → same shape, refreshed against backend at call time. failed[] populated
377
+ // for any leads that 404'd between launch and status-check.
378
+ ```
379
+
380
+ ## Custom fields wire format
381
+
382
+ The backend serializer accepts `"CUSTOM.<id>"` as a value in the `mappings.fields` map. 0.3.0 of `leadbay_import_leads` accepts both:
383
+
384
+ ```jsonc
385
+ // raw wire form (passes through unchanged):
386
+ mappings.fields = {col: "CUSTOM.8"}
387
+
388
+ // ergonomic shorthand (resolved against /crm/custom_fields):
389
+ mappings.custom_fields = {col: 8} // numeric id
390
+ mappings.custom_fields = {col: "8"} // string-shaped id
391
+ mappings.custom_fields = {col: "priority_test"} // exact name (case-insensitive fallback)
392
+ ```
393
+
394
+ New error codes (all surfaced with hint pointing at next action):
395
+ `IMPORT_CUSTOM_FIELD_UNKNOWN`, `IMPORT_INVALID_CUSTOM_MAPPING`, `IMPORT_CUSTOM_FIELD_NAME_AMBIGUOUS`, `IMPORT_CUSTOM_FIELD_CATALOG_REQUIRED`, `IMPORT_MAPPING_DUPLICATE_CUSTOM`, `IMPORT_MAPPING_CONFLICT_TARGET`.
396
+
397
+ ## Bulk store schema widening
398
+
399
+ `~/.leadbay/bulks.json` now has a `kind: "enrich" | "qualify"` discriminator. Old enrich rows (no `kind` field) are read with `kind: "enrich"` defaulted in. Existing `leadbay_bulk_enrich_status` callers see no change. Querying an enrich `bulk_id` via `leadbay_qualify_status` (or vice versa) returns the new `BULK_WRONG_KIND` error pointing at the right tool.
400
+
401
+ ## Adaptive budgets
402
+
403
+ When `leadbay_import_and_qualify` is called with no `total_budget_ms` or `per_lead_budget_ms`, the composite picks a strategy from input size:
404
+
405
+ | Input size | Strategy | total_budget_ms | per_lead_budget_ms |
406
+ |---|---|---|---|
407
+ | ≤ 5 leads | small | 3 min | 60s |
408
+ | 6 – 20 leads | default | 10 min | 90s |
409
+ | > 20 leads | large | 25 min | 120s |
410
+
411
+ The chosen values appear on the response as `chosen_budgets: {strategy, total_budget_ms, per_lead_budget_ms, wall_clock_estimate_ms}` so the agent can communicate the wall-clock to the human user.
412
+
413
+ ## See also
414
+
415
+ - [CHANGELOG.md 0.5.0 entry](./CHANGELOG.md) — full release notes.
416
+
417
+ ## Error-code reference (0.5.0)
418
+
419
+ Quick-reference for new error codes shipped in 0.5.0:
420
+
421
+ | Code | When | Next action |
422
+ |---|---|---|
423
+ | `IMPORT_CUSTOM_FIELD_UNKNOWN` | Caller passed `CUSTOM.<id>` or `mappings.custom_fields[col]` that doesn't exist on this org. | Call `leadbay_list_mappable_fields()`; pick a real id/name from `custom_fields[]`. |
424
+ | `IMPORT_INVALID_CUSTOM_MAPPING` | Caller passed a mapping value that's neither a StandardCrmFieldType nor a well-formed `CUSTOM.<digits>`. | Use a value from `leadbay_list_mappable_fields()` `mapping_value` field. |
425
+ | `IMPORT_CUSTOM_FIELD_NAME_AMBIGUOUS` | Two custom fields share the name (case-insensitive) the caller used. | Pass the numeric id instead. |
426
+ | `IMPORT_MAPPING_DUPLICATE_CUSTOM` | Same column appears in both `mappings.fields` and `mappings.custom_fields`. | Pick one of the two maps; remove the duplicate. |
427
+ | `IMPORT_MAPPING_CONFLICT_TARGET` | Two columns map to the same StandardCrmFieldType (e.g. both → `LEAD_NAME`). | Pick the column that contains the real value; drop the others from the mapping. |
428
+ | `IMPORT_CUSTOM_FIELD_CATALOG_REQUIRED` | Internal: catalog couldn't be fetched. | Retry; or use raw `CUSTOM.<id>` in `mappings.fields` instead of shorthand. |
429
+ | `BULK_TRACKER_UNAVAILABLE` | MCP server has no BulkTracker (qualify_id persistence). | Restart with `LEADBAY_BULK_STORE_ALLOW_MEMORY=1` or upgrade. |
430
+ | `BULK_INVALID_ID` | `qualify_id` is not a valid UUIDv4. | Pass the id returned by the prior `leadbay_import_and_qualify` call verbatim. |
431
+ | `BULK_NOT_FOUND` | Handle expired (>30d TTL) or never existed. | Re-launch via `leadbay_import_and_qualify`. |
432
+ | `BULK_PENDING` | Launch in flight or crashed mid-launch. | Retry in a few seconds; if persists >60s, relaunch. |
433
+ | `BULK_LAUNCH_FAILED` | The original launch failed permanently. | Re-launch. |
434
+ | `BULK_WRONG_KIND` | Caller passed an enrich `bulk_id` to `leadbay_qualify_status` (or vice versa). | Switch tools — enrich → `leadbay_bulk_enrich_status`, qualify → `leadbay_qualify_status`. |
435
+ | `IMPORT_PREVIEW_NO_UPLOAD` | Preview mode hit all-malformed input; nothing was uploaded. | Check that at least one record/domain is well-formed. |
436
+
437
+ ## Per-tool prereqs (0.5.0)
438
+
439
+ Quick scan of what each new tool requires before invocation:
440
+
441
+ | Tool | Admin role | Active billing | LEADBAY_MCP_WRITE |
442
+ |---|---|---|---|
443
+ | `leadbay_list_mappable_fields` | no | no | no (read-only) |
444
+ | `leadbay_qualify_status` | no | no | no (read-only) |
445
+ | `leadbay_import_and_qualify` | yes | yes | yes (`=1`, default ON in 0.3.0+) |
446
+ | `leadbay_import_leads` (0.3.0) | yes | yes | yes |
447
+
448
+ Pre-flight: call `leadbay_account_status` to read `user.admin` and `organization.plan`. Both writes will return typed errors (`IMPORT_ADMIN_REQUIRED` / `IMPORT_BILLING_REQUIRED` / `FORBIDDEN`) at the call site too — pre-flighting just gives the agent a chance to ask the user politely instead of attempting a write that 403s.