@leadbay/mcp 0.39.0 → 0.39.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -6731,7 +6731,7 @@ prose paragraph. Full recipe below.
6731
6731
 
6732
6732
  Build a single-call mixed-mode itinerary for a field sales tour. Combines \`leadbay_pull_followups\` (Monitor leads in the city \u2014 known accounts) with \`leadbay_pull_leads\` (Discover wishlist \u2014 new prospects, then client-side filtered by city) so the agent can answer the canonical #3630 US1 ask: *"I'm visiting Limoges in 4 days \u2014 propose 3 customers + 3 qualified prospects + 3 new high-potential discoveries."*
6733
6733
 
6734
- **Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; pick an id and re-call with \`city_id\`.
6734
+ **Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; re-call with \`city_id\` set to the id you pick AND \`city\` set to that candidate's \`name\`. Both are needed: the id scopes the follow-ups, the name scopes the Discover leads, and with \`city_id\` alone \`discover_leads\` comes back empty.
6735
6735
 
6736
6736
  **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
6737
6737
 
@@ -6761,7 +6761,7 @@ not an itinerary. So for ANY country-level \`city\` \u2014 this workspace's own
6761
6761
  visiting and re-call with that. \`status: "country_level_location"\` carries the
6762
6762
  same instruction in its \`hint\`.
6763
6763
 
6764
- **Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then filters by \`location.city/state/country/full\` substring match against the requested city. The \`discover_filter_note\` string in the response tells the agent the match ratio so it can be honest about coverage ("matched 3/30 by city/state" vs. "matched 12/30").
6764
+ **Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then keeps the leads whose own \`location.city\` names the requested city, and falls back to \`location.state\` only when no city matched (which is what a regional ask like "Texas" or "\xCEle-de-France" looks like). \`location.country\` is never consulted. \`discover_filter_note\` reports the ratio and which field carried it, so the agent can be honest about coverage. **When it says no Discover lead is in the city, say that** \u2014 return the Monitor half and offer \`leadbay_find_new_leads\` for that city. Never fill the gap with leads from elsewhere.
6765
6765
 
6766
6766
  **What \`tour_plan\` does NOT do**: it doesn't persist the tour as a campaign artifact. To do that \u2014 create a "Limoges Tour \u2013 May 24" campaign and attach the selected accounts \u2014 chain into \`leadbay_create_campaign({lead_ids: [...selected_ids], name: 'Limoges Tour \u2013 <date>'})\` after the user picks. See the \`leadbay_plan_tour_in_city\` prompt for the full end-to-end orchestrator.
6767
6767
 
@@ -7718,12 +7718,28 @@ var init_list_lenses = __esm({
7718
7718
  }
7719
7719
  });
7720
7720
 
7721
+ // ../core/dist/interactions.js
7722
+ function reportLeadInteractions(client, lensId, leadIds, types, logger) {
7723
+ if (leadIds.length === 0 || types.length === 0)
7724
+ return;
7725
+ const events = leadIds.flatMap((leadId) => types.map((type) => ({ type, lead_id: leadId, lens_id: String(lensId) })));
7726
+ void client.request("POST", "/interactions", events).catch((err) => {
7727
+ logger?.warn?.(`interactions: ${types.join("+")} for ${leadIds.length} lead(s) not recorded: ${err?.message ?? err?.code ?? err}`);
7728
+ });
7729
+ }
7730
+ var init_interactions = __esm({
7731
+ "../core/dist/interactions.js"() {
7732
+ "use strict";
7733
+ }
7734
+ });
7735
+
7721
7736
  // ../core/dist/tools/discover-leads.js
7722
7737
  var discoverLeads;
7723
7738
  var init_discover_leads = __esm({
7724
7739
  "../core/dist/tools/discover-leads.js"() {
7725
7740
  "use strict";
7726
7741
  init_tool_descriptions_generated();
7742
+ init_interactions();
7727
7743
  discoverLeads = {
7728
7744
  name: "leadbay_discover_leads",
7729
7745
  annotations: {
@@ -7752,11 +7768,12 @@ var init_discover_leads = __esm({
7752
7768
  },
7753
7769
  additionalProperties: false
7754
7770
  },
7755
- execute: async (client, params) => {
7771
+ execute: async (client, params, ctx) => {
7756
7772
  const lensId = params.lensId ?? await client.resolveDefaultLens();
7757
7773
  const page = params.page ?? 0;
7758
7774
  const count = Math.min(params.count ?? 20, 50);
7759
7775
  const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true`);
7776
+ reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
7760
7777
  const totalPages = res.pagination?.pages ?? 0;
7761
7778
  const currentPage = res.pagination?.page ?? page;
7762
7779
  const hasMore = currentPage < totalPages - 1;
@@ -7805,6 +7822,7 @@ var init_get_lead_profile = __esm({
7805
7822
  "../core/dist/tools/get-lead-profile.js"() {
7806
7823
  "use strict";
7807
7824
  init_tool_descriptions_generated();
7825
+ init_interactions();
7808
7826
  getLeadProfile = {
7809
7827
  name: "leadbay_get_lead_profile",
7810
7828
  annotations: {
@@ -7852,13 +7870,9 @@ var init_get_lead_profile = __esm({
7852
7870
  },
7853
7871
  required: ["lead", "contacts"]
7854
7872
  },
7855
- execute: async (client, params) => {
7873
+ execute: async (client, params, ctx) => {
7856
7874
  const lensId = params.lensId ?? await client.resolveDefaultLens();
7857
- void client.request("POST", "/interactions", [
7858
- { type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
7859
- { type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
7860
- ]).catch(() => {
7861
- });
7875
+ reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
7862
7876
  const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
7863
7877
  client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
7864
7878
  client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
@@ -14291,6 +14305,7 @@ var init_pull_leads = __esm({
14291
14305
  "../core/dist/composite/pull-leads.js"() {
14292
14306
  "use strict";
14293
14307
  init_lead_order();
14308
+ init_interactions();
14294
14309
  init_empty_lens_reason();
14295
14310
  init_tool_descriptions_generated();
14296
14311
  pullLeads = {
@@ -14429,6 +14444,9 @@ var init_pull_leads = __esm({
14429
14444
  return resolvedOrder.error;
14430
14445
  const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
14431
14446
  const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true${orderQs}`);
14447
+ if (params._reportSeen !== false) {
14448
+ reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
14449
+ }
14432
14450
  const summaries = await Promise.all(res.items.map(async (lead) => {
14433
14451
  try {
14434
14452
  const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
@@ -14944,13 +14962,33 @@ var init_followups_map = __esm({
14944
14962
  });
14945
14963
 
14946
14964
  // ../core/dist/composite/tour-plan.js
14947
- function cityMatches(lead, cityHint) {
14948
- if (!cityHint)
14949
- return true;
14950
- const hint = cityHint.toLowerCase();
14951
- const loc = lead?.location ?? {};
14952
- const haystacks = [loc.city, loc.state, loc.country, loc.full].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
14953
- return haystacks.some((h) => h.includes(hint) || hint.includes(h));
14965
+ function normalizeGeo(value) {
14966
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
14967
+ }
14968
+ function townName(value) {
14969
+ return normalizeGeo(value).replace(ADMIN_PREFIX, "");
14970
+ }
14971
+ function cityHintCore(cityHint) {
14972
+ const head = cityHint.split(",")[0] ?? "";
14973
+ const expanded = expandAlias(head);
14974
+ return { name: townName(expanded), isCity: expanded !== head };
14975
+ }
14976
+ function stateFieldOf(lead) {
14977
+ const state = lead?.location?.state;
14978
+ return typeof state === "string" ? normalizeGeo(state) : "";
14979
+ }
14980
+ function filterDiscoverByCity(leads, cityHint) {
14981
+ const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
14982
+ if (!hint.name)
14983
+ return { leads, matchedOn: null };
14984
+ const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
14985
+ const byCity = leads.filter((l) => cityOf(l) === hint.name);
14986
+ if (byCity.length > 0)
14987
+ return { leads: byCity, matchedOn: "city" };
14988
+ if (hint.isCity)
14989
+ return { leads: [], matchedOn: "city" };
14990
+ const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
14991
+ return { leads: byState, matchedOn: "state" };
14954
14992
  }
14955
14993
  function toMapLocation(lead, mode) {
14956
14994
  const pos = lead?.location?.pos;
@@ -15000,17 +15038,20 @@ function buildMap(monitorLeads, discoverLeads2) {
15000
15038
  }
15001
15039
  };
15002
15040
  }
15003
- var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, tourPlan;
15041
+ var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, ADMIN_PREFIX, tourPlan;
15004
15042
  var init_tour_plan = __esm({
15005
15043
  "../core/dist/composite/tour-plan.js"() {
15006
15044
  "use strict";
15007
15045
  init_pull_followups();
15008
15046
  init_pull_leads();
15047
+ init_interactions();
15009
15048
  init_country_guard();
15049
+ init_geo_helpers();
15010
15050
  init_tool_descriptions_generated();
15011
15051
  DEFAULT_FOLLOWUPS_COUNT = 6;
15012
15052
  DEFAULT_DISCOVER_COUNT = 6;
15013
15053
  DISCOVER_OVER_PULL = 30;
15054
+ ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
15014
15055
  tourPlan = {
15015
15056
  name: "leadbay_tour_plan",
15016
15057
  annotations: {
@@ -15030,7 +15071,7 @@ var init_tour_plan = __esm({
15030
15071
  },
15031
15072
  city_id: {
15032
15073
  type: "string",
15033
- description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver."
15074
+ description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver. Pass `city` alongside it with the NAME of the area you picked: the id scopes the Monitor half, and the name is the only thing that can scope the Discover half, which has no server-side geo filter. With `city_id` alone, `discover_leads` comes back empty."
15034
15075
  },
15035
15076
  followups_count: {
15036
15077
  type: "number",
@@ -15137,11 +15178,17 @@ var init_tour_plan = __esm({
15137
15178
  const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
15138
15179
  const [followupsResult, leadsResult] = await Promise.allSettled([
15139
15180
  pullFollowups.execute(client, {
15140
- city: params.city,
15181
+ // A pre-resolved id bypasses the resolver, which is what its own
15182
+ // schema promises. Forwarding the free text alongside it sends the
15183
+ // name back through /geo/search, and the name is the thing that was
15184
+ // ambiguous — so an agent recovering from `ambiguous_locations` by
15185
+ // picking an id would be handed the same ambiguity again. Here the
15186
+ // free text stays behind and scopes the Discover half instead.
15187
+ city: params.city_id ? void 0 : params.city,
15141
15188
  city_id: params.city_id,
15142
15189
  count: followupsCount
15143
15190
  }, ctx),
15144
- pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
15191
+ pullLeads.execute(client, { count: DISCOVER_OVER_PULL, _reportSeen: false }, ctx)
15145
15192
  ]);
15146
15193
  if (followupsResult.status === "fulfilled") {
15147
15194
  const r = followupsResult.value;
@@ -15151,7 +15198,7 @@ var init_tour_plan = __esm({
15151
15198
  location_ambiguities: r.location_ambiguities,
15152
15199
  monitor_leads: [],
15153
15200
  discover_leads: [],
15154
- discover_filter_note: "City was ambiguous; pick an id and re-call to proceed.",
15201
+ discover_filter_note: "City was ambiguous; re-call with `city_id` set to the id you pick AND `city` set to that candidate's `name`. The id scopes the follow-ups; the name is what scopes the Discover leads.",
15155
15202
  map_locations: [],
15156
15203
  map_summary: {
15157
15204
  total_leads: 0,
@@ -15175,9 +15222,27 @@ var init_tour_plan = __esm({
15175
15222
  if (leadsResult.status === "rejected") {
15176
15223
  ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
15177
15224
  }
15178
- const filtered = rawDiscover.filter((l) => cityMatches(l, params.city));
15225
+ const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
15226
+ const knownId = params.city_id ?? (cityName ? void 0 : params.city);
15227
+ const idOnly = Boolean(knownId) && !cityName;
15228
+ const { leads: filtered, matchedOn } = idOnly ? { leads: [], matchedOn: null } : filterDiscoverByCity(rawDiscover, cityName);
15179
15229
  const discoverLeads2 = filtered.slice(0, discoverCount);
15180
- const filterNote = params.city ? `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}'; returning top ${discoverLeads2.length}.` : `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
15230
+ const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
15231
+ if (pulledLensId != null) {
15232
+ reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
15233
+ }
15234
+ let filterNote;
15235
+ if (idOnly) {
15236
+ filterNote = `Discover leads need the NAME of the place, and this call passed an area id (${knownId}) and no name. Re-call with \`city\` set to the name of that area, keeping \`city_id\` so the Monitor half stays on the area you picked. The follow-ups below are already scoped to it.`;
15237
+ } else if (!cityName) {
15238
+ filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
15239
+ } else if (matchedOn === null) {
15240
+ filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
15241
+ } else if (filtered.length === 0) {
15242
+ filterNote = `No Discover lead in the active lens is in '${params.city}' (checked ${rawDiscover.length} candidates by city, then by state/region). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
15243
+ } else {
15244
+ filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
15245
+ }
15181
15246
  return {
15182
15247
  city: params.city ?? null,
15183
15248
  city_id: params.city_id ?? null,
@@ -15943,6 +16008,7 @@ var init_research_lead_by_id = __esm({
15943
16008
  "../core/dist/composite/research-lead-by-id.js"() {
15944
16009
  "use strict";
15945
16010
  init_web_fetch_helpers();
16011
+ init_interactions();
15946
16012
  init_tool_descriptions_generated();
15947
16013
  researchLeadById = {
15948
16014
  name: "leadbay_research_lead_by_id",
@@ -16187,11 +16253,7 @@ var init_research_lead_by_id = __esm({
16187
16253
  }
16188
16254
  const lensId = params.lensId ?? await client.resolveDefaultLens();
16189
16255
  const leadId = params.leadId;
16190
- void client.request("POST", "/interactions", [
16191
- { type: "LEAD_SEEN", leadId, lensId: String(lensId) },
16192
- { type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
16193
- ]).catch(() => {
16194
- });
16256
+ reportLeadInteractions(client, lensId, [leadId], ["LEAD_SEEN", "LEAD_CLICKED"], _ctx?.logger);
16195
16257
  const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
16196
16258
  client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
16197
16259
  client.request("GET", `/leads/${leadId}/ai_agent_responses`),
@@ -17234,6 +17296,7 @@ var init_get_lead_custom_fields = __esm({
17234
17296
  "../core/dist/composite/get-lead-custom-fields.js"() {
17235
17297
  "use strict";
17236
17298
  init_tool_descriptions_generated();
17299
+ init_interactions();
17237
17300
  getLeadCustomFields = {
17238
17301
  name: "leadbay_get_lead_custom_fields",
17239
17302
  annotations: {
@@ -17277,11 +17340,7 @@ var init_get_lead_custom_fields = __esm({
17277
17340
  },
17278
17341
  execute: async (client, params, ctx) => {
17279
17342
  const lensId = params.lensId ?? await client.resolveDefaultLens();
17280
- void client.request("POST", "/interactions", [
17281
- { type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
17282
- { type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
17283
- ]).catch(() => {
17284
- });
17343
+ reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
17285
17344
  const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
17286
17345
  const entries = lead.custom_fields ?? [];
17287
17346
  const needsCatalog = entries.some((e) => !e?.field?.name);
@@ -31319,7 +31378,7 @@ var OAUTH_BASE_URLS = {
31319
31378
  fr: "https://staging.api.leadbay.app"
31320
31379
  }
31321
31380
  };
31322
- var VERSION = "0.39.0";
31381
+ var VERSION = "0.39.2";
31323
31382
  var HELP = `
31324
31383
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31325
31384
 
@@ -9679,7 +9679,7 @@ prose paragraph. Full recipe below.
9679
9679
 
9680
9680
  Build a single-call mixed-mode itinerary for a field sales tour. Combines \`leadbay_pull_followups\` (Monitor leads in the city \u2014 known accounts) with \`leadbay_pull_leads\` (Discover wishlist \u2014 new prospects, then client-side filtered by city) so the agent can answer the canonical #3630 US1 ask: *"I'm visiting Limoges in 4 days \u2014 propose 3 customers + 3 qualified prospects + 3 new high-potential discoveries."*
9681
9681
 
9682
- **Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; pick an id and re-call with \`city_id\`.
9682
+ **Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; re-call with \`city_id\` set to the id you pick AND \`city\` set to that candidate's \`name\`. Both are needed: the id scopes the follow-ups, the name scopes the Discover leads, and with \`city_id\` alone \`discover_leads\` comes back empty.
9683
9683
 
9684
9684
  **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
9685
9685
 
@@ -9709,7 +9709,7 @@ not an itinerary. So for ANY country-level \`city\` \u2014 this workspace's own
9709
9709
  visiting and re-call with that. \`status: "country_level_location"\` carries the
9710
9710
  same instruction in its \`hint\`.
9711
9711
 
9712
- **Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then filters by \`location.city/state/country/full\` substring match against the requested city. The \`discover_filter_note\` string in the response tells the agent the match ratio so it can be honest about coverage ("matched 3/30 by city/state" vs. "matched 12/30").
9712
+ **Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then keeps the leads whose own \`location.city\` names the requested city, and falls back to \`location.state\` only when no city matched (which is what a regional ask like "Texas" or "\xCEle-de-France" looks like). \`location.country\` is never consulted. \`discover_filter_note\` reports the ratio and which field carried it, so the agent can be honest about coverage. **When it says no Discover lead is in the city, say that** \u2014 return the Monitor half and offer \`leadbay_find_new_leads\` for that city. Never fill the gap with leads from elsewhere.
9713
9713
 
9714
9714
  **What \`tour_plan\` does NOT do**: it doesn't persist the tour as a campaign artifact. To do that \u2014 create a "Limoges Tour \u2013 May 24" campaign and attach the selected accounts \u2014 chain into \`leadbay_create_campaign({lead_ids: [...selected_ids], name: 'Limoges Tour \u2013 <date>'})\` after the user picks. See the \`leadbay_plan_tour_in_city\` prompt for the full end-to-end orchestrator.
9715
9715
 
@@ -10649,6 +10649,16 @@ var listLenses = {
10649
10649
  }
10650
10650
  };
10651
10651
 
10652
+ // ../core/dist/interactions.js
10653
+ function reportLeadInteractions(client, lensId, leadIds, types, logger2) {
10654
+ if (leadIds.length === 0 || types.length === 0)
10655
+ return;
10656
+ const events = leadIds.flatMap((leadId) => types.map((type) => ({ type, lead_id: leadId, lens_id: String(lensId) })));
10657
+ void client.request("POST", "/interactions", events).catch((err) => {
10658
+ logger2?.warn?.(`interactions: ${types.join("+")} for ${leadIds.length} lead(s) not recorded: ${err?.message ?? err?.code ?? err}`);
10659
+ });
10660
+ }
10661
+
10652
10662
  // ../core/dist/tools/discover-leads.js
10653
10663
  var discoverLeads = {
10654
10664
  name: "leadbay_discover_leads",
@@ -10678,11 +10688,12 @@ var discoverLeads = {
10678
10688
  },
10679
10689
  additionalProperties: false
10680
10690
  },
10681
- execute: async (client, params) => {
10691
+ execute: async (client, params, ctx) => {
10682
10692
  const lensId = params.lensId ?? await client.resolveDefaultLens();
10683
10693
  const page = params.page ?? 0;
10684
10694
  const count = Math.min(params.count ?? 20, 50);
10685
10695
  const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true`);
10696
+ reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
10686
10697
  const totalPages = res.pagination?.pages ?? 0;
10687
10698
  const currentPage = res.pagination?.page ?? page;
10688
10699
  const hasMore = currentPage < totalPages - 1;
@@ -10771,13 +10782,9 @@ var getLeadProfile = {
10771
10782
  },
10772
10783
  required: ["lead", "contacts"]
10773
10784
  },
10774
- execute: async (client, params) => {
10785
+ execute: async (client, params, ctx) => {
10775
10786
  const lensId = params.lensId ?? await client.resolveDefaultLens();
10776
- void client.request("POST", "/interactions", [
10777
- { type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
10778
- { type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
10779
- ]).catch(() => {
10780
- });
10787
+ reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
10781
10788
  const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
10782
10789
  client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
10783
10790
  client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
@@ -16871,6 +16878,9 @@ var pullLeads = {
16871
16878
  return resolvedOrder.error;
16872
16879
  const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
16873
16880
  const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true${orderQs}`);
16881
+ if (params._reportSeen !== false) {
16882
+ reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
16883
+ }
16874
16884
  const summaries = await Promise.all(res.items.map(async (lead) => {
16875
16885
  try {
16876
16886
  const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
@@ -17363,13 +17373,34 @@ var followupsMap = {
17363
17373
  var DEFAULT_FOLLOWUPS_COUNT = 6;
17364
17374
  var DEFAULT_DISCOVER_COUNT = 6;
17365
17375
  var DISCOVER_OVER_PULL = 30;
17366
- function cityMatches(lead, cityHint) {
17367
- if (!cityHint)
17368
- return true;
17369
- const hint = cityHint.toLowerCase();
17370
- const loc = lead?.location ?? {};
17371
- const haystacks = [loc.city, loc.state, loc.country, loc.full].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
17372
- return haystacks.some((h) => h.includes(hint) || hint.includes(h));
17376
+ function normalizeGeo(value) {
17377
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
17378
+ }
17379
+ var ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
17380
+ function townName(value) {
17381
+ return normalizeGeo(value).replace(ADMIN_PREFIX, "");
17382
+ }
17383
+ function cityHintCore(cityHint) {
17384
+ const head = cityHint.split(",")[0] ?? "";
17385
+ const expanded = expandAlias(head);
17386
+ return { name: townName(expanded), isCity: expanded !== head };
17387
+ }
17388
+ function stateFieldOf(lead) {
17389
+ const state = lead?.location?.state;
17390
+ return typeof state === "string" ? normalizeGeo(state) : "";
17391
+ }
17392
+ function filterDiscoverByCity(leads, cityHint) {
17393
+ const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
17394
+ if (!hint.name)
17395
+ return { leads, matchedOn: null };
17396
+ const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
17397
+ const byCity = leads.filter((l) => cityOf(l) === hint.name);
17398
+ if (byCity.length > 0)
17399
+ return { leads: byCity, matchedOn: "city" };
17400
+ if (hint.isCity)
17401
+ return { leads: [], matchedOn: "city" };
17402
+ const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
17403
+ return { leads: byState, matchedOn: "state" };
17373
17404
  }
17374
17405
  function toMapLocation(lead, mode) {
17375
17406
  const pos = lead?.location?.pos;
@@ -17438,7 +17469,7 @@ var tourPlan = {
17438
17469
  },
17439
17470
  city_id: {
17440
17471
  type: "string",
17441
- description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver."
17472
+ description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver. Pass `city` alongside it with the NAME of the area you picked: the id scopes the Monitor half, and the name is the only thing that can scope the Discover half, which has no server-side geo filter. With `city_id` alone, `discover_leads` comes back empty."
17442
17473
  },
17443
17474
  followups_count: {
17444
17475
  type: "number",
@@ -17545,11 +17576,17 @@ var tourPlan = {
17545
17576
  const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
17546
17577
  const [followupsResult, leadsResult] = await Promise.allSettled([
17547
17578
  pullFollowups.execute(client, {
17548
- city: params.city,
17579
+ // A pre-resolved id bypasses the resolver, which is what its own
17580
+ // schema promises. Forwarding the free text alongside it sends the
17581
+ // name back through /geo/search, and the name is the thing that was
17582
+ // ambiguous — so an agent recovering from `ambiguous_locations` by
17583
+ // picking an id would be handed the same ambiguity again. Here the
17584
+ // free text stays behind and scopes the Discover half instead.
17585
+ city: params.city_id ? void 0 : params.city,
17549
17586
  city_id: params.city_id,
17550
17587
  count: followupsCount
17551
17588
  }, ctx),
17552
- pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
17589
+ pullLeads.execute(client, { count: DISCOVER_OVER_PULL, _reportSeen: false }, ctx)
17553
17590
  ]);
17554
17591
  if (followupsResult.status === "fulfilled") {
17555
17592
  const r = followupsResult.value;
@@ -17559,7 +17596,7 @@ var tourPlan = {
17559
17596
  location_ambiguities: r.location_ambiguities,
17560
17597
  monitor_leads: [],
17561
17598
  discover_leads: [],
17562
- discover_filter_note: "City was ambiguous; pick an id and re-call to proceed.",
17599
+ discover_filter_note: "City was ambiguous; re-call with `city_id` set to the id you pick AND `city` set to that candidate's `name`. The id scopes the follow-ups; the name is what scopes the Discover leads.",
17563
17600
  map_locations: [],
17564
17601
  map_summary: {
17565
17602
  total_leads: 0,
@@ -17583,9 +17620,27 @@ var tourPlan = {
17583
17620
  if (leadsResult.status === "rejected") {
17584
17621
  ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
17585
17622
  }
17586
- const filtered = rawDiscover.filter((l) => cityMatches(l, params.city));
17623
+ const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
17624
+ const knownId = params.city_id ?? (cityName ? void 0 : params.city);
17625
+ const idOnly = Boolean(knownId) && !cityName;
17626
+ const { leads: filtered, matchedOn } = idOnly ? { leads: [], matchedOn: null } : filterDiscoverByCity(rawDiscover, cityName);
17587
17627
  const discoverLeads2 = filtered.slice(0, discoverCount);
17588
- const filterNote = params.city ? `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}'; returning top ${discoverLeads2.length}.` : `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
17628
+ const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
17629
+ if (pulledLensId != null) {
17630
+ reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
17631
+ }
17632
+ let filterNote;
17633
+ if (idOnly) {
17634
+ filterNote = `Discover leads need the NAME of the place, and this call passed an area id (${knownId}) and no name. Re-call with \`city\` set to the name of that area, keeping \`city_id\` so the Monitor half stays on the area you picked. The follow-ups below are already scoped to it.`;
17635
+ } else if (!cityName) {
17636
+ filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
17637
+ } else if (matchedOn === null) {
17638
+ filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
17639
+ } else if (filtered.length === 0) {
17640
+ filterNote = `No Discover lead in the active lens is in '${params.city}' (checked ${rawDiscover.length} candidates by city, then by state/region). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
17641
+ } else {
17642
+ filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
17643
+ }
17589
17644
  return {
17590
17645
  city: params.city ?? null,
17591
17646
  city_id: params.city_id ?? null,
@@ -18539,11 +18594,7 @@ var researchLeadById = {
18539
18594
  }
18540
18595
  const lensId = params.lensId ?? await client.resolveDefaultLens();
18541
18596
  const leadId = params.leadId;
18542
- void client.request("POST", "/interactions", [
18543
- { type: "LEAD_SEEN", leadId, lensId: String(lensId) },
18544
- { type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
18545
- ]).catch(() => {
18546
- });
18597
+ reportLeadInteractions(client, lensId, [leadId], ["LEAD_SEEN", "LEAD_CLICKED"], _ctx?.logger);
18547
18598
  const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
18548
18599
  client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
18549
18600
  client.request("GET", `/leads/${leadId}/ai_agent_responses`),
@@ -19585,11 +19636,7 @@ var getLeadCustomFields = {
19585
19636
  },
19586
19637
  execute: async (client, params, ctx) => {
19587
19638
  const lensId = params.lensId ?? await client.resolveDefaultLens();
19588
- void client.request("POST", "/interactions", [
19589
- { type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
19590
- { type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
19591
- ]).catch(() => {
19592
- });
19639
+ reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
19593
19640
  const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
19594
19641
  const entries = lead.custom_fields ?? [];
19595
19642
  const needsCatalog = entries.some((e) => !e?.field?.name);
@@ -28613,7 +28660,7 @@ function parseWriteEnv(env = process.env) {
28613
28660
  }
28614
28661
 
28615
28662
  // src/http-server.ts
28616
- var VERSION = true ? "0.39.0" : "0.0.0-dev";
28663
+ var VERSION = true ? "0.39.2" : "0.0.0-dev";
28617
28664
  var PORT = Number(process.env.PORT ?? 8080);
28618
28665
  var HOST = process.env.HOST ?? "0.0.0.0";
28619
28666
  var logger = {
@@ -1805,7 +1805,7 @@ var init_installer_gui = __esm({
1805
1805
  init_install_dxt();
1806
1806
  init_install_shared();
1807
1807
  init_oauth();
1808
- VERSION = true ? "0.39.0" : "0.0.0-dev";
1808
+ VERSION = true ? "0.39.2" : "0.0.0-dev";
1809
1809
  MESSAGES = {
1810
1810
  en: {
1811
1811
  installer: {
@@ -1068,7 +1068,7 @@ async function oauthLogin(opts) {
1068
1068
  }
1069
1069
 
1070
1070
  // installer/installer-gui.ts
1071
- var VERSION = true ? "0.39.0" : "0.0.0-dev";
1071
+ var VERSION = true ? "0.39.2" : "0.0.0-dev";
1072
1072
  var MESSAGES = {
1073
1073
  en: {
1074
1074
  installer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.39.0",
3
+ "version": "0.39.2",
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",