@leadbay/mcp 0.11.0 → 0.12.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 +10 -0
- package/README.md +11 -11
- package/dist/bin.js +296 -13
- package/dist/{chunk-MZZMIZXA.js → chunk-J2Y4LCFM.js} +1095 -24
- package/dist/{dist-JZ2FLLN6.js → dist-YYVFSDMH.js} +1 -1
- package/package.json +1 -1
|
@@ -586,6 +586,50 @@ WHEN TO USE: at the start of a session to know what the agent can/can't do, afte
|
|
|
586
586
|
|
|
587
587
|
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.
|
|
588
588
|
`;
|
|
589
|
+
var leadbay_add_leads_to_campaign = `## WHEN TO USE
|
|
590
|
+
|
|
591
|
+
Trigger phrases: "add these leads to my <name> campaign", "attach these to <campaign>", "put these 5 in the Q2 Push campaign", "add to existing campaign".
|
|
592
|
+
|
|
593
|
+
Do NOT use for: "create a new campaign (not attach to an existing one)" \u2192 \`leadbay_create_campaign\`; "remove a lead from a campaign" \u2192 \`leadbay_create_campaign\`; "list which campaigns I have" \u2192 \`leadbay_list_campaigns\`.
|
|
594
|
+
|
|
595
|
+
Prefer when: user names an existing campaign (or supplies a campaign_id) AND a set of leads to attach. If they want a new campaign, use create_campaign with seed lead_ids instead
|
|
596
|
+
|
|
597
|
+
Examples that SHOULD invoke this tool:
|
|
598
|
+
- "Add the three new Tulsa leads to my 'OK Sweep' campaign."
|
|
599
|
+
- "Attach these qualified leads to campaign id 1f12...?"
|
|
600
|
+
- "Put the top 5 of today's batch into my Q2 Push."
|
|
601
|
+
|
|
602
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
603
|
+
- "Create a new campaign with these 9 leads."
|
|
604
|
+
- "What campaigns do I have?"
|
|
605
|
+
- "Show progression on my Limoges Tour."
|
|
606
|
+
|
|
607
|
+
## RENDER (quick)
|
|
608
|
+
|
|
609
|
+
One-line confirmation: \u2705 Added N leads to <campaign-name>
|
|
610
|
+
(M already present). If \`added === 0 && already_present > 0\` ("all
|
|
611
|
+
already in"), surface that as a no-op note instead of a success
|
|
612
|
+
banner. Offer NEXT STEPS chip for "View progression"
|
|
613
|
+
(campaign_progression) or "Add more".
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
Attach a list of \`lead_ids\` to an existing campaign. Wraps \`POST /campaigns/{id}/leads\`. The backend dedups against \`campaign_leads\`, so callers can safely retry without creating duplicates \u2014 the response separates \`added\` (newly attached) from \`already_present\` (no-op).
|
|
618
|
+
|
|
619
|
+
**Lead UUID source**: pass UUIDs returned from \`leadbay_pull_leads\` (\`lead.id\`), \`leadbay_pull_followups\` (\`lead.id\`), \`leadbay_tour_plan\` (\`monitor_leads[].id\` / \`discover_leads[].id\`), \`leadbay_research_lead_by_id\` / \`_by_name_fuzzy\` (\`lead.id\`). Backend \`ensureLeadsExist\` rejects unknown UUIDs with 404 \u2014 typo'd or fabricated IDs throw \`NOT_FOUND\`.
|
|
620
|
+
|
|
621
|
+
**Why batch vs. one-at-a-time**: every call rebuilds membership server-side and may regenerate the AI-suggested name (\`maybeRegenerateAiName\`). Prefer one call with N leads over N calls with one lead each.
|
|
622
|
+
|
|
623
|
+
**Side effect**: \`ensureLeadsInMonitor(orgId, leadIds)\` is called after a successful add \u2014 leads not already in the Monitor view get added there. This means adding a "fresh Discover" lead to a campaign moves it into the user's known-pipeline view.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
WHEN TO USE: the user has an existing campaign (created earlier in the session or visible via \`leadbay_list_campaigns\`) and wants to attach more leads to it.
|
|
628
|
+
|
|
629
|
+
WHEN NOT TO USE: to create a NEW campaign (use \`leadbay_create_campaign\` with \`lead_ids\` seeded); to list campaigns (\`leadbay_list_campaigns\`); to view per-lead progression (\`leadbay_campaign_progression\`); to remove leads (not currently exposed in the MCP \u2014 direct backend \`DELETE /campaigns/{id}/leads\` would be needed).
|
|
630
|
+
|
|
631
|
+
**Response**: \`{added: number, already_present: number}\`. Use both counts in the confirmation \u2014 silent dedup hides useful information from the user.
|
|
632
|
+
`;
|
|
589
633
|
var leadbay_add_note = `Add a note to a lead. Notes are visible to the whole organization in Leadbay.
|
|
590
634
|
|
|
591
635
|
WHEN TO USE: low-level \u2014 for free-form notes not tied to outreach actions, including meaningful per-lead notes/context preserved from an imported file after the import returns lead IDs.
|
|
@@ -691,6 +735,214 @@ Exactly two offers \u2014 keep it terse, this is a status tool:
|
|
|
691
735
|
| Qualification kicked off (async) | "Check progress in ~30s" | leadbay_qualify_status |
|
|
692
736
|
| Job is done / blocking call returned | "Refresh leads view \u2014 the new qualifications should be on the top" | leadbay_pull_leads(lensId = pinned) |
|
|
693
737
|
`;
|
|
738
|
+
var leadbay_campaign_call_sheet = `## WHEN TO USE
|
|
739
|
+
|
|
740
|
+
Trigger phrases: "show me my campaign as a call sheet", "list of people to call in <campaign>", "cold-calling cheat sheet", "work this campaign", "let's call through my <campaign>", "calling session for <campaign>".
|
|
741
|
+
|
|
742
|
+
Do NOT use for: "campaign progression pulse only (no contacts)" \u2192 \`leadbay_campaign_progression\`; "create a new campaign" \u2192 \`leadbay_create_campaign\`; "list all my campaigns" \u2192 \`leadbay_list_campaigns\`.
|
|
743
|
+
|
|
744
|
+
Prefer when: user wants an actionable call-ready view of ONE campaign with phones + LinkedIn ready to click. Pair with the \`leadbay_work_campaign\` prompt for the full dictation+epilogue loop
|
|
745
|
+
|
|
746
|
+
Examples that SHOULD invoke this tool:
|
|
747
|
+
- "Show me my Limoges Tour campaign as a call sheet."
|
|
748
|
+
- "I'm about to do a calling session \u2014 render the Q2 Push campaign."
|
|
749
|
+
- "Give me phones + LinkedIn for everyone in my OK Sweep campaign."
|
|
750
|
+
|
|
751
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
752
|
+
- "Quick pulse on how my campaigns are doing."
|
|
753
|
+
- "Save these 9 leads as a new campaign."
|
|
754
|
+
- "Tell me about Acme Corp."
|
|
755
|
+
|
|
756
|
+
## RENDER (quick)
|
|
757
|
+
|
|
758
|
+
Per-lead CARD sorted by AI score desc. Heading \`### \u26A1 **<Company>**
|
|
759
|
+
\u2014 <City>, <State>\`; one-line "\u2605 Next step: \u2026" from
|
|
760
|
+
split_ai_summary; then a 4-col table of contacts:
|
|
761
|
+
Contact / Phone / Role / Recent. Cell 1 stacks linked name + email
|
|
762
|
+
+ (if constructed) LinkedIn marker \xB0. Phone is \`[bare](tel:URL)\`.
|
|
763
|
+
Recent stacks last note + lead headline. Top of page = summary chip
|
|
764
|
+
from \`summary\` + readiness flags.
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
Build a cold-calling cheat sheet for one campaign. The composite joins two backend reads \u2014 \`/campaigns/{id}/contacts\` (per-contact phones, LinkedIn URLs, emails, job titles, recent notes) and \`/campaigns/{id}/leads\` (per-lead AI next-step, location, score, progress) \u2014 into a single call-ready payload aligned to the lead's page order.
|
|
769
|
+
|
|
770
|
+
**Per-lead block** carries:
|
|
771
|
+
|
|
772
|
+
- \`lead_id\`, \`lead_name\`, \`score\`, \`ai_agent_lead_score\`
|
|
773
|
+
- \`location\` (\`city\`, \`state\`, \`country\`, \`full\`, \`pos[lat,lng]\`)
|
|
774
|
+
- \`website\`, \`company_phone_numbers[]\` (often empty; use per-contact phones)
|
|
775
|
+
- \`next_step\` / \`approach_angle\` / \`worth_pursuing\` (from \`split_ai_summary\` \u2014 the structured calling angle)
|
|
776
|
+
- \`progress\` (\`total_contacts\` = reachable contact coverage; \`in_progress\` / \`declined\` / \`headline\` = outreach state \u2014 use those, not \`total_contacts\`, to tell whether the lead has been touched)
|
|
777
|
+
- \`affiliation.own_campaigns[]\` + \`other_users_campaign_count\` (overlap warning \u2014 avoid double-touching)
|
|
778
|
+
- \`contacts[]\` \u2014 every reachable contact for this lead, sorted: AI-pinned first, then recommended, then by phone availability. Each contact has:
|
|
779
|
+
- \`first_name\`, \`last_name\`, \`full_name\`, \`job_title\`
|
|
780
|
+
- \`phone_number\` (bare, e.g. \`"+1 512-792-7708"\`), \`phone_tel_url\` (canonical, e.g. \`"tel:+15127927708"\`)
|
|
781
|
+
- \`email\`, \`mailto_url\`
|
|
782
|
+
- \`linkedin_url\` + \`linkedin_url_source\` (\`"leadbay"\` when the backend has it, \`"constructed"\` when fallback-built from name + company)
|
|
783
|
+
- \`recent_notes[]\` \u2014 recent notes about this contact (often empty on cold campaigns)
|
|
784
|
+
|
|
785
|
+
**Map mode**: the response also carries \`map_locations[]\` \u2014 already-shaped entries for \`places_map_display_v0\` (\`{name, address, latitude, longitude, notes}\`). When the user wants a route ("plot my call list on a map"), pass \`map_locations\` directly into the widget without reshaping.
|
|
786
|
+
|
|
787
|
+
**Cross-campaign overlap warning**: when a contact's lead shows \`affiliation.other_users_campaign_count > 0\`, surface that ("\u26A0 2 teammates also have this lead in their own campaigns") so the rep can coordinate before calling.
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## RENDER \u2014 call-sheet markdown (PRIMARY)
|
|
792
|
+
|
|
793
|
+
This is a cold-calling surface. The user wants to scan \u2192 pick \u2192 tap-to-call \u2192 dictate outcome \u2192 next. Render one CARD per lead, sorted by AI score:
|
|
794
|
+
|
|
795
|
+
\`\`\`
|
|
796
|
+
### \u26A1 **<Company Name>** \u2014 <City>, <State>
|
|
797
|
+
|
|
798
|
+
**Score <ai_agent_lead_score or score>** \xB7 \u2605 Next step: <next_step>
|
|
799
|
+
|
|
800
|
+
| Contact | Phone | Role | Recent |
|
|
801
|
+
|---|---|---|---|
|
|
802
|
+
| **[<First Last>](<linkedin_url>)**<br>\u2709 [<email>](<mailto_url>) | [<phone bare>](<phone_tel_url>) | <job_title> | \u{1F4DD} "<last_note truncated>" (<rel_date>)<br>\u{1F4DE} <last_action_headline> |
|
|
803
|
+
| **[<First Last>](<linkedin_url>)** | [<phone bare>](<phone_tel_url>) | <job_title> | _(no notes)_ |
|
|
804
|
+
|
|
805
|
+
<short callout \u2014 "\u{1F4DE} 0 prior touches" / "\u26A0 Also in 2 teammates' campaigns" if applicable>
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
\`\`\`
|
|
809
|
+
|
|
810
|
+
**Cell 1 \u2014 Contact (stacked content)**:
|
|
811
|
+
- Line 1: \`**[<First Last>](<linkedin_url>)**\` \u2014 MUST always be a markdown link. Use \`contact.linkedin_url\` from the response (the composite already falls back to a \`linkedin.com/search/results/people/?keywords=\u2026\` URL when \`linkedin_page\` is missing \u2014 never render a bare name). When \`linkedin_url_source === "constructed"\`, append a trailing \` \xB0\` to the contact name to mark the fallback path.
|
|
812
|
+
- Line 2 (if email present): \`\u2709 [<email>](<mailto_url>)\` \u2014 \`mailto:\` link, auto-linkifies and opens the user's default mail app. Omit the line entirely if \`email === null\` rather than rendering "_no email_".
|
|
813
|
+
|
|
814
|
+
**Cell 2 \u2014 Phone (load-bearing for one-tap calling on mobile)**:
|
|
815
|
+
- Always render as a markdown link \`[bare](tel:URL)\`. The bare text auto-linkifies on most hosts; the explicit \`tel:\` link guarantees one-tap dialing on macOS / iOS / Android.
|
|
816
|
+
- Use the \`phone_tel_url\` from the response (already canonicalized to \`tel:+<digits>\`); do NOT reconstruct.
|
|
817
|
+
- When \`phone_number\` is null but the lead has \`company_phone_numbers[]\`, fall back to the company switchboard with a "(company line)" suffix.
|
|
818
|
+
- When neither contact phone nor company phone is present, render \`_no phone_\` \u2014 the rep will know this contact needs enrichment.
|
|
819
|
+
|
|
820
|
+
**Cell 3 \u2014 Role**: \`contact.job_title\`. Render \`\u2014\` when null.
|
|
821
|
+
|
|
822
|
+
**Cell 4 \u2014 Recent (stacked content, history surface)**:
|
|
823
|
+
- Line 1 (if \`contact.recent_notes[0]\` present): \`\u{1F4DD} "<note text, truncated to ~60 chars>" (<relative date>)\`. The relative date uses the note's \`created_at\` ISO timestamp \u2014 render as \`3d ago\`, \`2w ago\`, etc. Omit the line entirely if no notes.
|
|
824
|
+
- Line 2 (if \`last_action_headline\` on the lead-level block present): \`\u{1F4DE} <headline>\` \u2014 e.g. \`\u{1F4DE} CONTACTED\`, \`\u{1F4DE} MEETING_BOOKED\`. This is the LEAD's most recent interaction headline, not the contact's. Helpful for context but the per-contact note above is more specific.
|
|
825
|
+
- When both are absent, omit the cell content entirely \u2014 don't render \`_(no activity)_\` for every cold contact; it's noise.
|
|
826
|
+
|
|
827
|
+
**Sort + filter**:
|
|
828
|
+
- Top of the page: ONE-line summary chip from \`summary\` \u2014 \`\u{1F4CB} 12 leads \xB7 23 contacts \xB7 9 with a phone \xB7 7 with an email \xB7 3 already touched\`.
|
|
829
|
+
- Lead cards sorted by AI score desc; contacts within each card sorted AI-pinned > recommended > has-phone.
|
|
830
|
+
- If \`last_action_headline\` is present OR \`progress.in_progress > 0\` OR \`progress.declined > 0\`, render the card with a \u{1F4DE} prefix instead of \u26A1 \u2014 at-a-glance "already touched" vs "still cold". Do not use \`progress.total_contacts\`; that is contact coverage, not outreach history.
|
|
831
|
+
|
|
832
|
+
## RENDER \u2014 map widget (SECONDARY, when user asks for a route)
|
|
833
|
+
|
|
834
|
+
When the user says "plot my call list on a map" / "where are these leads" / "route my calling tour", route to \`places_map_display_v0\` using the \`map_locations\` array verbatim:
|
|
835
|
+
|
|
836
|
+
\`\`\`
|
|
837
|
+
places_map_display_v0({
|
|
838
|
+
locations: response.map_locations,
|
|
839
|
+
travel_mode: "driving"
|
|
840
|
+
})
|
|
841
|
+
\`\`\`
|
|
842
|
+
|
|
843
|
+
The composite has already built the notes string per place card (one sentence with the top contact's phone inline). After the widget, emit the standard call-sheet card list below it for the rich detail \u2014 the carousel renders bare phones as \`tel:\` but strips markdown.
|
|
844
|
+
|
|
845
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
846
|
+
|
|
847
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
848
|
+
|
|
849
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
850
|
+
|
|
851
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
852
|
+
|---|---|---|
|
|
853
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
854
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
855
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
856
|
+
|
|
857
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
858
|
+
|
|
859
|
+
**Rules:**
|
|
860
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
861
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
862
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
863
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## NEXT STEPS \u2014 wire the dictation+epilogue loop
|
|
869
|
+
|
|
870
|
+
After the user reports a call ("Called Bree, she wants pricing"), route to \`leadbay_report_outreach\` with BOTH \`note\` (the call summary) AND \`epilogue_status\` in one call. The four epilogue values:
|
|
871
|
+
|
|
872
|
+
- \`STILL_CHASING\` \u2014 still pursuing, no decision yet.
|
|
873
|
+
- \`COULD_NOT_REACH_STILL_TRYING\` \u2014 voicemail / no answer.
|
|
874
|
+
- \`INTEREST_VALIDATED_OR_MEETING_PLANED\` \u2014 qualified / meeting booked.
|
|
875
|
+
- \`NOT_INTERESTED_LOST\` \u2014 declined / not a fit.
|
|
876
|
+
|
|
877
|
+
The \`verification\` field is REQUIRED \u2014 pass \`{source: "user_confirmed", ref: <user's exact words>}\` since calls don't have message-ids.
|
|
878
|
+
|
|
879
|
+
WHEN TO USE: the user wants to actually WORK a campaign \u2014 calling session, dictation loop, follow-up sequence. Pair with the \`leadbay_work_campaign\` prompt for the end-to-end orchestrator (pick campaign \u2192 render \u2192 call \u2192 record loop).
|
|
880
|
+
|
|
881
|
+
WHEN NOT TO USE: for cross-campaign pulse (use \`leadbay_list_campaigns\`); for the slim progression view without contacts (\`leadbay_campaign_progression\` \u2014 same per-lead progress but no phone/LinkedIn fan-out); to log outreach AFTER a call (chain into \`leadbay_report_outreach\`).
|
|
882
|
+
|
|
883
|
+
**Response envelope**: \`{campaign_id, leads[], map_locations[], summary, pagination, _meta}\`.
|
|
884
|
+
|
|
885
|
+
## Linking a contact's name
|
|
886
|
+
|
|
887
|
+
**MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
|
|
888
|
+
|
|
889
|
+
URL priority (first applicable wins):
|
|
890
|
+
|
|
891
|
+
1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
|
|
892
|
+
2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
893
|
+
|
|
894
|
+
Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
|
|
895
|
+
`;
|
|
896
|
+
var leadbay_campaign_progression = `## WHEN TO USE
|
|
897
|
+
|
|
898
|
+
Trigger phrases: "how is my <name> campaign doing", "campaign progression", "lead-by-lead status on <campaign>", "who in <campaign> have I contacted", "what's stuck in my campaign".
|
|
899
|
+
|
|
900
|
+
Do NOT use for: "pulse across all campaigns (not one)" \u2192 \`leadbay_list_campaigns\`; "log an outreach event" \u2192 \`leadbay_report_outreach\`.
|
|
901
|
+
|
|
902
|
+
Prefer when: user named (or just selected from list_campaigns) ONE campaign and wants per-lead status. Use list_campaigns for the cross-campaign overview
|
|
903
|
+
|
|
904
|
+
Examples that SHOULD invoke this tool:
|
|
905
|
+
- "Walk me through the Limoges Tour campaign \u2014 who have I touched?"
|
|
906
|
+
- "Show progression on campaign 1f12...?"
|
|
907
|
+
- "What's stuck in my Q2 Push?"
|
|
908
|
+
|
|
909
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
910
|
+
- "Show me all my campaigns."
|
|
911
|
+
- "Create a new campaign."
|
|
912
|
+
- "Log that I emailed Acme."
|
|
913
|
+
|
|
914
|
+
## RENDER (quick)
|
|
915
|
+
|
|
916
|
+
Two-tier render. Top: one-line summary
|
|
917
|
+
("Limoges Tour: 9 leads \xB7 4 contacted \xB7 1 meeting \xB7 0 declined").
|
|
918
|
+
Then a markdown table per lead:
|
|
919
|
+
| Lead | Score | Contacts | Last action | Also in |
|
|
920
|
+
Each row's "Also in" column lists \`affiliation.own_campaigns[]\`
|
|
921
|
+
names (with a \u26A0 suffix if \`other_users_campaign_count > 0\`,
|
|
922
|
+
indicating teammates also have it). Sort by progress.headline
|
|
923
|
+
recency or by score desc \u2014 pick whichever surfaces "needs
|
|
924
|
+
attention" leads first.
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
Per-lead progression view for one campaign. Wraps \`GET /campaigns/{id}/leads\` (paginated). For every lead in the campaign:
|
|
929
|
+
|
|
930
|
+
- **lead** \u2014 full \`LeadPayload\` (same shape \`pull_leads\` / \`pull_followups\` return), so the agent has score, contacts, ai_summary, location, etc. without an extra round trip.
|
|
931
|
+
- **progress** \u2014 \`{total_contacts, in_progress, declined, headline}\`. \`total_contacts\` is reachable contact coverage; do not treat it as prior outreach. \`headline\` is the most recent \`InteractionType\` (CONTACTED / MEETING_BOOKED / DECLINED / etc.) and is the load-bearing field for "what stage is this lead at".
|
|
932
|
+
- **affiliation** \u2014 \`{own_campaigns: CampaignRefPayload[], other_users_campaign_count}\`. \`own_campaigns\` lists OTHER campaigns of yours this lead is in (overlap detection \u2014 avoids the rep doubling outreach across two of their own campaigns). \`other_users_campaign_count\` is how many teammates also have this lead in one of THEIR campaigns. This is the only cross-user signal the campaign API exposes.
|
|
933
|
+
|
|
934
|
+
The composite also adds a \`summary\` block computed across the current page (\`{page_size, contacted, in_progress, declined}\`) so a quick "how is this campaign doing" prompt doesn't need to roll up the items array itself.
|
|
935
|
+
|
|
936
|
+
**Pagination**: \`count\` defaults to 50; \`page\` is 0-indexed. For campaigns with hundreds of leads, page through and concatenate the summary counts.
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
WHEN TO USE: after \`leadbay_list_campaigns\` (or when the user named a specific campaign): the manager wants per-lead status to spot leads needing re-engagement, leads stuck, or leads that progressed to a meeting.
|
|
941
|
+
|
|
942
|
+
WHEN NOT TO USE: for cross-campaign pulse (use \`leadbay_list_campaigns\`); to drill into a lead's full timeline (use \`leadbay_get_lead_activities\` or \`leadbay_research_lead_by_id\`); to log outreach (\`leadbay_report_outreach\`).
|
|
943
|
+
|
|
944
|
+
**Response**: \`{items, pagination, summary, _meta}\`. Use the \`summary\` for the one-line headline; use \`items\` for the per-lead table.
|
|
945
|
+
`;
|
|
694
946
|
var leadbay_clear_selection = `Clear the user's transient selection.
|
|
695
947
|
|
|
696
948
|
WHEN TO USE: cleanup after manual selection work, or recovery from a stuck composite.
|
|
@@ -707,6 +959,55 @@ WHEN NOT TO USE: to replace with a different prompt \u2014 just call leadbay_ref
|
|
|
707
959
|
|
|
708
960
|
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\`.
|
|
709
961
|
`;
|
|
962
|
+
var leadbay_create_campaign = `## WHEN TO USE
|
|
963
|
+
|
|
964
|
+
Trigger phrases: "create a campaign called <name>", "save these leads as a campaign", "make a campaign for my <city> trip", "track these leads together", "group these leads into a named campaign", "persist these leads".
|
|
965
|
+
|
|
966
|
+
Do NOT use for: "list my existing campaigns" \u2192 \`leadbay_list_campaigns\`; "add leads to an existing campaign" \u2192 \`leadbay_add_leads_to_campaign\`; "log an outreach event" \u2192 \`leadbay_report_outreach\`.
|
|
967
|
+
|
|
968
|
+
Prefer when: user wants to persist a hand-picked set of leads as a named cohort they'll work through. Often follows leadbay_pull_leads / leadbay_tour_plan / leadbay_research_lead_by_id
|
|
969
|
+
|
|
970
|
+
Examples that SHOULD invoke this tool:
|
|
971
|
+
- "Save these 9 leads as a campaign called 'Limoges Tour \u2013 May 24'."
|
|
972
|
+
- "Create a campaign for the qualified leads I just picked."
|
|
973
|
+
- "Make a campaign for my SF visit and add those three accounts."
|
|
974
|
+
|
|
975
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
976
|
+
- "What campaigns do I have?"
|
|
977
|
+
- "Add three more leads to my 'Q2 Push' campaign."
|
|
978
|
+
- "I just emailed Acme \u2014 log it."
|
|
979
|
+
|
|
980
|
+
## RENDER (quick)
|
|
981
|
+
|
|
982
|
+
One-line confirmation: \u2705 Created **<name>** with N leads \xB7 <id-short>.
|
|
983
|
+
If the backend AI-generated the name (no \`name\` passed), surface that:
|
|
984
|
+
"AI-suggested name: <name>". Offer a NEXT STEPS chip for "Add more
|
|
985
|
+
leads" or "Open in web UI". Don't dump the full campaign payload as a
|
|
986
|
+
table \u2014 the user only needs the confirmation + the id for follow-up.
|
|
987
|
+
|
|
988
|
+
---
|
|
989
|
+
|
|
990
|
+
Create a new campaign \u2014 a server-persisted grouping of leads the user (or their manager) plans to work systematically. Wraps \`POST /campaigns\` (see \`.context/campaigns-probe/API.md\` for the discovered shape). Body is snake_case (the backend's \`apiJson\` uses \`JsonNamingStrategy.SnakeCase\`); this composite handles that translation \u2014 agents pass camelCase via the input schema but the wire is snake_case.
|
|
991
|
+
|
|
992
|
+
**Name behavior**:
|
|
993
|
+
- Pass \`name\` explicitly when the user named it ("Limoges Tour \u2013 May 24"). Max 255 chars.
|
|
994
|
+
- Omit \`name\` AND pass non-empty \`lead_ids\` \u2192 backend calls \`SuggestCampaignName.generate()\` to AI-pick a name from the seed leads. Returned as both \`name\` (final) and \`ai_generated_name\` (the suggestion, in case the user wants to rename later).
|
|
995
|
+
- Omit both \u2192 backend assigns a default (e.g. "Untitled campaign").
|
|
996
|
+
|
|
997
|
+
**Seed leads vs. empty**:
|
|
998
|
+
- Seed with \`lead_ids: [...]\` when the user already picked the leads (chain after \`leadbay_tour_plan\`, \`leadbay_pull_leads\`, \`leadbay_research_lead_by_id\`).
|
|
999
|
+
- Create empty (\`lead_ids: []\`, default) and add later via \`leadbay_add_leads_to_campaign\` \u2014 useful when the user named the campaign first and wants to populate it incrementally.
|
|
1000
|
+
|
|
1001
|
+
**Scope**: campaigns are created in the caller's organization and \`created_by = caller_user_id\`. The list endpoint (\`leadbay_list_campaigns\`) is filtered to the creator \u2014 campaigns ARE NOT shared with teammates by default. For #3630 US3 "manager creates a campaign for a rep", today's MCP workaround is to name campaigns descriptively ("North-East \u2013 John") and have the rep visit /app to access via the web UI; cross-user assignment would need backend work.
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
WHEN TO USE: the user wants to persist a hand-picked set of leads as a named cohort they'll work through systematically. Typical chains: \`tour_plan \u2192 create_campaign(lead_ids=[...selected])\` for trip planning, or \`pull_leads \u2192 research_lead_by_id \u2192 create_campaign\` for incremental promising-lead curation.
|
|
1006
|
+
|
|
1007
|
+
WHEN NOT TO USE: to LIST existing campaigns (use \`leadbay_list_campaigns\`); to ADD leads to an existing one (use \`leadbay_add_leads_to_campaign\`); to LOG an outreach event (use \`leadbay_report_outreach\`); to view per-lead campaign progression (use \`leadbay_campaign_progression\`).
|
|
1008
|
+
|
|
1009
|
+
**Response**: full \`CampaignPayload\` \u2014 \`{id, name, ai_generated_name?, ai_name_count, archived, created_by, created_at, updated_at, last_accessed_at}\`. Echo \`id\` back to the user as the handle for follow-up tool calls.
|
|
1010
|
+
`;
|
|
710
1011
|
var leadbay_create_custom_field = `Create an org-level CRM custom field for imports, then use the returned \`mapping_value\` in leadbay_import_leads / leadbay_import_and_qualify mappings. Use when the user's file contains valuable columns that do not fit Leadbay's standard fields, such as source-system deep links, source record IDs, campaign provenance, or user-requested enrichment attributes.
|
|
711
1012
|
|
|
712
1013
|
For HubSpot record links, prefer \`type:'EXTERNAL_ID'\` with \`config.url_template\` and import only the stable HubSpot id as the CSV value. Example: create field name 'HubSpot Contact', type 'EXTERNAL_ID', config \`{url_template:'https://app.hubspot.com/contacts/<portal-id>/record/0-1/{value}'}\`; then map \`hubspot_id\` to the returned \`mapping_value\`. If only a full URL column exists and the id cannot be safely extracted, use a TEXT field instead.
|
|
@@ -1314,6 +1615,46 @@ WHEN NOT TO USE: the user is just reading or researching a lead without expressi
|
|
|
1314
1615
|
|
|
1315
1616
|
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\`.
|
|
1316
1617
|
`;
|
|
1618
|
+
var leadbay_list_campaigns = `## WHEN TO USE
|
|
1619
|
+
|
|
1620
|
+
Trigger phrases: "what campaigns do I have", "list my campaigns", "show me my active campaigns", "campaign overview", "what's in flight", "pulse on my campaigns".
|
|
1621
|
+
|
|
1622
|
+
Do NOT use for: "create a new campaign" \u2192 \`leadbay_create_campaign\`; "drill into one specific campaign's progression" \u2192 \`leadbay_campaign_progression\`.
|
|
1623
|
+
|
|
1624
|
+
Prefer when: user wants the pulse / overview view across all their campaigns. Use campaign_progression to drill into one
|
|
1625
|
+
|
|
1626
|
+
Examples that SHOULD invoke this tool:
|
|
1627
|
+
- "What campaigns am I running?"
|
|
1628
|
+
- "Show me my active campaigns and how they're doing."
|
|
1629
|
+
- "Quick pulse on my campaigns."
|
|
1630
|
+
|
|
1631
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
1632
|
+
- "Create a 'Q2 Push' campaign."
|
|
1633
|
+
- "How are leads progressing in my Limoges Tour?"
|
|
1634
|
+
- "Today's fresh leads."
|
|
1635
|
+
|
|
1636
|
+
## RENDER (quick)
|
|
1637
|
+
|
|
1638
|
+
Render as a compact markdown table sorted by \`updated_at\` desc:
|
|
1639
|
+
| Campaign | Leads | Contacted | Meetings | Declined | Updated |
|
|
1640
|
+
Each row links to "View progression" via a NEXT STEPS chip that
|
|
1641
|
+
passes the campaign_id back into \`leadbay_campaign_progression\`.
|
|
1642
|
+
Suppress archived rows unless \`archived: true\` was passed.
|
|
1643
|
+
|
|
1644
|
+
---
|
|
1645
|
+
|
|
1646
|
+
List the caller's campaigns with roll-up stats. Wraps \`GET /campaigns\` (with \`?archived=true\` to surface archived ones). Each entry is \`CampaignWithStatsPayload\`: the campaign object + \`lead_count\`, \`contact_count\`, \`contacted\` (leads with any logged outreach), \`meeting_booked\` (leads with a recorded meeting outcome), \`declined\` (leads with a recorded decline).
|
|
1647
|
+
|
|
1648
|
+
**Scope warning**: this list is filtered server-side to campaigns the calling user CREATED. Other users in the same organization with their own campaigns are not surfaced. The web UI may aggregate differently for admins; the MCP-exposed view is creator-scoped. This matters for #3630 US3 "manager-led prospecting governance" \u2014 a manager can list their own campaigns but not see a rep's. Cross-user visibility would need backend work.
|
|
1649
|
+
|
|
1650
|
+
---
|
|
1651
|
+
|
|
1652
|
+
WHEN TO USE: the user wants the cross-campaign pulse \u2014 what cohorts are in flight, how active each is, which need attention.
|
|
1653
|
+
|
|
1654
|
+
WHEN NOT TO USE: for per-lead progression inside ONE campaign (use \`leadbay_campaign_progression\`); to create a campaign (\`leadbay_create_campaign\`); to add leads to one (\`leadbay_add_leads_to_campaign\`).
|
|
1655
|
+
|
|
1656
|
+
**Response**: \`{campaigns: CampaignWithStats[], _meta}\`. Sort by \`updated_at desc\` when rendering \u2014 recency is the manager's natural lens.
|
|
1657
|
+
`;
|
|
1317
1658
|
var leadbay_list_lenses = `List all available Leadbay lenses (saved lead-search configurations). Each lens defines a different target market or buyer segment. The lens with \`is_last_active=true\` is used by default for lead discovery.
|
|
1318
1659
|
|
|
1319
1660
|
WHEN TO USE: when the user wants to switch lens or asks "what lenses do I have".
|
|
@@ -2521,6 +2862,101 @@ WHEN NOT TO USE: from agent flow \u2014 use leadbay_refine_prompt, which polls f
|
|
|
2521
2862
|
|
|
2522
2863
|
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\`.
|
|
2523
2864
|
`;
|
|
2865
|
+
var leadbay_tour_plan = `## WHEN TO USE
|
|
2866
|
+
|
|
2867
|
+
Trigger phrases: "I'm visiting <city> in <N> days", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "mix of customers and prospects in <city>", "tour itinerary", "3 customers + 3 prospects + 3 new in <city>", "field sales tour".
|
|
2868
|
+
|
|
2869
|
+
Do NOT use for: "follow-ups only on the map (no new prospects mixed in)" \u2192 \`leadbay_followups_map\`; "new leads only, no geography" \u2192 \`leadbay_pull_leads\`; "research one specific account I picked" \u2192 \`leadbay_research_lead_by_id\`.
|
|
2870
|
+
|
|
2871
|
+
Prefer when: user wants both KNOWN accounts AND NEW discovery in one geographic view. If they only want one side, route to followups_map or pull_leads instead
|
|
2872
|
+
|
|
2873
|
+
Examples that SHOULD invoke this tool:
|
|
2874
|
+
- "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
|
|
2875
|
+
- "Plan my tour next Tuesday in Lyon: known accounts plus discoveries."
|
|
2876
|
+
- "Build a mixed itinerary for Berlin \u2014 I want both follow-ups and fresh leads."
|
|
2877
|
+
|
|
2878
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
2879
|
+
- "Show me my follow-ups for the SF trip."
|
|
2880
|
+
- "What's new in today's batch?"
|
|
2881
|
+
- "Tell me about Acme Corp."
|
|
2882
|
+
|
|
2883
|
+
## RENDER (quick)
|
|
2884
|
+
|
|
2885
|
+
Route the union of monitor_leads + discover_leads into
|
|
2886
|
+
\`places_map_display_v0\` \u2014 same recipe as followups_map (lat/lng
|
|
2887
|
+
split, full address, short notes). Tag each entry's notes with a
|
|
2888
|
+
mode badge: "\u2605 Customer" / "\u2605 Qualified" / "\u2726 New" (split Monitor
|
|
2889
|
+
by last_monitor_action; Discover = New). Below the widget: one
|
|
2890
|
+
intro sentence + per-group chat-prose list with LinkedIn-linked
|
|
2891
|
+
contact names.
|
|
2892
|
+
|
|
2893
|
+
---
|
|
2894
|
+
|
|
2895
|
+
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."*
|
|
2896
|
+
|
|
2897
|
+
**Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any admin level \u2014 city, state, country, region \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\`.
|
|
2898
|
+
|
|
2899
|
+
**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").
|
|
2900
|
+
|
|
2901
|
+
**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.
|
|
2902
|
+
|
|
2903
|
+
---
|
|
2904
|
+
|
|
2905
|
+
## RENDER \u2014 host-native map widget (REQUIRED, preferred)
|
|
2906
|
+
|
|
2907
|
+
Identical to \`leadbay_followups_map\`. Pass the union \`monitor_leads + discover_leads\` into \`places_map_display_v0\`. The mode information goes in the per-lead \`notes\` string as a **leading badge**:
|
|
2908
|
+
|
|
2909
|
+
- \`\u2605 Customer\` \u2014 Monitor lead with an existing \`last_monitor_action\` history (engaged / past interaction).
|
|
2910
|
+
- \`\u2605 Qualified\` \u2014 Monitor lead with a high \`ai_agent_lead_score\` or \`score\` but no recent action.
|
|
2911
|
+
- \`\u2726 New\` \u2014 Discover lead from \`discover_leads\`.
|
|
2912
|
+
|
|
2913
|
+
Then the rest of the notes string follows the standard recipe: ONE sentence, sector/fit + contact ask, bare phone + email auto-linkified. Example:
|
|
2914
|
+
|
|
2915
|
+
\`\`\`
|
|
2916
|
+
\u2726 New \u2014 Strong mid-size hardware distributor fit. Reach Marie Dupont, Sales Director: +33 5 55 12 34 56, m.dupont@example.fr.
|
|
2917
|
+
\`\`\`
|
|
2918
|
+
|
|
2919
|
+
Skip any lead whose \`location.pos\` is null (no lat/lng \u2192 no pin) \u2014 list them as a "+ N leads without coordinates" footer below the widget.
|
|
2920
|
+
|
|
2921
|
+
## Chat prose AFTER the widget (where markdown DOES render)
|
|
2922
|
+
|
|
2923
|
+
Group the leads into THREE sections (Customers / Qualified Prospects / New Discoveries) and emit a short chat-prose list per group with LinkedIn-linked contact name + bare phone/email pills. This is the "Notes from Claude" companion that the carousel can't render. Mirror the \`followups_map\` recipe exactly \u2014 score callout, contact name as markdown link, \`\xB7\`-separated channel pills.
|
|
2924
|
+
|
|
2925
|
+
If the user said something like "3+3+3", honor that split. If \`followups_count\` returned fewer Customers than asked, fill from Qualified.
|
|
2926
|
+
|
|
2927
|
+
## RENDER \u2014 fallback for hosts without \`places_map_display_v0\`
|
|
2928
|
+
|
|
2929
|
+
Emit per-lead markdown blocks grouped by mode, in the standard place-card-friendly format (\`### **Company** \xB7 City, State\`, mode badge + score, contact line, channel lines). Hosts that auto-detect addresses (Claude.ai web, cowork) build their own carousel from these blocks.
|
|
2930
|
+
|
|
2931
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
2932
|
+
|
|
2933
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
2934
|
+
|
|
2935
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
2936
|
+
|
|
2937
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
2938
|
+
|---|---|---|
|
|
2939
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
2940
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
2941
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
2942
|
+
|
|
2943
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
2944
|
+
|
|
2945
|
+
**Rules:**
|
|
2946
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
2947
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
2948
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
2949
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
2950
|
+
|
|
2951
|
+
|
|
2952
|
+
---
|
|
2953
|
+
|
|
2954
|
+
WHEN TO USE: the user signals a *mixed* tour-planning intent \u2014 they want both known accounts AND fresh discoveries on one geographic view, typically for a planned visit.
|
|
2955
|
+
|
|
2956
|
+
WHEN NOT TO USE: if the user only wants follow-ups (use \`leadbay_followups_map\`), only wants new leads (use \`leadbay_pull_leads\`), wants research on one specific account (\`leadbay_research_lead_by_id\`), or wants to persist the tour as a campaign artifact (chain into \`leadbay_create_campaign\` after this).
|
|
2957
|
+
|
|
2958
|
+
**Response envelope**: \`{city, city_id, monitor_leads, discover_leads, discover_filter_note, _meta}\` on happy path; \`{status: "ambiguous_locations", location_ambiguities, ...}\` when the passed \`city\` matched multiple admin areas.
|
|
2959
|
+
`;
|
|
2524
2960
|
var leadbay_update_lens = `Update lens metadata (name, description, mode flags). Does NOT change the audience filter \u2014 use leadbay_update_lens_filter for that.
|
|
2525
2961
|
|
|
2526
2962
|
WHEN TO USE: rename a lens or toggle \`multi_product_mode\` / \`use_hq_only\`.
|
|
@@ -6776,30 +7212,657 @@ var followupsMap = {
|
|
|
6776
7212
|
execute: pullFollowups.execute
|
|
6777
7213
|
};
|
|
6778
7214
|
|
|
6779
|
-
// ../core/dist/composite/
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
7215
|
+
// ../core/dist/composite/tour-plan.js
|
|
7216
|
+
var DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
7217
|
+
var DEFAULT_DISCOVER_COUNT = 6;
|
|
7218
|
+
var DISCOVER_OVER_PULL = 30;
|
|
7219
|
+
function cityMatches(lead, cityHint) {
|
|
7220
|
+
if (!cityHint)
|
|
7221
|
+
return true;
|
|
7222
|
+
const hint = cityHint.toLowerCase();
|
|
7223
|
+
const loc = lead?.location ?? {};
|
|
7224
|
+
const haystacks = [loc.city, loc.state, loc.country, loc.full].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
|
|
7225
|
+
return haystacks.some((h) => h.includes(hint) || hint.includes(h));
|
|
6789
7226
|
}
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
7227
|
+
var tourPlan = {
|
|
7228
|
+
name: "leadbay_tour_plan",
|
|
7229
|
+
annotations: {
|
|
7230
|
+
title: "Plan a mixed-mode tour itinerary (known + fresh leads)",
|
|
7231
|
+
readOnlyHint: true,
|
|
7232
|
+
destructiveHint: false,
|
|
7233
|
+
idempotentHint: true,
|
|
7234
|
+
openWorldHint: true
|
|
7235
|
+
},
|
|
7236
|
+
description: leadbay_tour_plan,
|
|
7237
|
+
inputSchema: {
|
|
7238
|
+
type: "object",
|
|
7239
|
+
properties: {
|
|
7240
|
+
city: {
|
|
7241
|
+
type: "string",
|
|
7242
|
+
description: "Free-text city or region (e.g. 'Limoges', 'Bay Area'). Resolved via the same /geo/search the followups_map uses. Ambiguous matches surface as `status: ambiguous_locations` with location_ambiguities[]; pick a location id and re-call with city_id."
|
|
7243
|
+
},
|
|
7244
|
+
city_id: {
|
|
7245
|
+
type: "string",
|
|
7246
|
+
description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver."
|
|
7247
|
+
},
|
|
7248
|
+
followups_count: {
|
|
7249
|
+
type: "number",
|
|
7250
|
+
description: `Top-N follow-up (Monitor) leads to return. Default ${DEFAULT_FOLLOWUPS_COUNT}.`
|
|
7251
|
+
},
|
|
7252
|
+
discover_count: {
|
|
7253
|
+
type: "number",
|
|
7254
|
+
description: `Top-N Discover leads (active lens wishlist) to return after client-side city filter. Default ${DEFAULT_DISCOVER_COUNT}.`
|
|
7255
|
+
}
|
|
7256
|
+
},
|
|
7257
|
+
additionalProperties: false
|
|
7258
|
+
},
|
|
7259
|
+
outputSchema: {
|
|
7260
|
+
type: "object",
|
|
7261
|
+
properties: {
|
|
7262
|
+
city: { type: ["string", "null"] },
|
|
7263
|
+
city_id: { type: ["string", "null"] },
|
|
7264
|
+
monitor_leads: {
|
|
7265
|
+
type: "array",
|
|
7266
|
+
description: "Follow-up (Monitor) leads in the requested city, sorted by AI / firmographic score. Each lead carries the same shape as pull_followups.",
|
|
7267
|
+
items: { type: "object" }
|
|
7268
|
+
},
|
|
7269
|
+
discover_leads: {
|
|
7270
|
+
type: "array",
|
|
7271
|
+
description: "Fresh Discover leads from the active lens, filtered client-side to match the city. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
7272
|
+
items: { type: "object" }
|
|
7273
|
+
},
|
|
7274
|
+
discover_filter_note: {
|
|
7275
|
+
type: "string",
|
|
7276
|
+
description: "Human-readable summary of the client-side geo filter applied to Discover leads (e.g. 'matched 3/30 by city/state')."
|
|
7277
|
+
},
|
|
7278
|
+
status: {
|
|
7279
|
+
type: "string",
|
|
7280
|
+
description: "'ambiguous_locations' when the passed `city` matched multiple admin areas \u2014 pick an id from location_ambiguities and re-call with city_id."
|
|
7281
|
+
},
|
|
7282
|
+
location_ambiguities: {
|
|
7283
|
+
type: "array",
|
|
7284
|
+
items: { type: "object" }
|
|
7285
|
+
},
|
|
7286
|
+
_meta: {
|
|
7287
|
+
type: "object",
|
|
7288
|
+
properties: {
|
|
7289
|
+
region: { type: "string" },
|
|
7290
|
+
latency_ms: { type: ["number", "null"] }
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
},
|
|
7294
|
+
required: ["monitor_leads", "discover_leads"]
|
|
7295
|
+
},
|
|
7296
|
+
execute: async (client, params, ctx) => {
|
|
7297
|
+
const followupsCount = params.followups_count ?? DEFAULT_FOLLOWUPS_COUNT;
|
|
7298
|
+
const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
|
|
7299
|
+
const [followupsResult, leadsResult] = await Promise.allSettled([
|
|
7300
|
+
pullFollowups.execute(client, {
|
|
7301
|
+
city: params.city,
|
|
7302
|
+
city_id: params.city_id,
|
|
7303
|
+
count: followupsCount
|
|
7304
|
+
}, ctx),
|
|
7305
|
+
pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
|
|
7306
|
+
]);
|
|
7307
|
+
if (followupsResult.status === "fulfilled") {
|
|
7308
|
+
const r = followupsResult.value;
|
|
7309
|
+
if (r?.status === "ambiguous_locations") {
|
|
7310
|
+
return {
|
|
7311
|
+
status: "ambiguous_locations",
|
|
7312
|
+
location_ambiguities: r.location_ambiguities,
|
|
7313
|
+
monitor_leads: [],
|
|
7314
|
+
discover_leads: [],
|
|
7315
|
+
discover_filter_note: "City was ambiguous; pick an id and re-call to proceed.",
|
|
7316
|
+
city: params.city ?? null,
|
|
7317
|
+
city_id: params.city_id ?? null,
|
|
7318
|
+
_meta: {
|
|
7319
|
+
region: client.region,
|
|
7320
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
7321
|
+
}
|
|
7322
|
+
};
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
const monitorLeads = followupsResult.status === "fulfilled" ? followupsResult.value?.leads ?? [] : [];
|
|
7326
|
+
if (followupsResult.status === "rejected") {
|
|
7327
|
+
ctx?.logger?.warn?.(`tour_plan: pull_followups failed: ${followupsResult.reason?.message ?? followupsResult.reason}`);
|
|
7328
|
+
}
|
|
7329
|
+
const rawDiscover = leadsResult.status === "fulfilled" ? leadsResult.value?.leads ?? [] : [];
|
|
7330
|
+
if (leadsResult.status === "rejected") {
|
|
7331
|
+
ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
|
|
7332
|
+
}
|
|
7333
|
+
const filtered = rawDiscover.filter((l) => cityMatches(l, params.city));
|
|
7334
|
+
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
7335
|
+
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.`;
|
|
7336
|
+
return {
|
|
7337
|
+
city: params.city ?? null,
|
|
7338
|
+
city_id: params.city_id ?? null,
|
|
7339
|
+
monitor_leads: monitorLeads,
|
|
7340
|
+
discover_leads: discoverLeads2,
|
|
7341
|
+
discover_filter_note: filterNote,
|
|
7342
|
+
_meta: {
|
|
7343
|
+
region: client.region,
|
|
7344
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
7345
|
+
}
|
|
7346
|
+
};
|
|
7347
|
+
}
|
|
7348
|
+
};
|
|
7349
|
+
|
|
7350
|
+
// ../core/dist/composite/create-campaign.js
|
|
7351
|
+
var createCampaign = {
|
|
7352
|
+
name: "leadbay_create_campaign",
|
|
7353
|
+
annotations: {
|
|
7354
|
+
title: "Create a named campaign (optionally seeded with leads)",
|
|
7355
|
+
readOnlyHint: false,
|
|
7356
|
+
destructiveHint: true,
|
|
7357
|
+
idempotentHint: false,
|
|
7358
|
+
openWorldHint: true
|
|
7359
|
+
},
|
|
7360
|
+
description: leadbay_create_campaign,
|
|
7361
|
+
optional: true,
|
|
7362
|
+
// gated behind LEADBAY_MCP_WRITE=1 in MCP
|
|
7363
|
+
inputSchema: {
|
|
7364
|
+
type: "object",
|
|
7365
|
+
properties: {
|
|
7366
|
+
name: {
|
|
7367
|
+
type: "string",
|
|
7368
|
+
description: "Campaign display name (max 255 chars). Omit to let the backend AI-generate one from the seed lead_ids. If lead_ids is empty AND name is omitted, the backend assigns a default."
|
|
7369
|
+
},
|
|
7370
|
+
lead_ids: {
|
|
7371
|
+
type: "array",
|
|
7372
|
+
description: "Lead UUIDs to attach at creation. Empty array (default) creates an empty campaign \u2014 add leads later via leadbay_add_leads_to_campaign. Non-empty seed enables AI name suggestion.",
|
|
7373
|
+
items: { type: "string" }
|
|
7374
|
+
}
|
|
7375
|
+
},
|
|
7376
|
+
additionalProperties: false
|
|
7377
|
+
},
|
|
7378
|
+
outputSchema: {
|
|
7379
|
+
type: "object",
|
|
7380
|
+
properties: {
|
|
7381
|
+
id: { type: "string", description: "Campaign UUID." },
|
|
7382
|
+
name: { type: "string" },
|
|
7383
|
+
ai_generated_name: { type: ["string", "null"] },
|
|
7384
|
+
ai_name_count: { type: "number" },
|
|
7385
|
+
archived: { type: "boolean" },
|
|
7386
|
+
created_by: { type: "string" },
|
|
7387
|
+
created_at: { type: "string" },
|
|
7388
|
+
updated_at: { type: "string" },
|
|
7389
|
+
last_accessed_at: { type: "string" }
|
|
7390
|
+
},
|
|
7391
|
+
required: ["id", "name", "created_at"]
|
|
7392
|
+
},
|
|
7393
|
+
execute: async (client, params) => {
|
|
7394
|
+
const body = {
|
|
7395
|
+
lead_ids: params.lead_ids ?? []
|
|
7396
|
+
};
|
|
7397
|
+
if (params.name && params.name.trim().length > 0) {
|
|
7398
|
+
body.name = params.name.slice(0, 255);
|
|
7399
|
+
}
|
|
7400
|
+
const result = await client.request("POST", `/campaigns`, body);
|
|
7401
|
+
return result;
|
|
7402
|
+
}
|
|
7403
|
+
};
|
|
7404
|
+
|
|
7405
|
+
// ../core/dist/composite/add-leads-to-campaign.js
|
|
7406
|
+
var addLeadsToCampaign = {
|
|
7407
|
+
name: "leadbay_add_leads_to_campaign",
|
|
7408
|
+
annotations: {
|
|
7409
|
+
title: "Add leads to an existing campaign",
|
|
7410
|
+
readOnlyHint: false,
|
|
7411
|
+
destructiveHint: true,
|
|
7412
|
+
idempotentHint: true,
|
|
7413
|
+
openWorldHint: true
|
|
7414
|
+
},
|
|
7415
|
+
description: leadbay_add_leads_to_campaign,
|
|
7416
|
+
optional: true,
|
|
7417
|
+
inputSchema: {
|
|
7418
|
+
type: "object",
|
|
7419
|
+
properties: {
|
|
7420
|
+
campaign_id: {
|
|
7421
|
+
type: "string",
|
|
7422
|
+
description: "Campaign UUID (from leadbay_create_campaign or leadbay_list_campaigns)."
|
|
7423
|
+
},
|
|
7424
|
+
lead_ids: {
|
|
7425
|
+
type: "array",
|
|
7426
|
+
description: "Lead UUIDs to add. Backend rejects unknown lead UUIDs with 404 \u2014 pass UUIDs sourced from pull_leads / pull_followups / tour_plan / research.",
|
|
7427
|
+
items: { type: "string" },
|
|
7428
|
+
minItems: 1
|
|
7429
|
+
}
|
|
7430
|
+
},
|
|
7431
|
+
required: ["campaign_id", "lead_ids"],
|
|
7432
|
+
additionalProperties: false
|
|
7433
|
+
},
|
|
7434
|
+
outputSchema: {
|
|
7435
|
+
type: "object",
|
|
7436
|
+
properties: {
|
|
7437
|
+
added: { type: "number", description: "Leads newly attached." },
|
|
7438
|
+
already_present: { type: "number", description: "Leads that were already in this campaign \u2014 no-op." }
|
|
7439
|
+
},
|
|
7440
|
+
required: ["added", "already_present"]
|
|
7441
|
+
},
|
|
7442
|
+
execute: async (client, params) => {
|
|
7443
|
+
if (!params.lead_ids || params.lead_ids.length === 0) {
|
|
7444
|
+
throw client.makeError("INVALID_PARAMS", "lead_ids must be a non-empty array", "Pass at least one lead UUID to add. To create an empty campaign, use leadbay_create_campaign with lead_ids: [].");
|
|
7445
|
+
}
|
|
7446
|
+
const result = await client.request("POST", `/campaigns/${params.campaign_id}/leads`, { lead_ids: params.lead_ids });
|
|
7447
|
+
return result;
|
|
7448
|
+
}
|
|
7449
|
+
};
|
|
7450
|
+
|
|
7451
|
+
// ../core/dist/composite/list-campaigns.js
|
|
7452
|
+
var listCampaigns = {
|
|
7453
|
+
name: "leadbay_list_campaigns",
|
|
7454
|
+
annotations: {
|
|
7455
|
+
title: "List your campaigns (with roll-up stats)",
|
|
7456
|
+
readOnlyHint: true,
|
|
7457
|
+
destructiveHint: false,
|
|
7458
|
+
idempotentHint: true,
|
|
7459
|
+
openWorldHint: true
|
|
7460
|
+
},
|
|
7461
|
+
description: leadbay_list_campaigns,
|
|
7462
|
+
inputSchema: {
|
|
7463
|
+
type: "object",
|
|
7464
|
+
properties: {
|
|
7465
|
+
archived: {
|
|
7466
|
+
type: "boolean",
|
|
7467
|
+
description: "Include archived campaigns only. Default false (active only)."
|
|
7468
|
+
}
|
|
7469
|
+
},
|
|
7470
|
+
additionalProperties: false
|
|
7471
|
+
},
|
|
7472
|
+
outputSchema: {
|
|
7473
|
+
type: "object",
|
|
7474
|
+
properties: {
|
|
7475
|
+
campaigns: {
|
|
7476
|
+
type: "array",
|
|
7477
|
+
description: "Each entry is {campaign, lead_count, contact_count, contacted, meeting_booked, declined}. `contacted` = leads with at least one logged outreach; `meeting_booked` = leads with a recorded meeting outcome; `declined` = leads with a recorded decline outcome.",
|
|
7478
|
+
items: { type: "object" }
|
|
7479
|
+
},
|
|
7480
|
+
_meta: {
|
|
7481
|
+
type: "object",
|
|
7482
|
+
properties: {
|
|
7483
|
+
region: { type: "string" },
|
|
7484
|
+
latency_ms: { type: ["number", "null"] }
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
},
|
|
7488
|
+
required: ["campaigns"]
|
|
7489
|
+
},
|
|
7490
|
+
execute: async (client, params) => {
|
|
7491
|
+
const archived = params.archived ?? false;
|
|
7492
|
+
const qs = archived ? "?archived=true" : "";
|
|
7493
|
+
const campaigns = await client.request("GET", `/campaigns${qs}`);
|
|
7494
|
+
return {
|
|
7495
|
+
campaigns,
|
|
7496
|
+
_meta: {
|
|
7497
|
+
region: client.region,
|
|
7498
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
7499
|
+
}
|
|
7500
|
+
};
|
|
7501
|
+
}
|
|
7502
|
+
};
|
|
7503
|
+
|
|
7504
|
+
// ../core/dist/composite/campaign-progression.js
|
|
7505
|
+
function hasOutreachSignal(progress) {
|
|
7506
|
+
if (!progress)
|
|
7507
|
+
return false;
|
|
7508
|
+
return Boolean(progress.headline || (progress.in_progress ?? 0) > 0 || (progress.declined ?? 0) > 0);
|
|
7509
|
+
}
|
|
7510
|
+
var campaignProgression = {
|
|
7511
|
+
name: "leadbay_campaign_progression",
|
|
7512
|
+
annotations: {
|
|
7513
|
+
title: "Read per-lead progression inside a campaign",
|
|
7514
|
+
readOnlyHint: true,
|
|
7515
|
+
destructiveHint: false,
|
|
7516
|
+
idempotentHint: true,
|
|
7517
|
+
openWorldHint: true
|
|
7518
|
+
},
|
|
7519
|
+
description: leadbay_campaign_progression,
|
|
7520
|
+
inputSchema: {
|
|
7521
|
+
type: "object",
|
|
7522
|
+
properties: {
|
|
7523
|
+
campaign_id: {
|
|
7524
|
+
type: "string",
|
|
7525
|
+
description: "Campaign UUID (from leadbay_create_campaign or leadbay_list_campaigns)."
|
|
7526
|
+
},
|
|
7527
|
+
count: { type: "number", description: "Leads per page (default 50, server-capped)." },
|
|
7528
|
+
page: { type: "number", description: "0-indexed page (default 0)." }
|
|
7529
|
+
},
|
|
7530
|
+
required: ["campaign_id"],
|
|
7531
|
+
additionalProperties: false
|
|
7532
|
+
},
|
|
7533
|
+
outputSchema: {
|
|
7534
|
+
type: "object",
|
|
7535
|
+
properties: {
|
|
7536
|
+
items: {
|
|
7537
|
+
type: "array",
|
|
7538
|
+
description: "Per-lead progression rows: {lead, progress: {total_contacts, in_progress, declined, headline}, affiliation: {own_campaigns, other_users_campaign_count}}. `headline` is the most recent interaction type (e.g. CONTACTED, MEETING_BOOKED, DECLINED).",
|
|
7539
|
+
items: { type: "object" }
|
|
7540
|
+
},
|
|
7541
|
+
pagination: {
|
|
7542
|
+
type: "object",
|
|
7543
|
+
properties: {
|
|
7544
|
+
page: { type: "number" },
|
|
7545
|
+
pages: { type: "number" },
|
|
7546
|
+
total: { type: "number" }
|
|
7547
|
+
}
|
|
7548
|
+
},
|
|
7549
|
+
summary: {
|
|
7550
|
+
type: "object",
|
|
7551
|
+
description: "Roll-up across the current page: how many leads have any outreach, how many converted to meetings, how many were declined.",
|
|
7552
|
+
properties: {
|
|
7553
|
+
page_size: { type: "number" },
|
|
7554
|
+
contacted: { type: "number" },
|
|
7555
|
+
in_progress: { type: "number" },
|
|
7556
|
+
declined: { type: "number" }
|
|
7557
|
+
}
|
|
7558
|
+
},
|
|
7559
|
+
_meta: {
|
|
7560
|
+
type: "object",
|
|
7561
|
+
properties: {
|
|
7562
|
+
region: { type: "string" },
|
|
7563
|
+
latency_ms: { type: ["number", "null"] }
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7566
|
+
},
|
|
7567
|
+
required: ["items", "pagination", "summary"]
|
|
7568
|
+
},
|
|
7569
|
+
execute: async (client, params) => {
|
|
7570
|
+
const count = params.count ?? 50;
|
|
7571
|
+
const page = params.page ?? 0;
|
|
7572
|
+
const result = await client.request("GET", `/campaigns/${params.campaign_id}/leads?count=${count}&page=${page}`);
|
|
7573
|
+
let contacted = 0;
|
|
7574
|
+
let inProgress = 0;
|
|
7575
|
+
let declined = 0;
|
|
7576
|
+
for (const row of result.items) {
|
|
7577
|
+
const p = row.progress;
|
|
7578
|
+
if (hasOutreachSignal(p))
|
|
7579
|
+
contacted++;
|
|
7580
|
+
if ((p?.in_progress ?? 0) > 0)
|
|
7581
|
+
inProgress++;
|
|
7582
|
+
if ((p?.declined ?? 0) > 0)
|
|
7583
|
+
declined++;
|
|
7584
|
+
}
|
|
7585
|
+
return {
|
|
7586
|
+
items: result.items,
|
|
7587
|
+
pagination: result.pagination,
|
|
7588
|
+
summary: {
|
|
7589
|
+
page_size: result.items.length,
|
|
7590
|
+
contacted,
|
|
7591
|
+
in_progress: inProgress,
|
|
7592
|
+
declined
|
|
7593
|
+
},
|
|
7594
|
+
_meta: {
|
|
7595
|
+
region: client.region,
|
|
7596
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
7597
|
+
}
|
|
7598
|
+
};
|
|
7599
|
+
}
|
|
7600
|
+
};
|
|
7601
|
+
|
|
7602
|
+
// ../core/dist/composite/campaign-call-sheet.js
|
|
7603
|
+
function hasOutreachSignal2(progress) {
|
|
7604
|
+
if (!progress)
|
|
7605
|
+
return false;
|
|
7606
|
+
return Boolean(progress.headline || (progress.in_progress ?? 0) > 0 || (progress.declined ?? 0) > 0);
|
|
7607
|
+
}
|
|
7608
|
+
function normalizeLinkedin(v) {
|
|
7609
|
+
if (typeof v !== "string")
|
|
7610
|
+
return null;
|
|
7611
|
+
const t = v.trim();
|
|
7612
|
+
if (!t || t.toLowerCase() === "null")
|
|
7613
|
+
return null;
|
|
7614
|
+
return t;
|
|
7615
|
+
}
|
|
7616
|
+
function constructLinkedinFallback(first, last, company) {
|
|
7617
|
+
if (!first || !last)
|
|
7618
|
+
return null;
|
|
7619
|
+
const cleanedCompany = (company ?? "").replace(/\s+(Inc|LLC|Corp|GmbH|Ltd|Co|S\.A\.|S\.L\.|PLC|AG|SAS|SARL)\.?$/gi, "").trim();
|
|
7620
|
+
const keywords = encodeURIComponent([first, last, cleanedCompany].filter(Boolean).join(" "));
|
|
7621
|
+
return `https://www.linkedin.com/search/results/people/?keywords=${keywords}`;
|
|
7622
|
+
}
|
|
7623
|
+
var campaignCallSheet = {
|
|
7624
|
+
name: "leadbay_campaign_call_sheet",
|
|
7625
|
+
annotations: {
|
|
7626
|
+
title: "Cold-calling cheat sheet for a campaign",
|
|
7627
|
+
readOnlyHint: true,
|
|
7628
|
+
destructiveHint: false,
|
|
7629
|
+
idempotentHint: true,
|
|
7630
|
+
openWorldHint: true
|
|
7631
|
+
},
|
|
7632
|
+
description: leadbay_campaign_call_sheet,
|
|
7633
|
+
inputSchema: {
|
|
7634
|
+
type: "object",
|
|
7635
|
+
properties: {
|
|
7636
|
+
campaign_id: {
|
|
7637
|
+
type: "string",
|
|
7638
|
+
description: "Campaign UUID (from leadbay_create_campaign or leadbay_list_campaigns)."
|
|
7639
|
+
},
|
|
7640
|
+
count: {
|
|
7641
|
+
type: "number",
|
|
7642
|
+
description: "Leads per page for the underlying /campaigns/{id}/leads fetch (default 50). The contacts endpoint is not paginated; the composite always fetches all contacts and aligns them with the leads page."
|
|
7643
|
+
},
|
|
7644
|
+
page: {
|
|
7645
|
+
type: "number",
|
|
7646
|
+
description: "0-indexed page (default 0)."
|
|
7647
|
+
}
|
|
7648
|
+
},
|
|
7649
|
+
required: ["campaign_id"],
|
|
7650
|
+
additionalProperties: false
|
|
7651
|
+
},
|
|
7652
|
+
outputSchema: {
|
|
7653
|
+
type: "object",
|
|
7654
|
+
properties: {
|
|
7655
|
+
campaign_id: { type: "string" },
|
|
7656
|
+
leads: {
|
|
7657
|
+
type: "array",
|
|
7658
|
+
description: "Per-lead call-ready blocks sorted by score desc. Each block: {lead_id, lead_name, score, ai_agent_lead_score, location, website, next_step, approach_angle, worth_pursuing, progress, affiliation, contacts: [{first_name, last_name, full_name, phone_number, phone_tel_url, email, mailto_url, linkedin_url, linkedin_url_source, job_title, recommended, pinned, recent_notes}]}.",
|
|
7659
|
+
items: { type: "object" }
|
|
7660
|
+
},
|
|
7661
|
+
map_locations: {
|
|
7662
|
+
type: "array",
|
|
7663
|
+
description: "Ready-to-pass entries for `places_map_display_v0` when the user wants a geographic call route: {name, address, latitude, longitude, notes}. One entry per lead that has a valid pos[]. Skip leads without coordinates.",
|
|
7664
|
+
items: { type: "object" }
|
|
7665
|
+
},
|
|
7666
|
+
summary: {
|
|
7667
|
+
type: "object",
|
|
7668
|
+
description: "Roll-up: total_leads, total_contacts, leads_with_phone, leads_with_email, leads_with_coords, leads_without_contacts, leads_already_contacted (headline/in_progress/declined outreach signal present).",
|
|
7669
|
+
properties: {
|
|
7670
|
+
total_leads: { type: "number" },
|
|
7671
|
+
total_contacts: { type: "number" },
|
|
7672
|
+
leads_with_phone: { type: "number" },
|
|
7673
|
+
leads_with_email: { type: "number" },
|
|
7674
|
+
leads_with_coords: { type: "number" },
|
|
7675
|
+
leads_without_contacts: { type: "number" },
|
|
7676
|
+
leads_already_contacted: { type: "number" }
|
|
7677
|
+
}
|
|
7678
|
+
},
|
|
7679
|
+
readiness: {
|
|
7680
|
+
type: "object",
|
|
7681
|
+
description: "Pre-computed booleans the orchestrator prompt uses to decide which session modes to OFFER. ready_for_calling (phone coverage \u226560%), ready_for_emailing (email coverage \u226560%), needs_enrichment (\u226530% no-contact leads OR both phone+email coverage <40%), travel_friendly (\u22655 geocoded leads AND coord coverage \u226560%).",
|
|
7682
|
+
properties: {
|
|
7683
|
+
ready_for_calling: { type: "boolean" },
|
|
7684
|
+
ready_for_emailing: { type: "boolean" },
|
|
7685
|
+
needs_enrichment: { type: "boolean" },
|
|
7686
|
+
travel_friendly: { type: "boolean" }
|
|
7687
|
+
}
|
|
7688
|
+
},
|
|
7689
|
+
pagination: {
|
|
7690
|
+
type: "object",
|
|
7691
|
+
properties: {
|
|
7692
|
+
page: { type: "number" },
|
|
7693
|
+
pages: { type: "number" },
|
|
7694
|
+
total: { type: "number" }
|
|
7695
|
+
}
|
|
7696
|
+
},
|
|
7697
|
+
_meta: {
|
|
7698
|
+
type: "object",
|
|
7699
|
+
properties: {
|
|
7700
|
+
region: { type: "string" },
|
|
7701
|
+
latency_ms: { type: ["number", "null"] }
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
},
|
|
7705
|
+
required: ["campaign_id", "leads", "summary"]
|
|
7706
|
+
},
|
|
7707
|
+
execute: async (client, params) => {
|
|
7708
|
+
const count = params.count ?? 50;
|
|
7709
|
+
const page = params.page ?? 0;
|
|
7710
|
+
const [contactsRes, leadsRes] = await Promise.all([
|
|
7711
|
+
client.request("GET", `/campaigns/${params.campaign_id}/contacts`),
|
|
7712
|
+
client.request("GET", `/campaigns/${params.campaign_id}/leads?count=${count}&page=${page}`)
|
|
7713
|
+
]);
|
|
7714
|
+
const contactsByLeadId = /* @__PURE__ */ new Map();
|
|
7715
|
+
for (const row of contactsRes) {
|
|
7716
|
+
contactsByLeadId.set(row.lead_id, row);
|
|
7717
|
+
}
|
|
7718
|
+
const blocks = leadsRes.items.map((row) => {
|
|
7719
|
+
const lead = row.lead;
|
|
7720
|
+
const contactsRow = contactsByLeadId.get(lead.id);
|
|
7721
|
+
const contacts = (contactsRow?.contacts ?? []).map((c) => {
|
|
7722
|
+
const linkedin = normalizeLinkedin(c.contact.linkedin_page);
|
|
7723
|
+
const fallbackLi = linkedin ? null : constructLinkedinFallback(c.contact.first_name, c.contact.last_name, lead.name);
|
|
7724
|
+
const phone = (c.contact.phone_number ?? "").trim() || null;
|
|
7725
|
+
const telUrl = phone ? `tel:${phone.replace(/[^\d+]/g, "")}` : null;
|
|
7726
|
+
const email = (c.contact.email ?? "").trim() || null;
|
|
7727
|
+
const mailtoUrl = email ? `mailto:${email}` : null;
|
|
7728
|
+
const fullName = [c.contact.first_name, c.contact.last_name].filter(Boolean).join(" ").trim();
|
|
7729
|
+
return {
|
|
7730
|
+
id: c.contact.id,
|
|
7731
|
+
first_name: c.contact.first_name ?? null,
|
|
7732
|
+
last_name: c.contact.last_name ?? null,
|
|
7733
|
+
full_name: fullName || null,
|
|
7734
|
+
job_title: c.contact.job_title ?? null,
|
|
7735
|
+
phone_number: phone,
|
|
7736
|
+
phone_tel_url: telUrl,
|
|
7737
|
+
email,
|
|
7738
|
+
mailto_url: mailtoUrl,
|
|
7739
|
+
linkedin_url: linkedin ?? fallbackLi,
|
|
7740
|
+
linkedin_url_source: linkedin ? "leadbay" : fallbackLi ? "constructed" : null,
|
|
7741
|
+
recommended: c.contact.recommended ?? false,
|
|
7742
|
+
pinned: c.contact.pinned ?? false,
|
|
7743
|
+
pinned_by_ai: c.contact.pinned_by_ai ?? false,
|
|
7744
|
+
recent_notes: c.recent_notes ?? []
|
|
7745
|
+
};
|
|
7746
|
+
});
|
|
7747
|
+
contacts.sort((a, b) => {
|
|
7748
|
+
const aRank = (a.pinned_by_ai ? 4 : 0) + (a.recommended ? 2 : 0) + (a.phone_number ? 1 : 0);
|
|
7749
|
+
const bRank = (b.pinned_by_ai ? 4 : 0) + (b.recommended ? 2 : 0) + (b.phone_number ? 1 : 0);
|
|
7750
|
+
return bRank - aRank;
|
|
7751
|
+
});
|
|
7752
|
+
const splitSummary = lead.split_ai_summary ?? {};
|
|
7753
|
+
const headline = row.progress?.headline ?? null;
|
|
7754
|
+
return {
|
|
7755
|
+
lead_id: lead.id,
|
|
7756
|
+
lead_name: lead.name,
|
|
7757
|
+
score: lead.score ?? null,
|
|
7758
|
+
ai_agent_lead_score: lead.ai_agent_lead_score ?? null,
|
|
7759
|
+
website: lead.website ?? null,
|
|
7760
|
+
location: lead.location ?? null,
|
|
7761
|
+
company_phone_numbers: lead.phone_numbers ?? [],
|
|
7762
|
+
next_step: splitSummary.next_step ?? null,
|
|
7763
|
+
approach_angle: splitSummary.approach_angle ?? null,
|
|
7764
|
+
worth_pursuing: splitSummary.worth_pursuing ?? null,
|
|
7765
|
+
short_description: lead.short_description ?? null,
|
|
7766
|
+
last_action_headline: headline,
|
|
7767
|
+
progress: row.progress ?? null,
|
|
7768
|
+
affiliation: row.affiliation ?? null,
|
|
7769
|
+
contacts
|
|
7770
|
+
};
|
|
7771
|
+
});
|
|
7772
|
+
blocks.sort((a, b) => {
|
|
7773
|
+
const aScore = a.ai_agent_lead_score ?? a.score ?? 0;
|
|
7774
|
+
const bScore = b.ai_agent_lead_score ?? b.score ?? 0;
|
|
7775
|
+
return bScore - aScore;
|
|
7776
|
+
});
|
|
7777
|
+
const mapLocations = blocks.filter((b) => {
|
|
7778
|
+
const pos = b.location?.pos;
|
|
7779
|
+
return Array.isArray(pos) && pos.length === 2 && pos.every((n) => typeof n === "number");
|
|
7780
|
+
}).map((b) => {
|
|
7781
|
+
const topContact = b.contacts[0];
|
|
7782
|
+
const angle = b.next_step ?? b.approach_angle ?? "Worth pursuing";
|
|
7783
|
+
let notes;
|
|
7784
|
+
if (topContact?.phone_number && topContact?.full_name) {
|
|
7785
|
+
notes = `\u2605 ${angle} \u2014 call ${topContact.full_name}, \u260E ${topContact.phone_number}.`;
|
|
7786
|
+
} else if (topContact?.email && topContact?.full_name) {
|
|
7787
|
+
notes = `\u2605 ${angle} \u2014 email ${topContact.full_name} at ${topContact.email}.`;
|
|
7788
|
+
} else if (topContact?.full_name) {
|
|
7789
|
+
notes = `\u2605 ${angle} \u2014 reach ${topContact.full_name} (no channel enriched yet).`;
|
|
7790
|
+
} else {
|
|
7791
|
+
notes = `\u2605 ${angle} \u2014 enrich titles to surface a contact.`;
|
|
7792
|
+
}
|
|
7793
|
+
return {
|
|
7794
|
+
name: b.lead_name,
|
|
7795
|
+
address: b.location?.full ?? [b.location?.city, b.location?.state, b.location?.country].filter(Boolean).join(", "),
|
|
7796
|
+
latitude: b.location.pos[0],
|
|
7797
|
+
longitude: b.location.pos[1],
|
|
7798
|
+
notes: notes.slice(0, 280)
|
|
7799
|
+
// place-card notes truncate visibly past ~30 words
|
|
7800
|
+
};
|
|
7801
|
+
});
|
|
7802
|
+
const totalContacts = blocks.reduce((acc, b) => acc + b.contacts.length, 0);
|
|
7803
|
+
const leadsWithPhone = blocks.filter((b) => b.contacts.some((c) => c.phone_number) || b.company_phone_numbers.length > 0).length;
|
|
7804
|
+
const leadsWithEmail = blocks.filter((b) => b.contacts.some((c) => c.email)).length;
|
|
7805
|
+
const leadsWithoutContacts = blocks.filter((b) => b.contacts.length === 0).length;
|
|
7806
|
+
const leadsAlreadyContacted = blocks.filter((b) => hasOutreachSignal2(b.progress)).length;
|
|
7807
|
+
const leadsWithCoords = blocks.filter((b) => Array.isArray(b.location?.pos) && b.location.pos.length === 2 && b.location.pos.every((n) => typeof n === "number")).length;
|
|
7808
|
+
const total = blocks.length;
|
|
7809
|
+
const phoneRatio = total > 0 ? leadsWithPhone / total : 0;
|
|
7810
|
+
const emailRatio = total > 0 ? leadsWithEmail / total : 0;
|
|
7811
|
+
const coordRatio = total > 0 ? leadsWithCoords / total : 0;
|
|
7812
|
+
const noContactRatio = total > 0 ? leadsWithoutContacts / total : 0;
|
|
7813
|
+
const readiness = {
|
|
7814
|
+
ready_for_calling: phoneRatio >= 0.6,
|
|
7815
|
+
ready_for_emailing: emailRatio >= 0.6,
|
|
7816
|
+
needs_enrichment: noContactRatio >= 0.3 || phoneRatio < 0.4 && emailRatio < 0.4,
|
|
7817
|
+
travel_friendly: leadsWithCoords >= 5 && coordRatio >= 0.6
|
|
7818
|
+
};
|
|
7819
|
+
return {
|
|
7820
|
+
campaign_id: params.campaign_id,
|
|
7821
|
+
leads: blocks,
|
|
7822
|
+
map_locations: mapLocations,
|
|
7823
|
+
summary: {
|
|
7824
|
+
total_leads: blocks.length,
|
|
7825
|
+
total_contacts: totalContacts,
|
|
7826
|
+
leads_with_phone: leadsWithPhone,
|
|
7827
|
+
leads_with_email: leadsWithEmail,
|
|
7828
|
+
leads_with_coords: leadsWithCoords,
|
|
7829
|
+
leads_without_contacts: leadsWithoutContacts,
|
|
7830
|
+
leads_already_contacted: leadsAlreadyContacted
|
|
7831
|
+
},
|
|
7832
|
+
readiness,
|
|
7833
|
+
pagination: leadsRes.pagination,
|
|
7834
|
+
_meta: {
|
|
7835
|
+
region: client.region,
|
|
7836
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
7837
|
+
}
|
|
7838
|
+
};
|
|
7839
|
+
}
|
|
7840
|
+
};
|
|
7841
|
+
|
|
7842
|
+
// ../core/dist/composite/research-lead-by-id.js
|
|
7843
|
+
function normalizeLinkedinPage5(v) {
|
|
7844
|
+
if (v == null)
|
|
7845
|
+
return null;
|
|
7846
|
+
if (typeof v !== "string")
|
|
7847
|
+
return null;
|
|
7848
|
+
const trimmed = v.trim();
|
|
7849
|
+
if (!trimmed || trimmed.toLowerCase() === "null")
|
|
7850
|
+
return null;
|
|
7851
|
+
return trimmed;
|
|
7852
|
+
}
|
|
7853
|
+
function renderResearchLeadMarkdown(shape) {
|
|
7854
|
+
const out = [];
|
|
7855
|
+
const firm = shape.firmographics ?? {};
|
|
7856
|
+
const name = firm.name ?? "(unnamed lead)";
|
|
7857
|
+
out.push(`# ${name}`);
|
|
7858
|
+
if (firm.website)
|
|
7859
|
+
out.push(`Website: ${firm.website}`);
|
|
7860
|
+
if (firm.location)
|
|
7861
|
+
out.push(`Location: ${firm.location}`);
|
|
7862
|
+
if (typeof firm.score === "number" || firm.score === null) {
|
|
7863
|
+
const aiScore = firm.ai_agent_lead_score;
|
|
7864
|
+
out.push(`Score: ${firm.score ?? "\u2014"}` + (aiScore != null ? ` \xB7 AI: ${aiScore}` : ""));
|
|
7865
|
+
}
|
|
6803
7866
|
if (firm.short_description)
|
|
6804
7867
|
out.push(`
|
|
6805
7868
|
${firm.short_description}`);
|
|
@@ -11420,6 +12483,10 @@ var compositeReadTools = [
|
|
|
11420
12483
|
pullLeads,
|
|
11421
12484
|
pullFollowups,
|
|
11422
12485
|
followupsMap,
|
|
12486
|
+
tourPlan,
|
|
12487
|
+
listCampaigns,
|
|
12488
|
+
campaignProgression,
|
|
12489
|
+
campaignCallSheet,
|
|
11423
12490
|
researchLeadById,
|
|
11424
12491
|
researchLeadByNameFuzzy,
|
|
11425
12492
|
recallOrderedTitles,
|
|
@@ -11460,7 +12527,11 @@ var compositeWriteTools = [
|
|
|
11460
12527
|
// likeLead/dislikeLead are granular-shaped but should always be available
|
|
11461
12528
|
// to the agent without requiring LEADBAY_MCP_ADVANCED=1.
|
|
11462
12529
|
likeLead,
|
|
11463
|
-
dislikeLead
|
|
12530
|
+
dislikeLead,
|
|
12531
|
+
// Campaign write composites — persist a hand-picked cohort of leads.
|
|
12532
|
+
// Backend POST endpoints; gated behind LEADBAY_MCP_WRITE=1 in MCP.
|
|
12533
|
+
createCampaign,
|
|
12534
|
+
addLeadsToCampaign
|
|
11464
12535
|
];
|
|
11465
12536
|
var compositeTools = [
|
|
11466
12537
|
...compositeReadTools,
|