@leadbay/mcp 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.9.1 — 2026-05-16
|
|
4
|
+
|
|
5
|
+
**B23 fix — prompts no longer override per-tool RENDERING blocks**: 0.9.0 shipped RENDERING + NEXT STEPS blocks on every composite tool description. But agents still rendered prose for the daily-leads workflow, because the orchestrating `leadbay_daily_check_in` prompt's Phase 3 directed motivational one-line summaries that "won" over the per-tool RENDERING block in pull_leads. Phase 3 is rewritten to defer to the canonical pull_leads table layout (score bars, three columns, hide-list) and to add a 2–4 sentence "Today's nudges" paragraph ABOVE the table for the 3 most-promising rows — never in place of it. The same pattern is applied to `leadbay_qualify_top_n` (Phase 3 re-pulls newly-qualified leads via pull_leads and renders the canonical table, with a "Standouts from this batch" line above) and `leadbay_research_a_domain` (Phase 2 renders the research-company-card layout for the deep-dive result, with a 2–3 sentence summary above).
|
|
6
|
+
|
|
7
|
+
**New `gates/defer-to-tool-rendering` snippet — architectural prevention**: a new gate snippet codifies the rule "the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it." Included from every prompt that orchestrates a composite carrying a RENDERING block (`leadbay_daily_check_in`, `leadbay_qualify_top_n`, `leadbay_research_a_domain`, `leadbay_import_file`, `leadbay_prospecting_overview`, and the new `leadbay_followup_check_in`). A new assembler test enforces the gate include: any prompt whose `expected_calls` lists a composite with a RENDERING block MUST include the snippet — CI fails otherwise. Closes the door on future prompts quietly bypassing the contract.
|
|
8
|
+
|
|
9
|
+
**Discover-vs-Monitor routing fix (companion to B23)**: 0.9.0 shipped `leadbay_pull_followups` (the Monitor view) and `leadbay_pull_leads` (Discover wishlist) as parallel entry points but no orchestrator prompt parallel to `leadbay_daily_check_in` for the follow-up flow. When the user asked "leads I should follow up with today", no prompt auto-triggered and the agent freelanced — typically iterating pages of `pull_leads` filtering by `prospecting_actions_count > 0` (the wrong backend table). Four fixes:
|
|
10
|
+
|
|
11
|
+
- **New `leadbay_followup_check_in` prompt** — Monitor-view orchestrator paired 1:1 with `leadbay_daily_check_in` (Discovery). Triggers on "follow up", "already known leads", "leads I haven't contacted", "leads in [city]", "before my trip", "this week", "what's overdue", "who should I re-engage". Calls `leadbay_pull_followups` (never `pull_leads`), renders the canonical followups-table with a "Where to start today" paragraph above. Auto-emits as a Claude Code skill via the existing skills pipeline.
|
|
12
|
+
- **Narrowed `leadbay_daily_check_in` triggers** — `short_description` rewritten to scope to DISCOVERY phrasings ("best NEW leads", "what's new today", "let's prospect"); explicitly does NOT trigger on "what should I follow up on", "before my trip", etc. New failure_mode entry catches accidental routing into discovery from follow-up queries.
|
|
13
|
+
- **Anti-confusion guardrail in `leadbay_pull_followups` description** — names the specific failure mode ("iterating pages of `pull_leads` looking for rows with `prospecting_actions_count > 0` or `notes_count > 0` → STOP, wrong entry point") and points to `leadbay_followup_check_in` as the canonical orchestrator. The pair of backend tables doesn't share rows — a touched lead may age out of the new-leads queue entirely.
|
|
14
|
+
- **Routing-pair section in `leadbay_prospecting_overview`** — explicit mapping from user phrasing to orchestrator prompt, plus a hard rule never to call `pull_leads` directly for a follow-up query (or `pull_followups` for a discovery query).
|
|
15
|
+
|
|
16
|
+
**Server-side fix to the catalog filter**: the `buildPromptsCatalogParagraph` filter that drops bullets referencing unexposed `leadbay_*` tools now exempts prompt-name references too (prompts are always exposed). Previously a discovery bullet that pointed the follow-up flow to `leadbay_followup_check_in` would have been silently dropped from the catalog.
|
|
17
|
+
|
|
18
|
+
**Six new regression eval scenarios**: B23 rendering — `daily-check-in/rendering-table-contract`, `qualify-top-n/rendering-refresh-table`, `research-a-domain/rendering-card-contract`. Routing — `followup-check-in/routing-regression` (calls `pull_followups`, NEVER `pull_leads`), `followup-check-in/cross-mode-pivot` (recognizes the pivot offer to discovery), `followup-check-in/geo-followup` (handles geo without fabricating an `admin_area_id`). Picked up automatically by the existing eval framework via the per-prompt eval files; the new `leadbay_followup_check_in.eval.ts` runs all three.
|
|
19
|
+
|
|
20
|
+
**Files touched**: 1 new gate snippet, 1 new prompt (`leadbay_followup_check_in`), 5 modified prompts (`leadbay_daily_check_in`, `leadbay_qualify_top_n`, `leadbay_research_a_domain`, `leadbay_import_file`, `leadbay_prospecting_overview`), 1 modified tool description (`leadbay_pull_followups`), 1 server.ts catalog-filter fix, 1 new assembler test, 6 new scenario files, 4 modified/new eval files, 1 new invariants module, touchfile registry updated. No backend schema changes; no tool surface changes; same wire protocol as 0.9.0.
|
|
21
|
+
|
|
3
22
|
## 0.9.0 — 2026-05-16
|
|
4
23
|
|
|
5
24
|
**RENDERING + NEXT STEPS blocks in every composite tool description**: agents consuming composite tools today default to prose summaries when they don't know how to present the data. Tool descriptions now carry two new prescriptive blocks the agent reads verbatim — `RENDERING` (a recipe for how to present the response: table columns, glyph palette, link targets, fields to hide) and `NEXT STEPS` (an observation → suggestion table the agent picks 2–3 contextually relevant offers from, never reciting the whole menu). Lands on the seven highest-leverage composites: `leadbay_pull_leads`, `leadbay_research_company`, `leadbay_prepare_outreach`, `leadbay_bulk_qualify_leads`, `leadbay_import_leads`, `leadbay_import_and_qualify`, `leadbay_import_status`, `leadbay_list_mappable_fields`, `leadbay_resolve_import_rows`. The blocks add ~5–7k chars per description, so the audit's per-tool char budget was raised from 3500 → 12000 with a comment explaining the design tradeoff.
|
package/dist/bin.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
granularReadTools,
|
|
9
9
|
granularWriteTools,
|
|
10
10
|
resolveRegion
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ZRTAIXEJ.js";
|
|
12
12
|
|
|
13
13
|
// src/bin.ts
|
|
14
14
|
import { realpathSync } from "fs";
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
|
|
34
34
|
// src/prompts.generated.ts
|
|
35
35
|
var leadbay_daily_check_in = `
|
|
36
|
-
Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask: "get me leadbay leads", "best leads to prospect today", "what should I
|
|
36
|
+
Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask focused on NEW leads from the Discover wishlist: "get me leadbay leads", "best NEW leads to prospect today", "what's new today", "show me my batch", "let's prospect". For follow-up phrasings ("what should I follow up on", "leads I've already worked", "before my trip"), this is the wrong prompt \u2014 route to \`leadbay_followup_check_in\` instead. If the user's intent is ambiguous ("what should I work on?"), ASK once before picking an entry point.
|
|
37
37
|
|
|
38
38
|
# Resilience rules for Leadbay long-running tools
|
|
39
39
|
|
|
@@ -72,9 +72,93 @@ Call \`leadbay_account_status\` to see what quota I have left and which lens is
|
|
|
72
72
|
# PHASE 2 \u2014 FRESH BATCH
|
|
73
73
|
Call \`leadbay_pull_leads\` to get today's fresh batch. Capture \`response.lens.id\` (the response nests it under \`lens\`). **Use it as an explicit \`lensId\` argument on every subsequent Leadbay call this session** \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. (See Rule 1 above \u2014 a mid-session lens shift discards your top-10 work.)
|
|
74
74
|
|
|
75
|
-
# PHASE 3 \u2014 TRIAGE (top 10,
|
|
75
|
+
# PHASE 3 \u2014 TRIAGE (top 10, table + nudges)
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
Render the **top 10 leads** using the canonical \`leadbay_pull_leads\` layout:
|
|
78
|
+
|
|
79
|
+
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
80
|
+
|
|
81
|
+
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## RENDERING \u2014 markdown table, three columns, score-bar driven
|
|
85
|
+
|
|
86
|
+
Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
|
|
87
|
+
|
|
88
|
+
## Score-bar (10-segment, inline-code wrapped)
|
|
89
|
+
|
|
90
|
+
Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers \u2014 HTML \`<span>\` is stripped inside table cells.
|
|
91
|
+
|
|
92
|
+
Glyphs (use these exact characters; do not substitute):
|
|
93
|
+
|
|
94
|
+
- \`\u25B0\` \u2014 firmographic-only fill
|
|
95
|
+
- \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
|
|
96
|
+
- \`\u25B1\` \u2014 empty
|
|
97
|
+
|
|
98
|
+
Computation:
|
|
99
|
+
|
|
100
|
+
\`\`\`
|
|
101
|
+
total_filled = round(score / 10), clamped to 0..10
|
|
102
|
+
ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
|
|
103
|
+
clamped to [0, total_filled]
|
|
104
|
+
normal_filled = total_filled \u2212 ai_segments
|
|
105
|
+
bar = "\u25B0" \xD7 normal_filled
|
|
106
|
+
+ "\u2756" \xD7 ai_segments
|
|
107
|
+
+ "\u25B1" \xD7 (10 \u2212 total_filled)
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is null, set \`ai_segments = 0\` (no \u2756). Always wrap the bar in backticks. Print the legend \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster cap \xB7 \`\u25B1\` unfilled \`\` once below the table.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
**Column 1 \u2014 Company**
|
|
114
|
+
|
|
115
|
+
- Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
|
|
116
|
+
- Insert \`<br>\` between lines.
|
|
117
|
+
- Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
|
|
118
|
+
- Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
|
|
119
|
+
- Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
|
|
120
|
+
- Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
|
|
121
|
+
|
|
122
|
+
**Column 2 \u2014 Why it fits**
|
|
123
|
+
|
|
124
|
+
- One sentence, \u2264 20 words.
|
|
125
|
+
- Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead\` or \`leadbay_research_company\`.
|
|
126
|
+
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
127
|
+
- No bullet lists, no line breaks inside the cell.
|
|
128
|
+
|
|
129
|
+
**Column 3 \u2014 Contact**
|
|
130
|
+
|
|
131
|
+
\`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
|
|
132
|
+
|
|
133
|
+
**Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
|
|
134
|
+
|
|
135
|
+
## Linking a contact's name
|
|
136
|
+
|
|
137
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
138
|
+
|
|
139
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
140
|
+
|
|
141
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
142
|
+
|
|
143
|
+
\`\`\`
|
|
144
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
148
|
+
|
|
149
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
150
|
+
|
|
151
|
+
## Linking the company
|
|
152
|
+
|
|
153
|
+
Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
|
|
154
|
+
|
|
155
|
+
When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
|
|
156
|
+
|
|
157
|
+
\`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
ABOVE the table, add a 2\u20134 sentence "Today's nudges" paragraph for the 3 most-promising rows. The nudges speak to urgency / opportunity / freshness \u2014 what makes acting on these RIGHT NOW the right call. Do NOT repeat the "why it fits" column from the table; the nudges should add fresh framing the table doesn't carry (e.g., recent news from the \`qualification_summary\` excerpt, a window closing, a competitor activity the user mentioned earlier in the session). One sentence per nudge, salesperson voice, not coachspeak.
|
|
78
162
|
|
|
79
163
|
If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bulk_qualify_leads\` with \`lensId:<captured>\`, \`count:<1.5x deficit, capped at 25>\`, and **\`wait_for_completion:false\`**. Capture \`qualify_id\` from the response and poll \`leadbay_qualify_status\` every ~10s until \`status:'done'\`. Then re-pull with the same \`lensId\` to pick up the newly qualified leads. **Never re-pull without \`lensId\` \u2014 you will lose your batch to a lens shift.** (The \`leadbay_qualify_top_n\` slash-prompt wraps this same tool with a friendlier surface for users; agents should call the underlying tool directly here.)
|
|
80
164
|
|
|
@@ -114,6 +198,11 @@ A job well done has TWO deliverables:
|
|
|
114
198
|
IRON LAW \u2014 NO FABRICATION. Every lead id, contact email, custom field id, mapping decision, and tool argument must trace to a value you read from the file the user attached or to an output from a leadbay_* tool call in this session. Do not invent values. Do not "fill in" a missing leadId with a name match. Do not synthesize a CRM id from a guess. If a value is missing, leave the field blank and say so.
|
|
115
199
|
|
|
116
200
|
|
|
201
|
+
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
202
|
+
|
|
203
|
+
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
204
|
+
|
|
205
|
+
|
|
117
206
|
# PHASE 1 \u2014 SCAN
|
|
118
207
|
|
|
119
208
|
Read the file yourself. Inspect every header, sample values from multiple rows, row count, duplicate/blank columns, and obvious dirty data. Build a column preservation plan before importing: for each meaningful column decide standard field, CONTACT_* field, Leadbay note, custom field, derived helper, or skip with a reason. Default to preserving client-provided business data; skip only blank placeholders, duplicate plumbing, raw unparsed blobs after extracting their useful values, or values that would actively harm data quality.
|
|
@@ -315,9 +404,12 @@ Leadbay supports two parallel ways to find leads to act on. Detect which entry t
|
|
|
315
404
|
\`\`\`
|
|
316
405
|
Discovery entry Follow-up entry
|
|
317
406
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
318
|
-
leadbay_pull_leads
|
|
319
|
-
(new from the Discover
|
|
320
|
-
wishlist, lens-driven)
|
|
407
|
+
leadbay_pull_leads leadbay_pull_followups
|
|
408
|
+
(new from the Discover (re-engagement on the
|
|
409
|
+
wishlist, lens-driven) Monitor view of known leads)
|
|
410
|
+
\u2502 \u2502
|
|
411
|
+
wrapped by: wrapped by:
|
|
412
|
+
leadbay_daily_check_in leadbay_followup_check_in
|
|
321
413
|
\u2502 \u2502
|
|
322
414
|
\u2502 optional: \u2502 filters by user phrasing:
|
|
323
415
|
\u2502 research_company / \u2502 geo, sector, recency,
|
|
@@ -343,6 +435,14 @@ leadbay_pull_leads (re-engagement on the Monitor view \u2014
|
|
|
343
435
|
|
|
344
436
|
When in doubt, ask. The two paths return overlapping but differently-ranked data; presenting the wrong one wastes the user's time.
|
|
345
437
|
|
|
438
|
+
**Routing the user's first message to an entry point (orchestrator prompts):**
|
|
439
|
+
|
|
440
|
+
- "give me leads", "what's new", "let's prospect", "today's batch", "best NEW leads" \u2192 \`leadbay_daily_check_in\` (Discovery orchestrator \u2014 wraps \`leadbay_pull_leads\`)
|
|
441
|
+
- "follow up", "already known", "leads I should contact", "before my trip", "this week", "what's overdue", "re-engage" \u2192 \`leadbay_followup_check_in\` (Follow-up orchestrator \u2014 wraps \`leadbay_pull_followups\`)
|
|
442
|
+
- Ambiguous ("what should I work on?") \u2192 ASK once: "Looking for NEW leads from your wishlist, or follow-ups on leads you've already worked?"
|
|
443
|
+
|
|
444
|
+
Never call \`leadbay_pull_leads\` directly for a follow-up query. Never call \`leadbay_pull_followups\` for a discovery query. The two tools read from different backend tables; iterating pages of one to fake the other is a known failure mode (see the anti-confusion guardrail in \`pull_followups\`'s description).
|
|
445
|
+
|
|
346
446
|
## The outreach loop
|
|
347
447
|
|
|
348
448
|
After \`leadbay_prepare_outreach\` returns a brief, the agent drafts. Adapt to the user's connected outreach tools \u2014 these change the draft idiom:
|
|
@@ -397,22 +497,125 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` (the
|
|
|
397
497
|
## What to read once you've matched intent
|
|
398
498
|
|
|
399
499
|
You don't need to memorize every tool here \u2014 each tool's own description carries a RENDERING block (how to present the response) and a NEXT STEPS block (observation \u2192 suggestion table). Read the relevant tool's description in full when the user picks an entry point. This overview just gets you to the right starting tool.
|
|
500
|
+
|
|
501
|
+
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
502
|
+
|
|
503
|
+
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
400
504
|
`;
|
|
401
505
|
var leadbay_qualify_top_n = `
|
|
402
506
|
Qualify the top {{arg:count_or_default}} un-qualified leads in the active Leadbay lens.
|
|
403
507
|
|
|
508
|
+
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
509
|
+
|
|
510
|
+
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
511
|
+
|
|
512
|
+
|
|
404
513
|
# PHASE 1 \u2014 LAUNCH
|
|
405
514
|
Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}\`.
|
|
406
515
|
|
|
407
516
|
# PHASE 2 \u2014 POLL
|
|
408
|
-
While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive
|
|
517
|
+
While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive \u2014 one inline status sentence per check, never expanded into a card:
|
|
518
|
+
|
|
519
|
+
## Status / scalar \u2014 single-sentence shape
|
|
520
|
+
|
|
521
|
+
The response is a status confirmation or scalar \u2014 render exactly one sentence inline. Do NOT emit a card or a table. Do NOT enumerate the affected records (that's the next tool's job).
|
|
522
|
+
|
|
523
|
+
Template patterns to follow:
|
|
524
|
+
|
|
525
|
+
- Job kicked off \u2192 \`"\u2713 <Verb> N <noun(s)> \u2014 typically ~M minutes. I'll refresh when it's done."\`
|
|
526
|
+
- No work needed \u2192 \`"All N <noun(s)> already <state> \u2014 no work to do."\`
|
|
527
|
+
- Long-running \u2192 \`"\u23F3 <Verb> still running \u2014 N% complete; check back in ~M minutes."\`
|
|
528
|
+
- Failure \u2192 \`"\u26A0 <Verb> failed: <error>. <recovery hint>"\`
|
|
529
|
+
|
|
530
|
+
After the status line, propose the obvious refresh / progress-check / recovery action in the NEXT STEPS block. Never expand the status into a card.
|
|
531
|
+
|
|
409
532
|
|
|
410
533
|
# PHASE 3 \u2014 SUMMARIZE
|
|
411
534
|
|
|
412
|
-
When \`bulk_qualify_leads\` returns,
|
|
413
|
-
|
|
414
|
-
-
|
|
415
|
-
|
|
535
|
+
When \`bulk_qualify_leads\` returns, surface results in two parts.
|
|
536
|
+
|
|
537
|
+
**Status line first** \u2014 one sentence using the status-inline shape above: how many qualified, how many are still running (name them by lead_id + lead name if available so the user can poll later).
|
|
538
|
+
|
|
539
|
+
**Then a refreshed table** \u2014 re-pull the newly-qualified leads via \`leadbay_pull_leads\` with the same \`lensId\` and render them using the canonical pull_leads layout:
|
|
540
|
+
|
|
541
|
+
## RENDERING \u2014 markdown table, three columns, score-bar driven
|
|
542
|
+
|
|
543
|
+
Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
|
|
544
|
+
|
|
545
|
+
## Score-bar (10-segment, inline-code wrapped)
|
|
546
|
+
|
|
547
|
+
Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers \u2014 HTML \`<span>\` is stripped inside table cells.
|
|
548
|
+
|
|
549
|
+
Glyphs (use these exact characters; do not substitute):
|
|
550
|
+
|
|
551
|
+
- \`\u25B0\` \u2014 firmographic-only fill
|
|
552
|
+
- \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
|
|
553
|
+
- \`\u25B1\` \u2014 empty
|
|
554
|
+
|
|
555
|
+
Computation:
|
|
556
|
+
|
|
557
|
+
\`\`\`
|
|
558
|
+
total_filled = round(score / 10), clamped to 0..10
|
|
559
|
+
ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
|
|
560
|
+
clamped to [0, total_filled]
|
|
561
|
+
normal_filled = total_filled \u2212 ai_segments
|
|
562
|
+
bar = "\u25B0" \xD7 normal_filled
|
|
563
|
+
+ "\u2756" \xD7 ai_segments
|
|
564
|
+
+ "\u25B1" \xD7 (10 \u2212 total_filled)
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is null, set \`ai_segments = 0\` (no \u2756). Always wrap the bar in backticks. Print the legend \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster cap \xB7 \`\u25B1\` unfilled \`\` once below the table.
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
**Column 1 \u2014 Company**
|
|
571
|
+
|
|
572
|
+
- Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
|
|
573
|
+
- Insert \`<br>\` between lines.
|
|
574
|
+
- Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
|
|
575
|
+
- Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
|
|
576
|
+
- Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
|
|
577
|
+
- Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
|
|
578
|
+
|
|
579
|
+
**Column 2 \u2014 Why it fits**
|
|
580
|
+
|
|
581
|
+
- One sentence, \u2264 20 words.
|
|
582
|
+
- Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead\` or \`leadbay_research_company\`.
|
|
583
|
+
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
584
|
+
- No bullet lists, no line breaks inside the cell.
|
|
585
|
+
|
|
586
|
+
**Column 3 \u2014 Contact**
|
|
587
|
+
|
|
588
|
+
\`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
|
|
589
|
+
|
|
590
|
+
**Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
|
|
591
|
+
|
|
592
|
+
## Linking a contact's name
|
|
593
|
+
|
|
594
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
595
|
+
|
|
596
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
597
|
+
|
|
598
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
599
|
+
|
|
600
|
+
\`\`\`
|
|
601
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
602
|
+
\`\`\`
|
|
603
|
+
|
|
604
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
605
|
+
|
|
606
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
607
|
+
|
|
608
|
+
## Linking the company
|
|
609
|
+
|
|
610
|
+
Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
|
|
611
|
+
|
|
612
|
+
When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
|
|
613
|
+
|
|
614
|
+
\`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
ABOVE the table, add a 1\u20132 sentence "Standouts from this batch" line that calls out the 3 highest-\`ai_agent_lead_score\` rows \u2014 this is supplementary commentary, not a replacement for the table.
|
|
416
619
|
|
|
417
620
|
# PHASE 4 \u2014 RECOMMEND
|
|
418
621
|
Recommend the single most-promising lead from this batch and offer to research it deeply with \`leadbay_research_lead\`. Do not actually call \`research_lead\` yet \u2014 wait for my go.
|
|
@@ -434,30 +637,136 @@ var leadbay_research_a_domain = `
|
|
|
434
637
|
IRON LAW \u2014 NO FABRICATION. Every lead id, contact email, custom field id, mapping decision, and tool argument must trace to a value you read from the file the user attached or to an output from a leadbay_* tool call in this session. Do not invent values. Do not "fill in" a missing leadId with a name match. Do not synthesize a CRM id from a guess. If a value is missing, leave the field blank and say so.
|
|
435
638
|
|
|
436
639
|
|
|
640
|
+
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
641
|
+
|
|
642
|
+
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
643
|
+
|
|
644
|
+
|
|
437
645
|
Research the company with domain '{{arg:domain}}' for me using Leadbay.
|
|
438
646
|
|
|
439
647
|
# PHASE 1 \u2014 IMPORT + QUALIFY
|
|
440
|
-
Call \`leadbay_import_and_qualify\` with \`domains=[{domain:'{{arg:domain}}'}]\`. This imports the lead AND runs AI qualification in one call. If the response indicates \`quota_blocked\` or \`still_running\`, say so explicitly.
|
|
648
|
+
Call \`leadbay_import_and_qualify\` with \`domains=[{domain:'{{arg:domain}}'}]\`. This imports the lead AND runs AI qualification in one call. If the response indicates \`quota_blocked\` or \`still_running\`, say so explicitly. Render the import status using the canonical terse single-record summary (never an enumeration of every imported lead):
|
|
649
|
+
|
|
650
|
+
## RENDERING \u2014 import result summary (single-record, terse)
|
|
651
|
+
|
|
652
|
+
The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
|
|
653
|
+
|
|
654
|
+
**Header \u2014 single line, choose by status:**
|
|
655
|
+
|
|
656
|
+
- Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
|
|
657
|
+
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
658
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
659
|
+
|
|
660
|
+
**When failures or ambiguous rows are non-empty**, follow the header with a small bulleted list (\u2264 5 items): \`<row identifier or domain> \xB7 <reason>\`. Then \`"*+N more \u2014 leadbay_import_status for full detail*"\`.
|
|
661
|
+
|
|
662
|
+
**When the user's request implied a downstream use** ("import then prep outreach for them"), emit \`Imported leadIds: <up to 5 ids, then '+N more'>\` \u2014 just the ids. Let the next composite render the leads.
|
|
663
|
+
|
|
664
|
+
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead\` in NEXT STEPS.
|
|
665
|
+
|
|
441
666
|
|
|
442
667
|
# PHASE 2 \u2014 DEEP DIVE
|
|
443
|
-
When the import resolves, call \`leadbay_research_lead\` on the new leadId.
|
|
668
|
+
When the import resolves, call \`leadbay_research_lead\` on the new leadId. Render the result using the canonical single-record card layout \u2014 detect MODE A (Discovery) since the user asked to "research" a domain rather than to prepare outreach:
|
|
669
|
+
|
|
670
|
+
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
671
|
+
|
|
672
|
+
Present as a single-record card, not a table. This tool gets invoked in two distinct user contexts \u2014 detect which and adapt the body density accordingly.
|
|
673
|
+
|
|
674
|
+
**MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`leadbay_research_lead\`, or decide whether to start outreach.
|
|
675
|
+
|
|
676
|
+
**MODE B \u2014 Contact preparation.** The user is about to call or email someone at this company and needs the talking points. Signals: "I'm calling them", "draft an email", "before my call", "outreach prep", "what should I say", or the conversation has already touched on a specific contact. Next step is usually \`leadbay_prepare_outreach\`.
|
|
677
|
+
|
|
678
|
+
Default to MODE A when uncertain. Always offer the cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
679
|
+
|
|
680
|
+
### Common structure (both modes)
|
|
681
|
+
|
|
682
|
+
- **Header** (H4 or H5): \`<10-segment score bar>\` \`[Company name](website)\`. Use the score-bar algorithm; the bar lives in a single inline-code span. Prefix \`https://\` to website if it's a bare hostname.
|
|
683
|
+
- **Pill row** (immediately below the header): short location \xB7 compact size \xB7 social pill chips iterated over \`social_urls\` (each non-null platform becomes \`[<platform-label>](<url>)\`) \xB7 \`[website-domain](website)\` \xB7 \`\u260E phone\` when \`phone_numbers[]\` is non-empty (use the first number). All \` \xB7 \`-separated.
|
|
684
|
+
- **Blurb**: render \`description\` (preferred) or \`short_description\` as a single blockquoted paragraph.
|
|
685
|
+
- **Staleness line**: italic, \`"Researched <relative time>"\` from \`web_insights_fetched_at\`. Use \`"today"\` / \`"yesterday"\` / \`"N days ago"\` up to 30 days, then absolute date. Prefix with \`\u26A0\` if older than 30 days.
|
|
686
|
+
- **Contacts table** (always at the bottom):
|
|
687
|
+
\`\`\`
|
|
688
|
+
| | Name | Title | LinkedIn |
|
|
689
|
+
\`\`\`
|
|
690
|
+
Markers in column 1:
|
|
691
|
+
- \`\u2605\` \u2014 \`recommended_contact\` match.
|
|
692
|
+
- \`\u{1F48E}\` \u2014 name fuzzy-matches a \`hot: true\` entry in \`web_insights\` key_people. (Use \`\u{1F48E}\`, not \`\u{1F525}\`, to avoid glyph collision with the follow-up status badge.)
|
|
693
|
+
Sort \`\u2605\` first, then \`\u{1F48E}\`-only rows, then API order. Link the name via \`linkedin_page\` first; fall back to LinkedIn people-search with \`<First>+<Last>+<Company>\`. Append \`\xB0\` only when the fallback is in use AND \`social_presence.linkedin == false\`. Cap to 6 rows; if \`contacts_count > shown\`, end with \`"+N more \u2014 ask to see the full list"\`.
|
|
694
|
+
|
|
695
|
+
### MODE A body (Discovery, fuller, scannable)
|
|
696
|
+
|
|
697
|
+
Render each non-empty \`web_insights\` section as H5 with the emoji + label intact. Section order: \`\u{1F3E2} company profile\` \u2192 \`\u{1F4C8} business signals\` \u2192 \`\u{1F4A1} prospecting clues\` \u2192 \`\u{1F9E9} strategic positioning\` \u2192 \`\u{1F50E} technologies & innovation\`. Inside each, bullet 3\u20135 items. Sort \`hot: true\` items first. **Bold** the description text of hot items; leave cold items plain. Render \`source\` as \`[source](url)\` at the end; include \`date\` when present. Omit empty sections. Skip \`\u{1F517} social links\` (already in the pill row) and \`\u{1F464} key people\` (already in the contacts table).
|
|
698
|
+
|
|
699
|
+
### MODE B body (Contact preparation, tighter)
|
|
700
|
+
|
|
701
|
+
Render exactly two H5 sections:
|
|
702
|
+
|
|
703
|
+
##### \u{1F3AF} Conversation hooks
|
|
704
|
+
|
|
705
|
+
Distill the 3 most recent / most hot signals from \`\u{1F4C8} business signals\` and \`\u{1F4A1} prospecting clues\` into one-sentence talking points in salesperson voice. Strip the academic framing. Cite the source inline.
|
|
706
|
+
|
|
707
|
+
##### \u{1F464} About the person *(only when recommended_contact is non-empty)*
|
|
708
|
+
|
|
709
|
+
2-line summary: their title + any context from \`web_insights\` key_people. If they appear in a hot signal ("X appointed CEO"), surface that prominently.
|
|
710
|
+
|
|
711
|
+
Skip \u{1F3E2} profile, \u{1F9E9} strategic positioning, \u{1F50E} technologies in MODE B \u2014 context the user doesn't need for the next 30 seconds.
|
|
712
|
+
|
|
713
|
+
If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification: N questions answered, avg boost X"\` and offer to expand in NEXT STEPS.
|
|
714
|
+
|
|
715
|
+
**Hide:** \`id\`, \`lead.id\`, \`contact.id\`, \`lead.location.pos\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`recommended_contact_title\` (duplicates \`recommended_contact.job_title\`), empty arrays, fields whose value is the string \`"null"\`, \`contact.source\` (internal), insights whose \`source\` is empty.
|
|
716
|
+
|
|
717
|
+
**Legend (print once below the card):** \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster \xB7 \`\u25B1\` unfilled \xB7 \u2605 recommended \xB7 \u{1F48E} hot in web_insights \xB7 \xB0 = no company LinkedIn (fallback link only) \`\`
|
|
718
|
+
|
|
719
|
+
## Linking a contact's name
|
|
720
|
+
|
|
721
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
722
|
+
|
|
723
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
724
|
+
|
|
725
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
726
|
+
|
|
727
|
+
\`\`\`
|
|
728
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
729
|
+
\`\`\`
|
|
730
|
+
|
|
731
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
732
|
+
|
|
733
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
734
|
+
|
|
735
|
+
## Linking the company
|
|
736
|
+
|
|
737
|
+
Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
|
|
738
|
+
|
|
739
|
+
When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
|
|
740
|
+
|
|
741
|
+
\`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
|
|
742
|
+
|
|
743
|
+
|
|
444
744
|
|
|
445
745
|
# PHASE 3 \u2014 SUMMARY
|
|
446
|
-
|
|
746
|
+
Place a 2\u20133 sentence summary ABOVE the card with:
|
|
447
747
|
- Who is this company (1 sentence)
|
|
448
748
|
- Their fit (cite specific \`qualification_answers\` from the qualification response)
|
|
449
|
-
-
|
|
450
|
-
- Which contact would I email first (name, role, source)
|
|
749
|
+
- Which contact would I email first (one short clause \u2014 the card's contacts table carries the rest)
|
|
451
750
|
|
|
452
|
-
Be honest about uncertainty: if any field
|
|
751
|
+
The card itself handles the signal callouts (\`\u{1F4C8} business signals\`, \`\u{1F4A1} prospecting clues\`). Do NOT re-narrate signals in prose above the card \u2014 that's what the card sections are for. Be honest about uncertainty: if any field is missing from tool responses, say "not surfaced by qualification" rather than guessing.
|
|
453
752
|
`;
|
|
454
753
|
var PROMPT_META = {
|
|
455
|
-
leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": `Run the canonical daily check-in:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
754
|
+
leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": `Run the canonical daily check-in: account state, fresh batch, triage
|
|
755
|
+
top 10, deep-dive every promising one, offer contact enrichment. The
|
|
756
|
+
morning DISCOVERY workflow (new leads from the lens wishlist). Trigger
|
|
757
|
+
on "leadbay leads", "best NEW leads", "what's new today", "show me the
|
|
758
|
+
day's batch", "let's prospect". Do NOT trigger on follow-up phrasings
|
|
759
|
+
("what should I follow up on", "before my trip") \u2014 those go to
|
|
760
|
+
\`leadbay_followup_check_in\`.
|
|
761
|
+
`, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_research_lead", "leadbay_bulk_qualify_leads", "leadbay_enrich_contacts"], "failure_modes": ["Calls leadbay_report_outreach without explicit user authorization", "Surfaces fewer than 10 leads when more are available, or fails to top up via leadbay_qualify_top_n when the batch is short", `Replaces the canonical pull_leads table layout with prose per row (the per-tool RENDERING block is the structural contract; "Today's nudges" goes above it, not in place of it)`, "Skips the nudge paragraph entirely \u2014 the table alone is fine but adding the nudge is the value-add", "Skips deep research on promising leads (Phase 4) \u2014 the agent must call leadbay_research_lead on each, not just one", "Triggers contact enrichment without asking the user first (it consumes quota)", "Skips the STOP byproduct and proposes next actions on its own", 'Fires 10 parallel leadbay_research_lead calls and treats "stream closed" errors as terminal \u2014 must serialize and retry singletons', "Re-pulls leadbay_pull_leads without passing the captured lensId, allowing a backend lens shift to discard the Phase 2 batch", 'Treats a "Request timed out" from leadbay_bulk_qualify_leads as terminal instead of retrying with wait_for_completion:false + qualify_status polling', 'Triggers on a follow-up query (e.g., "leads I should follow up with") that should have routed to `leadbay_followup_check_in` \u2014 the two entry points are different data sources (Discover wishlist vs Monitor view) per \xA71.6'] },
|
|
762
|
+
leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": `Run the canonical follow-up check-in: surface KNOWN leads from the
|
|
763
|
+
Monitor view that need re-engagement today, ranked by AI urgency,
|
|
764
|
+
with the canonical pull_followups table layout. Trigger when the
|
|
765
|
+
user asks "follow up", "already known leads", "leads I haven't
|
|
766
|
+
contacted", "leads in [city]", "before my trip", "this week",
|
|
767
|
+
"this month", "what's overdue", "who should I re-engage", or
|
|
768
|
+
anything that implies pre-existing pipeline context.
|
|
769
|
+
`, "arguments": [], "expected_calls": ["leadbay_pull_followups", "leadbay_research_lead", "leadbay_prepare_outreach"], "failure_modes": ["Calls leadbay_pull_leads (the Discover entry point) instead of leadbay_pull_followups \u2014 these are different data sources; the Discover queue does NOT contain Monitor's known-but-cold pipeline", 'Iterates pages of leadbay_pull_leads filtering by engagement_count to "fake" a follow-up view (a real bug observed in 0.9.0 \u2014 the right move is to call pull_followups directly)', "Replaces the canonical pull_followups table layout with prose per row (the per-tool RENDERING block is the structural contract; commentary belongs above or below)", 'Skips the cross-mode pivot offer at the end ("Want to see NEW leads from your wishlist instead?" routes to leadbay_pull_leads)'] },
|
|
461
770
|
leadbay_import_file: { "name": "leadbay_import_file", "short_description": "Import a user-supplied CSV/file into Leadbay through five phases with\nevidence gates \u2014 scan, derive, resolve identities, preserve & commit,\nthen optionally qualify and report. The job is to maximize how many\nrows the Leadbay system actually ingests and matches.\n", "arguments": [{ "name": "file", "description": "Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.", "required": false }, { "name": "instruction", "description": 'Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".', "required": false }], "expected_calls": ["leadbay_resolve_import_rows", "leadbay_list_mappable_fields", "leadbay_create_custom_field", "leadbay_import_leads", "leadbay_import_and_qualify", "leadbay_add_note", "leadbay_import_status"], "failure_modes": ["Picks LEADBAY_ID from score alone, name-only, fuzzy-name-only, root-domain-only, brand-only, postcode-only, or city-only evidence", "Drops meaningful business notes or CRM record links instead of preserving them as custom fields or lead notes", "Treats a consumer mailbox domain (gmail.com, hotmail.com, ...) as the company domain", "Skips deriving company_domain from a business email when no website column exists (this kills match rate)", "Skips the COLUMN PRESERVATION PLAN byproduct before importing", "Skips the DECISION LOG byproduct before writing LEADBAY_ID", "Returns the imported records WITHOUT writing LEADBAY_ID values back into the user's file (leaves the user no audit trail of what matched)", "Fabricates leadIds, contact emails, or mapping IDs not present in the file or a tool response"] },
|
|
462
771
|
leadbay_log_outreach: { "name": "leadbay_log_outreach", "short_description": "Log outreach (an email I sent, a call I made, a meeting I had) on a\nspecific lead. Captures verification so the SDR pipeline trusts the entry.\n", "arguments": [{ "name": "lead_id", "description": "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead.", "required": true }, { "name": "summary", "description": "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').", "required": true }], "expected_calls": ["leadbay_report_outreach"], "failure_modes": ["Calls leadbay_report_outreach without first collecting a verification source", "Fabricates a gmail_message_id or calendar_event_id (the human team treats verification as canonical)", "Records outreach to a different lead_id than the one the user supplied", "Skips the dry_run step when the user is unsure what would be sent"] },
|
|
463
772
|
leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
|
|
@@ -466,14 +775,15 @@ snooze, and the connected-outreach-tool registry. Trigger when the
|
|
|
466
775
|
conversation involves Leadbay leads, prospecting, pipeline, follow-up,
|
|
467
776
|
outreach, or lens / ICP \u2014 anything from "show me my leads" to "what
|
|
468
777
|
should I follow up on" to "I'll send via lemlist".
|
|
469
|
-
`, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_research_company", "leadbay_research_lead", "leadbay_prepare_outreach", "leadbay_report_outreach", "leadbay_set_pushback", "leadbay_remove_pushback", "leadbay_bulk_qualify_leads", "leadbay_enrich_titles", "leadbay_import_leads", "leadbay_add_note", "leadbay_adjust_audience"], "failure_modes": ['Drives outreach without asking the user "how did it go?" afterwards \u2014 leaving prospecting_actions and epilogue_status stale', 'Says "epilogue" in user-facing dialogue instead of "outcome"', 'Says "Monitor" in user-facing dialogue instead of "follow-ups"', 'Treats a "not now / next quarter" reply as a note instead of routing through the pushback mechanism', "Drafts outreach in a generic format when the user has a connected sequencer (lemlist, Outreach.io, etc.) that has its own idiom", "Re-pulls leads without passing the captured lensId, allowing a backend lens shift to discard prior work", "Skips the STOP byproduct in any multi-step workflow it triggers"] },
|
|
470
|
-
leadbay_qualify_top_n: { "name": "leadbay_qualify_top_n", "short_description": "Bulk-qualify the top N un-qualified leads in the active lens. Uses\nleadbay_bulk_qualify_leads with a sensible default budget.\n", "arguments": [{ "name": "count", "description": "How many leads to qualify (default 10, max 25). Higher counts may take 5+ minutes.", "required": false }], "expected_calls": ["leadbay_bulk_qualify_leads", "leadbay_research_lead"], "failure_modes": ["Picks a count larger than the user asked for (or larger than the max 25)", "Glosses over still-running leads in the summary instead of naming them", "Recommends a lead from the existing qualified pool instead of one from this batch's actual results"] },
|
|
778
|
+
`, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_research_company", "leadbay_research_lead", "leadbay_prepare_outreach", "leadbay_report_outreach", "leadbay_set_pushback", "leadbay_remove_pushback", "leadbay_bulk_qualify_leads", "leadbay_enrich_titles", "leadbay_import_leads", "leadbay_add_note", "leadbay_adjust_audience"], "failure_modes": ['Drives outreach without asking the user "how did it go?" afterwards \u2014 leaving prospecting_actions and epilogue_status stale', 'Says "epilogue" in user-facing dialogue instead of "outcome"', 'Says "Monitor" in user-facing dialogue instead of "follow-ups"', 'Treats a "not now / next quarter" reply as a note instead of routing through the pushback mechanism', "Drafts outreach in a generic format when the user has a connected sequencer (lemlist, Outreach.io, etc.) that has its own idiom", "Re-pulls leads without passing the captured lensId, allowing a backend lens shift to discard prior work", "Skips the STOP byproduct in any multi-step workflow it triggers", "Calls leadbay_pull_leads (Discover wishlist) for a follow-up query, or leadbay_pull_followups (Monitor view) for a discovery query \u2014 the two entry points read from different backend tables; the right orchestrators are leadbay_daily_check_in (discovery) and leadbay_followup_check_in (follow-up)"] },
|
|
779
|
+
leadbay_qualify_top_n: { "name": "leadbay_qualify_top_n", "short_description": "Bulk-qualify the top N un-qualified leads in the active lens. Uses\nleadbay_bulk_qualify_leads with a sensible default budget.\n", "arguments": [{ "name": "count", "description": "How many leads to qualify (default 10, max 25). Higher counts may take 5+ minutes.", "required": false }], "expected_calls": ["leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_pull_leads", "leadbay_research_lead"], "failure_modes": ["Picks a count larger than the user asked for (or larger than the max 25)", "Glosses over still-running leads in the summary instead of naming them", "Recommends a lead from the existing qualified pool instead of one from this batch's actual results", 'Replaces the canonical pull_leads table with prose when rendering the newly-qualified batch (the per-tool RENDERING block is the structural contract; "standouts" commentary sits above it)', "Expands the qualify-status sentence into a card or table instead of the one-line status-inline render"] },
|
|
471
780
|
leadbay_refine_audience: { "name": "leadbay_refine_audience", "short_description": "Refine the kind of leads Leadbay surfaces beyond firmographics, with a\nfree-text instruction. Handles the clarification round-trip if the new\nprompt is ambiguous.\n", "arguments": [{ "name": "instruction", "description": "The refinement (e.g. 'focus on hospitals running their own IT'). Set to plain English.", "required": true }], "expected_calls": ["leadbay_refine_prompt", "leadbay_account_status"], "failure_modes": ["Calls leadbay_answer_clarification on the user's behalf instead of surfacing the clarification verbatim", "Glosses over the clarification options instead of presenting them as offered", "Promises immediate effect when status='applied' actually triggers an async intelligence recompute"] },
|
|
472
|
-
leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Import a company by domain and run deep qualification + research in one\npass. Use when a colleague mentions a name and you want everything Leadbay\nknows about it.\n", "arguments": [{ "name": "domain", "description": "The company's primary domain (e.g. 'acme.com'). Protocol/path are stripped.", "required": true }], "expected_calls": ["leadbay_import_and_qualify", "leadbay_research_lead"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Reports certainty about fit when qualification didn't actually run (e.g. quota_blocked)", "Skips the research step after import completes"] }
|
|
781
|
+
leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Import a company by domain and run deep qualification + research in one\npass. Use when a colleague mentions a name and you want everything Leadbay\nknows about it.\n", "arguments": [{ "name": "domain", "description": "The company's primary domain (e.g. 'acme.com'). Protocol/path are stripped.", "required": true }], "expected_calls": ["leadbay_import_and_qualify", "leadbay_research_lead"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Reports certainty about fit when qualification didn't actually run (e.g. quota_blocked)", "Skips the research step after import completes", "Renders the research_lead result as a freeform narrative instead of the canonical research-company-card layout (the card with header score bar, pill row, signal sections, contacts table is the structural contract; commentary belongs ABOVE or BELOW it)", "Enumerates every imported lead in prose instead of the terse single-record summary from the import-result rendering snippet"] }
|
|
473
782
|
};
|
|
474
783
|
var PROMPT_CATALOG_HEADER = `This server exposes the following workflow prompts via \`prompts/list\` and \`prompts/get\`. Some MCP clients render them as slash commands; if your client does not, you (the agent) should invoke them directly via \`prompts/get\` when the user's request matches one of the triggers described below.`;
|
|
475
784
|
var PROMPT_CATALOG_BULLETS = {
|
|
476
|
-
leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Run the canonical daily check-in:
|
|
785
|
+
leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Run the canonical daily check-in: account state, fresh batch, triage top 10, deep-dive every promising one, offer contact enrichment. The morning DISCOVERY workflow (new leads from the lens wishlist). Trigger on "leadbay leads", "best NEW leads", "what's new today", "show me the day's batch", "let's prospect". Do NOT trigger on follow-up phrasings ("what should I follow up on", "before my trip") \u2014 those go to \`leadbay_followup_check_in\`.`,
|
|
786
|
+
leadbay_followup_check_in: `- \`leadbay_followup_check_in\`: Run the canonical follow-up check-in: surface KNOWN leads from the Monitor view that need re-engagement today, ranked by AI urgency, with the canonical pull_followups table layout. Trigger when the user asks "follow up", "already known leads", "leads I haven't contacted", "leads in [city]", "before my trip", "this week", "this month", "what's overdue", "who should I re-engage", or anything that implies pre-existing pipeline context.`,
|
|
477
787
|
leadbay_import_file: `- \`leadbay_import_file\` (optional args: file, instruction): Import a user-supplied CSV/file into Leadbay through five phases with evidence gates \u2014 scan, derive, resolve identities, preserve & commit, then optionally qualify and report. The job is to maximize how many rows the Leadbay system actually ingests and matches.`,
|
|
478
788
|
leadbay_log_outreach: `- \`leadbay_log_outreach\` (required args: lead_id, summary): Log outreach (an email I sent, a call I made, a meeting I had) on a specific lead. Captures verification so the SDR pipeline trusts the entry.`,
|
|
479
789
|
leadbay_prospecting_overview: `- \`leadbay_prospecting_overview\`: Orientation for working with Leadbay from any host \u2014 discovery vs. follow-up, the outreach loop, outcome recording, imports, pushback / snooze, and the connected-outreach-tool registry. Trigger when the conversation involves Leadbay leads, prospecting, pipeline, follow-up, outreach, or lens / ICP \u2014 anything from "show me my leads" to "what should I follow up on" to "I'll send via lemlist".`,
|
|
@@ -734,6 +1044,7 @@ function buildRhythmParagraph(has) {
|
|
|
734
1044
|
return "Suggested rhythm: a healthy agent pattern is a daily check-in \u2014 pull fresh leads, skim the auto-qualified top, deepen 1-3 promising ones, propose outreach to the user. If your host supports scheduling, offer to set up a daily run.";
|
|
735
1045
|
}
|
|
736
1046
|
var TOOL_REFERENCE_PATTERN = /\bleadbay_[a-z][a-z0-9_]*\b/g;
|
|
1047
|
+
var PROMPT_NAMES = new Set(Object.keys(PROMPT_CATALOG_BULLETS));
|
|
737
1048
|
function buildPromptsCatalogParagraph(has) {
|
|
738
1049
|
const safeBullets = [];
|
|
739
1050
|
for (const [promptName, bullet] of Object.entries(PROMPT_CATALOG_BULLETS)) {
|
|
@@ -741,6 +1052,7 @@ function buildPromptsCatalogParagraph(has) {
|
|
|
741
1052
|
for (const match of bullet.matchAll(TOOL_REFERENCE_PATTERN)) {
|
|
742
1053
|
const name = match[0];
|
|
743
1054
|
if (name === promptName) continue;
|
|
1055
|
+
if (PROMPT_NAMES.has(name)) continue;
|
|
744
1056
|
referencedTools.add(name);
|
|
745
1057
|
}
|
|
746
1058
|
const allExposed = [...referencedTools].every((n) => has(n));
|
|
@@ -1061,7 +1373,7 @@ function buildServer(client, opts = {}) {
|
|
|
1061
1373
|
|
|
1062
1374
|
// src/bin.ts
|
|
1063
1375
|
import { createRequire } from "module";
|
|
1064
|
-
var VERSION = "0.9.
|
|
1376
|
+
var VERSION = "0.9.1";
|
|
1065
1377
|
var HELP = `
|
|
1066
1378
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
1067
1379
|
|
|
@@ -1350,7 +1662,7 @@ async function runLogin(args) {
|
|
|
1350
1662
|
let result;
|
|
1351
1663
|
try {
|
|
1352
1664
|
if (pinnedRegion && !allowFallback) {
|
|
1353
|
-
const { REGIONS } = await import("./dist-
|
|
1665
|
+
const { REGIONS } = await import("./dist-X5AOPZ2V.js");
|
|
1354
1666
|
const baseUrl = REGIONS[pinnedRegion];
|
|
1355
1667
|
const c = createClient({ region: pinnedRegion });
|
|
1356
1668
|
const token = await loginAt(baseUrl, email, password);
|
|
@@ -1842,7 +2154,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
1842
2154
|
let region;
|
|
1843
2155
|
try {
|
|
1844
2156
|
if (pinnedRegion && !allowFallback) {
|
|
1845
|
-
const { REGIONS } = await import("./dist-
|
|
2157
|
+
const { REGIONS } = await import("./dist-X5AOPZ2V.js");
|
|
1846
2158
|
const baseUrl = REGIONS[pinnedRegion];
|
|
1847
2159
|
token = await loginAt(baseUrl, email, password);
|
|
1848
2160
|
region = pinnedRegion;
|
|
@@ -1117,23 +1117,24 @@ Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus
|
|
|
1117
1117
|
|
|
1118
1118
|
Practical mapping from user phrasing to criterion:
|
|
1119
1119
|
|
|
1120
|
-
| User phrase | Criterion
|
|
1121
|
-
|
|
1122
|
-
| "leads in Lyon" | \`{type: "location_ids", locations: [<admin_area_id>]}\`
|
|
1123
|
-
| "healthcare staffing" | \`{type: "keywords", keywords: ["healthcare", "staffing"]}\`
|
|
1124
|
-
| "leads I haven't touched in 30 days" | \`{type: "last_action_date", last_days: 30}\`
|
|
1125
|
-
| "leads I liked" | \`{type: "liked"}\`
|
|
1126
|
-
| "leads
|
|
1127
|
-
| "
|
|
1128
|
-
| "Y Combinator companies" | \`{type: "yc"}\` |
|
|
1120
|
+
| User phrase | Criterion |
|
|
1121
|
+
|--------------------------------------|----------------------------------------------------------------------|
|
|
1122
|
+
| "leads in Lyon" | \`{type: "location_ids", locations: [<admin_area_id>]}\` |
|
|
1123
|
+
| "healthcare staffing" | \`{type: "keywords", keywords: ["healthcare", "staffing"]}\` |
|
|
1124
|
+
| "leads I haven't touched in 30 days" | \`{type: "last_action_date", last_days: 30}\` |
|
|
1125
|
+
| "leads I liked" | \`{type: "liked"}\` |
|
|
1126
|
+
| "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
|
|
1127
|
+
| "Y Combinator companies" | \`{type: "yc"}\` |
|
|
1129
1128
|
|
|
1130
1129
|
Geo filtering requires \`admin_area_id\` resolution (the backend doesn't accept free-text city names in \`location_ids\`). The MCP doesn't expose an admin-area lookup yet \u2014 for now, ask the user to pick the geo from the Leadbay app's filter UI, or skip the geo filter and rely on agent post-filtering of the response.
|
|
1131
1130
|
|
|
1132
1131
|
**Pushback exclusion.** Leads with active pushback (\`pushback_status\` set and \`pushback_until > today\`) are excluded from the response. The composite enforces this client-side; \`total_excluded_by_pushback\` in the output reports how many rows were dropped.
|
|
1133
1132
|
|
|
1134
|
-
WHEN TO USE:
|
|
1133
|
+
WHEN TO USE: re-engaging pipeline ("what should I follow up on", "stale leads"), filtering monitored leads by city / sector / recency / action type / liked. The canonical orchestrator is the \`leadbay_followup_check_in\` prompt.
|
|
1135
1134
|
|
|
1136
|
-
WHEN NOT TO USE: for NEW leads
|
|
1135
|
+
WHEN NOT TO USE: for NEW leads \u2014 that's \`leadbay_pull_leads\` (Discover).
|
|
1136
|
+
|
|
1137
|
+
**Anti-confusion guardrail.** If you're iterating \`pull_leads\` pages looking for rows with \`prospecting_actions_count > 0\` or \`notes_count > 0\`, STOP \u2014 wrong entry point. The two read different backend tables; a lead with follow-up history may not appear in \`pull_leads\` (already aged out of the new-leads queue). Call \`pull_followups\` instead.
|
|
1137
1138
|
|
|
1138
1139
|
---
|
|
1139
1140
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|