@leadbay/mcp 0.28.0 → 0.30.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.30.0 — 2026-08-19
4
+
5
+ Encode the **single-country rule** across every location-accepting surface
6
+ (product#3951). Each backend serves exactly ONE country, so a country name is
7
+ never a location criterion — whole-country intent means omitting the filter.
8
+ It never failed loudly: the admin-area index excludes country nodes
9
+ (product#3885), so the value trigram-matched a same-named town ("France" → the
10
+ commune of Francs, "United States" → Statesboro) and silently fenced the search
11
+ to one village. 3/3 sessions in the 2026-08-02 acceptance eval passed one; an FR
12
+ session burned six variants inside that fence before answering wrongly.
13
+
14
+ - **The descriptions were telling the agent to do it.** This was a
15
+ contradiction sweep, not a missing rule. `followups-map.md.tmpl:47` said to
16
+ pass `countries ("France", "United States")` and advertised
17
+ `level 2 (country)` as searchable; `pull-followups`, `adjust-audience`,
18
+ `new-lens`, `tour-plan` and the `leadbay_followup_check_in` prompt each
19
+ legitimized country-level values. All rewritten to enumerate the levels the
20
+ argument accepts. (Their `"Bavaria"` examples went too — a German region on a
21
+ US/FR-only product.)
22
+ - **New shared snippet** `heuristics/single-country-universe.md`, included by 8
23
+ tool descriptions and 5 prompts, carrying the rule, the measured failure and
24
+ a recovery that branches — because one recovery is wrong for most cases.
25
+ `country_locations[].axis` and `[].kind` decide it: only the HOME country on
26
+ the INCLUDE axis means "omit the argument". A foreign country is unsupported,
27
+ not unfiltered (re-running unfiltered answers a France question with US
28
+ leads). On the EXCLUDE axis omitting is the inverse of the request. And when
29
+ the argument carries a real place beside the country, only the country comes
30
+ off — "keep the city, drop the country".
31
+ - **Mechanical rejection** in `_country-guard.ts` + `_country-names.ts` (full
32
+ ISO 3166-1, English + French, no new dependency). Delivered in each tool's own
33
+ idiom: composites return `status: "country_level_location"` and write nothing,
34
+ `update_lens_filter` throws (including on `dry_run`), `list_locations` returns
35
+ its empty envelope. Always the first statement of `execute`, so a bad value
36
+ costs zero HTTP.
37
+ - **Exemptions that keep real prospecting working:** a `sovereign` field
38
+ (Guadeloupe/Martinique/Réunion/Guyane valid on FR, Puerto Rico/Guam on US),
39
+ region homonyms (Georgia the state, Jersey), and no foreign alpha-2 rejection
40
+ on US, where 26 ISO codes double as state postal codes. Test sweeps over all
41
+ 50 states + postal codes and all 13 régions + 101 départements hold the line.
42
+ - **New audit** `test/audit/single-country-rule.test.ts` — asserts the rule is
43
+ present in all 11 surfaces AND that none of them still says a country is a
44
+ valid geo value, so the pre-fix state is unmergeable. It imports
45
+ `COUNTRY_LEVEL_LOCATION` from core so a rename cannot leave the prose
46
+ teaching a recovery for an error that no longer exists.
47
+ - **`WORKFLOWS.md`**: row 39 gains the country-is-not-a-territory rule and a
48
+ success criterion; new row 52 "Country-wide scope — omit the location filter"
49
+ with its contract.
50
+ - **Writes stop rather than retry.** On `new_lens` / `adjust_audience` /
51
+ `update_lens_filter`, "drop the country and re-call" persists a lens change
52
+ expressing a scope the workspace already has — the mutation WORKFLOWS.md
53
+ forbids for this ask. The guard stops instead, but only when the country was
54
+ the request's ONLY scope: a sector, a size or a real place elsewhere in the
55
+ request is written as asked. An exclusion that is not a foreign country
56
+ blocks the write outright whatever else survives, since dropping it persists
57
+ the opposite of what was asked.
58
+ - **The country is read, never inferred.** `_meta.region` is the only evidence
59
+ of which country a workspace serves; the user's wording is not, and
60
+ `agent_memory_capture` must never store it — a wrong country there is
61
+ replayed as remembered fact.
62
+ - **Two eval scenarios** under `test/eval/scenarios/country-scope/` (over- and
63
+ under-deliver). Gated behind `EVAL=1`; CI protection is the audit. Run live
64
+ on FR and US staging tenants: no country value reached a geo argument on any
65
+ call, no lens was written, no country was captured to memory, and the country
66
+ named in each answer traced to `_meta.region`.
67
+ - Freed the budget for the snippet by de-padding the `pull-followups` NEXT
68
+ STEPS table (1109 chars of markdown column alignment, no content change) —
69
+ that tool was 52 chars from the 17000 cap.
70
+ - **Closed four holes found reviewing the above.** (1) The wrapper list caught
71
+ "across the United States" and "dans toute la France" but not the bare
72
+ prepositions that carry most real traffic — `in the United States`,
73
+ `en France`, `aux États-Unis`, `dans la France` — so the plainest spelling of
74
+ the bug was the one that still reached `/geo/search`. (2) `EU` and
75
+ `European Union` were covered while **`UE` / `Union européenne`** were not, on
76
+ the one backend whose users write French; the French supra-national spellings
77
+ are in now. (3) A `set_filter` carrying a country BESIDE a real criterion
78
+ emitted one hint that said both "say the result covers everything" and "never
79
+ as covering everything" — the `otherScope` flag was consulted only for writes,
80
+ and both read call sites hardcoded `false` while computing the true value one
81
+ line above. The hint and its sibling caveat now derive from one fact.
82
+ (4) `leadbay_top_accounts_to_activate` rendered with `territory: "France"`
83
+ opened with "pass it as `locations` on the lens" — an instruction to make the
84
+ exact call the rest of the prompt forbids, ~35 lines above the branch that
85
+ would have corrected it. The substituted sentence now carries the country
86
+ caveat itself. The existing audit could not see any of this: it reads the
87
+ UNRENDERED body, where that sentence is still `{{arg:territory_block}}`, so
88
+ `test/audit/prompt-rendered-country-args.test.ts` renders each prompt with
89
+ hostile arguments instead.
90
+ - The shared snippet documented a weaker rule than the code enforces: it named
91
+ the write-stop for a country-only scope and for non-foreign exclusions, but
92
+ not for a **foreign / supra-national INCLUDE carrying other criteria**, which
93
+ the guard does stop. An agent following the snippet could pre-strip the
94
+ country and save a home-country lens for a territory nobody asked about,
95
+ never reaching the guard at all.
96
+
97
+ Known gap: a country passed as an already-resolved numeric admin-area id stays
98
+ invisible client-side (deciding whether id "1234" is a country needs a lookup
99
+ this client does not have). Tracked in product#3939; a test records it.
100
+
3
101
  ## 0.28.0 — 2026-07-31
4
102
 
5
103
  Add **`leadbay_top_accounts_to_activate`** — a prompt that builds a ranked
package/README.md CHANGED
@@ -280,11 +280,10 @@ Leadbay connection OK.
280
280
 
281
281
  ### Claude Desktop / ChatGPT / remote-MCP clients
282
282
 
283
- Leadbay runs a hosted MCP server that any remote-MCP client can connect to without a local install. Pick the URL for your account's region:
283
+ Leadbay runs a hosted MCP server that any remote-MCP client can connect to without a local install. One URL works for every region:
284
284
 
285
285
  ```
286
- https://mcp.leadbay.app/mcp # US accounts
287
- https://mcp.leadbay.app/fr/mcp # FR accounts
286
+ https://mcp.leadbay.app/mcp # all accounts (US and FR)
288
287
  ```
289
288
 
290
289
  - **Claude Desktop**: Settings → Connectors → Add custom connector → paste the URL.
@@ -292,7 +291,7 @@ https://mcp.leadbay.app/fr/mcp # FR accounts
292
291
 
293
292
  On first connect the client runs the Leadbay OAuth sign-in (the server advertises OAuth 2.0 Protected Resource Metadata per RFC 9728 and challenges unauthenticated requests with `401 + WWW-Authenticate`). Sign in once in the browser; the client stores the token and sends it as `Authorization: Bearer <token>` on every request. No token to copy-paste, no local Node install needed.
294
293
 
295
- The region is encoded in the URL because OAuth discovery happens before sign-in and Leadbay tokens are region-scoped a US account uses `/mcp`, a FR account uses `/fr/mcp`. If the sign-in prompt never appears, you're on an old build of the hosted server (pre-0.21.0); it auto-updates on release.
294
+ You don't pick a region. Sign-in goes through Stargate, Leadbay's single region-agnostic OAuth authority, and your region rides in the token itself rather than in the connector path so `/mcp` resolves US and FR accounts alike. `/fr/mcp` still works as a compatibility alias for connectors configured before this change; there is no need to migrate and no benefit to using it. If the sign-in prompt never appears, you're on an old build of the hosted server (pre-0.21.0); it auto-updates on release.
296
295
 
297
296
  **Updates are automatic** — the hosted server is always running the latest published release. You never need to update a config file or restart anything on your side.
298
297