@leadbay/mcp 0.34.0 → 0.35.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,159 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.35.0 — 2026-09-02
4
+
5
+ The MCP no longer keeps any record of the jobs it starts (product#4005,
6
+ product#4039). Every launch already receives a durable id from the Leadbay
7
+ backend, and the status tools now take that id.
8
+
9
+ **Why.** Three tools — `leadbay_bulk_qualify_leads`, `leadbay_import_leads`,
10
+ `leadbay_import_and_qualify` — failed on the hosted server with
11
+ `BULK_TRACKER_UNAVAILABLE` (9 customers, 53 times; `import_and_qualify` never
12
+ once worked there). The file they depended on (`~/.leadbay/bulks.json`) was only
13
+ ever created by the stdio entrypoint. Its own header said it existed *"while the
14
+ Leadbay backend doesn't yet issue a real bulk_id"*; the backend now does.
15
+
16
+ **Breaking.** `bulk_id`, `handle_id` and `qualify_id` are gone as inputs, with
17
+ no transition period — they were valid for minutes on a local install and never
18
+ worked on the server.
19
+
20
+ | Tool | Now returns / takes |
21
+ |---|---|
22
+ | `leadbay_enrich_titles`, `leadbay_bulk_qualify_leads` | return the backend's `notification_id` + `lead_ids` (+ `lens_id`) |
23
+ | `leadbay_import_leads`, `leadbay_import_and_qualify` | return the backend's `importIds` / `import_ids` |
24
+ | `leadbay_bulk_enrich_status`, `leadbay_qualify_status` | take `notification_id` and/or `lead_ids` (+ `lens_id`) |
25
+ | `leadbay_import_status` | takes `importIds`, which it already accepted |
26
+
27
+ `packages/core/src/jobs/bulk-store.ts` and its tests are deleted, along with
28
+ `LEADBAY_BULK_STORE_PATH`, `LEADBAY_BULK_STORE_ALLOW_MEMORY` and the `BULK_*`
29
+ error codes. What survives is a five-minute in-memory guard so an assistant
30
+ retry does not charge twice (`jobs/launch-guard.ts`); it claims the launch
31
+ BEFORE firing it, so a concurrent identical call is told `launch_in_flight`
32
+ rather than handed an empty ticket (product#4039).
33
+
34
+ Verified live on staging through fresh server processes: a job started in one
35
+ process resolves by its backend id from another, including after the
36
+ notification was archived with `leadbay_acknowledge_notification`. The
37
+ notification is listable within a second of the launch ack.
38
+
39
+ Found in that run and fixed in the same release:
40
+
41
+ - **A notification of the wrong kind was answered as if it were the right
42
+ one.** A qualification's notification fed to `leadbay_bulk_enrich_status`
43
+ read as a finished enrichment; an enrichment's fed to `leadbay_qualify_status`
44
+ read as a running qualification. Both tools now check the notification's kind
45
+ (`ENRICH_JOB_WRONG_KIND` / `QUALIFY_JOB_WRONG_KIND`) and name the right tool.
46
+ - **Enrichment notifications do not always carry counters.** On staging the
47
+ finished row had `in_progress` and a title but no `bulk_progress`, so the
48
+ `notification_id`-only call said "not a bulk job". It now says
49
+ `ENRICH_JOB_NO_COUNTERS` with the backend's running/finished flag and asks for
50
+ the `lead_ids` the launch returned, which always answer.
51
+ - `leadbay_qualify_status` no longer throws not-found when the caller also
52
+ passed `lead_ids` + `lens_id`; it answers per lead, as its own hint says to.
53
+ - `leadbay_enrich_titles` declared `notification_id` / `lead_ids` / `reused`
54
+ as inputs instead of outputs; `leadbay_bulk_enrich_status` required
55
+ `notification_id` in its output when the `lead_ids`-only path omits it.
56
+ - The artifact SDK's `lb.enrichment()` (`@leadbay/components`) still read
57
+ `bulk_id`, so every widget enrichment reported done instantly with no job
58
+ polled. It now carries `notification_id` + `lead_ids`.
59
+ - The live eval harness and two live smoke suites still imported the deleted
60
+ store; tool copy in eight places still told the assistant to poll with a
61
+ `bulk_id` / `qualify_id`.
62
+
63
+ **The assistant is now told, in the tool descriptions, that a launched job
64
+ cannot be stopped.** Leadbay has no cancel: enrichment, qualification and
65
+ imports have a launch route and a read route and nothing else
66
+ (`LeadsRoutes.kt`, `ImportsRoutes.kt`, `/1.6` specs). The retired store made
67
+ this reachable in the wrong direction — a host cancellation flipped a local
68
+ record to `cancelled`, and the status tools answered *"no further work is in
69
+ flight … relaunch"*, which spends the quota a second time on rows Leadbay is
70
+ still processing. That text is gone with the store; the true rule now ships on
71
+ the surface the assistant actually reads. Three variants ship, because handing a tool
72
+ the wrong one is its own bug.
73
+
74
+ | Tools | Snippet | Why |
75
+ |---|---|---|
76
+ | `enrich_titles`, `bulk_qualify_leads`, `import_and_qualify` | `heuristics/launched-work-cannot-be-stopped.md` | they call `beginLaunch`, so a re-call inside the window returns the ids the first call produced — best-effort, never a guarantee |
77
+ | `bulk_enrich_status`, `qualify_status`, `import_status` | `heuristics/launched-work-poll-only.md` | read-only. Re-calling launches nothing, and the launcher's "check your quota before retrying" text would stall the poll loop these tools exist for |
78
+ | `qualify_lead`, `enrich_contacts`, `launch_bulk_enrichment` | `heuristics/unguarded-launch.md` | they POST directly with no guard, so the guarded re-call advice would buy a second paid launch. `enrich_contacts` is on the hosted route, so this is not only an advanced-mode concern |
79
+
80
+ Those same three tools shipped `idempotentHint: true`, which this repo defines as
81
+ *"calling the same tool twice with the same arguments is safe and produces the
82
+ same observable outcome (no double-write side-effect)"*. That is the
83
+ machine-readable half of the claim the prose now contradicts, and a host may act
84
+ on it automatically. They are `idempotentHint: false` now. This is the one
85
+ behaviour change in an otherwise text-only release;
86
+ `packages/mcp/test/annotations.test.ts` carried the old value in its expectation
87
+ table and is corrected with the code.
88
+
89
+ The six prompts that already carry the long-running-tool rules carry the guarded
90
+ variant too. `leadbay_import_leads` sits 157 chars under the
91
+ 17,000 cap and states the cancel case in one sentence instead; its own SLOW
92
+ BACKEND paragraph already carries the rest.
93
+
94
+ The rule applies to a launched or running result only, never to a discovery,
95
+ preview or `dry_run` result, and it branches because recovery does. With a
96
+ handle: poll it, and do not launch the work that handle covers again. With a
97
+ handle and a subset the result says never started — `failed[]` entries with
98
+ `error:"not_queued"`, or a `rows_pending_upload` count — re-run that subset
99
+ only. With no result at all: read `leadbay_account_status` first, because the
100
+ double-launch guard is in-memory, five minutes and per process, and
101
+ `bulk-qualify-leads.ts` drops its claim when the launch POST throws even if the
102
+ backend accepted it. The rule says so rather than promising a recovery the guard
103
+ cannot deliver.
104
+
105
+ `cancelled: true` on the two import results is described the same way instead of
106
+ as *"ctx.signal aborted mid-flight"*, each names the flag a timeout uses instead,
107
+ and the import one says the counts can stop moving on a chunk cancelled before
108
+ its mappings were committed (product#4064).
109
+ `packages/mcp/test/audit/launched-work-not-cancellable.test.ts` holds both halves
110
+ — every branch is present on the guarded tools, the no-guard warning on the
111
+ unguarded ones with the guarded advice asserted absent, matched on collapsed
112
+ whitespace so a rewrap cannot silently pass it, and no generated description or
113
+ prompt can tell the assistant to relaunch after a cancel again.
114
+ `packages/mcp/test/launched-work-rule-over-the-wire-e2e.test.ts` proves the same
115
+ thing where it counts, driving the real Hono app on a real socket through
116
+ `StreamableHTTPServerTransport` and reading `tools/list` — the catalogue objects
117
+ the audit reads are not what a chat host receives.
118
+
119
+ ## 0.34.1 — 2026-09-07
120
+
121
+ `leadbay_set_lead_status` threw `TypeError: (params.lead_ids ?? []).filter is
122
+ not a function` five times in 29 s on the hosted route (product#4079, Sentry
123
+ MCP-3T): a scheduled agent passed `lead_ids` as something other than an array.
124
+ The same class hit `leadbay_new_lens` on 2026-08-04 (`texts.filter is not a
125
+ function`, `locations` as a string, MCP-3B). Both times the agent retried the
126
+ identical call four or five times, because a raw TypeError names no argument.
127
+
128
+ The CallTool handler in `server.ts` never checked `inputSchema` before
129
+ `tool.execute`; its own comment said enforcement was ours, and it only enforced
130
+ `_triggered_by`. 32 tools declare a top-level `array` or `object` parameter and
131
+ none of them defended against a string: four threw, two silently corrupted
132
+ (`adjust_audience` spread `"tech"` into `["t","e","c","h"]`), the rest forwarded
133
+ the string to the backend.
134
+
135
+ One guard now runs before `execute`, after the `LAST_PROMPT_REQUIRED` check
136
+ (`findShapeMismatch`): for each top-level schema property whose `type` is
137
+ `array` or `object`, a present non-null argument of the wrong JSON shape returns
138
+ `BAD_INPUT` naming the field, the expected shape and the received type
139
+ (`lead_ids must be a JSON array (got string)`), through the same envelope branch
140
+ a tool's own `BAD_INPUT` takes. So PostHog sees `ok:false error_code:BAD_INPUT`
141
+ and Sentry groups the event with the tool's existing `BAD_INPUT` issue
142
+ (`source:business`) instead of a `source:unexpected` stack fingerprint.
143
+
144
+ Deliberately not done: no coercion of a bare string into a one-element array (a
145
+ JSON-stringified array would become one bogus id and fail per lead inside
146
+ `failed[]`); no scalar checks (hosts send `"20"` for a number and tools coerce);
147
+ no `required`, `items`, nested or `additionalProperties` validation; no runtime
148
+ dependency. `null` counts as absent. One visible change on an existing tool:
149
+ `leadbay_report_outreach` with `verification` given as a string answered
150
+ `VERIFICATION_REQUIRED`; it now answers `BAD_INPUT … must be a JSON object (got
151
+ string)`. Tools executed directly outside the MCP server (OpenClaw consumes
152
+ `@leadbay/core` without `buildServer`) keep the raw throw.
153
+
154
+ Test: `packages/mcp/test/unit/input-shape-guard.test.ts` drives `tools/call`
155
+ through `buildServer` with the incident shapes.
156
+
3
157
  ## 0.34.0 — 2026-09-02
4
158
 
5
159
  The OpenAI app directory rejects an app that sells digital goods — "plugins may
package/MIGRATION.md CHANGED
@@ -1,3 +1,54 @@
1
+ # Migration: leadbay-mcp 0.34.x → 0.35.0
2
+
3
+ The MCP no longer keeps a local job file. Every long-running launch hands back
4
+ the **backend's own id**, so a job started in one process resolves in any other
5
+ — including on the hosted server, which never had the local file.
6
+
7
+ ## The three handles are gone
8
+
9
+ | Was | Pass instead | On |
10
+ |---|---|---|
11
+ | `bulk_id` | `notification_id` (job counters) and/or `lead_ids` (per-lead progress) | `leadbay_bulk_enrich_status` |
12
+ | `qualify_id` | `notification_id` and/or `lead_ids` + `lens_id` | `leadbay_qualify_status` |
13
+ | `handle_id` | `importIds[]` | `leadbay_import_status` |
14
+
15
+ All three are returned by the launch call. Nothing is stored MCP-side, so they
16
+ resolve from a later message, a later conversation, or the next day. A call that
17
+ still passes an old name is answered with an error naming the id to pass, not a
18
+ crash.
19
+
20
+ `leadbay_enrich_titles` renamed its `re_used` output field to `reused`, matching
21
+ `leadbay_bulk_qualify_leads` and `leadbay_import_and_qualify`.
22
+
23
+ `leadbay_import_and_qualify` returns NO qualification `notification_id` — its
24
+ qualify phase runs per-lead. Poll it with the `lead_ids` + `lens_id` it returned;
25
+ its `notification_ids[]` are the FILE-IMPORT ones, polled via
26
+ `leadbay_import_status({importIds})`.
27
+
28
+ ## Knowing a job is finished
29
+
30
+ `leadbay_qualify_status.status` is always `"launched"` — it is not a progress
31
+ field. On the `notification_id` path the job is done when `in_progress` is false;
32
+ `still_running[]` is empty on that path from the first poll and must not be read
33
+ as "done". On the `lead_ids` path, done means `still_running[]` is empty.
34
+
35
+ `leadbay_bulk_enrich_status` answers `ENRICH_JOB_NO_COUNTERS` when the enrichment
36
+ notification carries no per-contact counters (the common case) — re-call with the
37
+ `lead_ids` the launch returned.
38
+
39
+ ## Errors removed
40
+
41
+ `BULK_TRACKER_UNAVAILABLE`, `BULK_INVALID_ID`, `BULK_NOT_FOUND`, `BULK_PENDING`,
42
+ `BULK_LAUNCH_FAILED`, `BULK_WRONG_KIND`, `BULK_CANCELLED`, `BULK_STORE_UNAVAILABLE`
43
+ no longer exist, and neither does `~/.leadbay/bulks.json` or the
44
+ `LEADBAY_BULK_STORE_ALLOW_MEMORY` escape hatch. The replacements are
45
+ `ENRICH_STATUS_INPUT_REQUIRED`, `ENRICH_JOB_NOT_FOUND`, `ENRICH_JOB_WRONG_KIND`,
46
+ `ENRICH_JOB_NO_COUNTERS`, `QUALIFY_STATUS_INPUT_REQUIRED`, `QUALIFY_JOB_NOT_FOUND`
47
+ and `QUALIFY_JOB_WRONG_KIND`. The 0.5.0 sections below describe the old store and
48
+ are kept as history.
49
+
50
+ ---
51
+
1
52
  # Migration: leadbay-mcp 0.5.x → 0.6.0 (UNRELEASED)
2
53
 
3
54
  The "MCP best-practice" upgrade. Five behaviour additions and ONE
package/README.md CHANGED
@@ -460,7 +460,7 @@ Then the final `tools/call` response.
460
460
 
461
461
  ### `notifications/cancelled` — actually cancelling
462
462
 
463
- Send the cancellation by id; the server's `ToolContext.signal` aborts the polling loop within ≤2 seconds, the bulk-store entry is marked `cancelled`, and the next `bulk_enrich_status` returns `BULK_CANCELLED` so the agent stops polling.
463
+ Send the cancellation by id; the server's `ToolContext.signal` aborts the polling loop within ≤2 seconds. The job itself keeps running on the backend — poll `leadbay_bulk_enrich_status` with the `notification_id` later to pick it up.
464
464
 
465
465
  ```json
466
466
  { "jsonrpc": "2.0", "method": "notifications/cancelled",