@leadbay/mcp 0.9.0 → 0.10.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 +81 -0
- package/README.md +43 -3
- package/dist/bin.js +714 -70
- package/dist/{chunk-2EGNZRD7.js → chunk-F3EWCHME.js} +1610 -254
- package/dist/{dist-IIPWWDS5.js → dist-BHLIJAIH.js} +17 -5
- package/package.json +4 -2
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-F3EWCHME.js";
|
|
12
12
|
|
|
13
13
|
// src/bin.ts
|
|
14
14
|
import { realpathSync } from "fs";
|
|
@@ -33,11 +33,11 @@ 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
|
|
|
40
|
-
These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`
|
|
40
|
+
These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
41
41
|
|
|
42
42
|
## Rule 1 \u2014 Pin the lens
|
|
43
43
|
|
|
@@ -47,9 +47,9 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
47
47
|
|
|
48
48
|
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\`, which returns \`{status:'running', qualify_id}\` immediately. Then poll \`leadbay_qualify_status\` (or \`leadbay_import_status\`) every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
49
49
|
|
|
50
|
-
## Rule 3 \u2014 Serialize \`
|
|
50
|
+
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
51
51
|
|
|
52
|
-
\`
|
|
52
|
+
\`leadbay_research_lead_by_id\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
|
|
53
53
|
|
|
54
54
|
## Rule 4 \u2014 Retry, don't replan
|
|
55
55
|
|
|
@@ -72,15 +72,99 @@ 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_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
|
|
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
|
|
|
81
165
|
# PHASE 4 \u2014 DEEP DIVE (every promising lead)
|
|
82
166
|
|
|
83
|
-
Call \`
|
|
167
|
+
Call \`leadbay_research_lead_by_id\` on **every** lead from your top 10 that the user might realistically prospect today (filter out clearly weak fits if any). Don't pick just one. **Call it sequentially** \u2014 one at a time, or batches of at most 3 in parallel. Do not fire 10 in parallel \u2014 it triggers transport backpressure that surfaces as \`"Tool permission stream closed"\` errors (see Rule 3 above). If a call fails, retry that single lead once; if the retry also fails, note the lead id and continue. Report Phase 4 results even if 1\u20132 leads were unresearchable.
|
|
84
168
|
|
|
85
169
|
For each researched lead surface:
|
|
86
170
|
- what makes it promising (1\u20132 sentences citing signals from the research)
|
|
@@ -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.
|
|
@@ -283,7 +372,7 @@ You are working with Leadbay through the \`leadbay_*\` MCP tools. This prompt or
|
|
|
283
372
|
|
|
284
373
|
# Resilience rules for Leadbay long-running tools
|
|
285
374
|
|
|
286
|
-
These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`
|
|
375
|
+
These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
287
376
|
|
|
288
377
|
## Rule 1 \u2014 Pin the lens
|
|
289
378
|
|
|
@@ -293,9 +382,9 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
293
382
|
|
|
294
383
|
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\`, which returns \`{status:'running', qualify_id}\` immediately. Then poll \`leadbay_qualify_status\` (or \`leadbay_import_status\`) every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
295
384
|
|
|
296
|
-
## Rule 3 \u2014 Serialize \`
|
|
385
|
+
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
297
386
|
|
|
298
|
-
\`
|
|
387
|
+
\`leadbay_research_lead_by_id\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
|
|
299
388
|
|
|
300
389
|
## Rule 4 \u2014 Retry, don't replan
|
|
301
390
|
|
|
@@ -315,13 +404,16 @@ 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
|
-
\u2502
|
|
324
|
-
\u2502
|
|
415
|
+
\u2502 research_lead_by_name_fuzzy / \u2502 geo, sector, recency,
|
|
416
|
+
\u2502 research_lead_by_id \u2502 liked / pushback / outcome
|
|
325
417
|
\u2502 (deepen profile) \u2502
|
|
326
418
|
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
327
419
|
\u25BC
|
|
@@ -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,25 +497,128 @@ 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_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
|
|
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
|
-
Recommend the single most-promising lead from this batch and offer to research it deeply with \`
|
|
621
|
+
Recommend the single most-promising lead from this batch and offer to research it deeply with \`leadbay_research_lead_by_id\`. Do not actually call \`research_lead_by_id\` yet \u2014 wait for my go.
|
|
419
622
|
`;
|
|
420
623
|
var leadbay_refine_audience = `
|
|
421
624
|
Refine the Leadbay audience prompt to: {{arg:instruction}}
|
|
@@ -434,46 +637,153 @@ 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_by_id\` in NEXT STEPS.
|
|
665
|
+
|
|
441
666
|
|
|
442
667
|
# PHASE 2 \u2014 DEEP DIVE
|
|
443
|
-
When the import resolves, call \`
|
|
668
|
+
When the import resolves, call \`leadbay_research_lead_by_id\` 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_by_id\`, 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_by_id", "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_by_id 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_by_id 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_by_id", "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
|
-
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
|
|
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_by_id.", "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.
|
|
464
773
|
follow-up, the outreach loop, outcome recording, imports, pushback /
|
|
465
774
|
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", "
|
|
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", "
|
|
778
|
+
`, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_research_lead_by_id", "leadbay_research_lead_by_name_fuzzy", "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_by_id"], "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", "
|
|
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_by_id"], "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_by_id 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".`,
|
|
@@ -572,7 +882,7 @@ var CATALOG = [
|
|
|
572
882
|
arguments: [
|
|
573
883
|
{
|
|
574
884
|
name: "lead_id",
|
|
575
|
-
description: "The lead UUID. Get it from leadbay_pull_leads or
|
|
885
|
+
description: "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.",
|
|
576
886
|
required: true
|
|
577
887
|
},
|
|
578
888
|
{
|
|
@@ -655,7 +965,7 @@ function listResourceTemplates() {
|
|
|
655
965
|
{
|
|
656
966
|
uriTemplate: "lead://{uuid}/profile",
|
|
657
967
|
name: "Lead profile",
|
|
658
|
-
description: "Full profile for a single Leadbay lead by UUID. Read-only. Cached client-side; cheaper than calling
|
|
968
|
+
description: "Full profile for a single Leadbay lead by UUID. Read-only. Cached client-side; cheaper than calling leadbay_research_lead_by_id when you already have the id.",
|
|
659
969
|
mimeType: "application/json"
|
|
660
970
|
},
|
|
661
971
|
{
|
|
@@ -706,9 +1016,221 @@ async function readResource(uri, client) {
|
|
|
706
1016
|
);
|
|
707
1017
|
}
|
|
708
1018
|
|
|
1019
|
+
// src/host-widgets.ts
|
|
1020
|
+
var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown when the data shape fits. Three to know: (1) `places_map_display_v0` \u2014 for \u22652 locations / map / travel intent. Pass `{name, address, latitude, longitude, notes}` per location; the host enriches via Google Places. (2) `message_compose_v1` \u2014 for any outreach draft (email / message / call opener). Pass 2\u20133 strategic variants with goal-oriented labels ("Push for alignment", "Reference M&A signal") \u2014 NOT tone labels. (3) `ask_user_input_v0` \u2014 for the NEXT STEPS questions every Leadbay tool emits. Pass `single_select` with 2\u20134 mutually-exclusive options from the tool\'s NEXT STEPS table. When the host doesn\'t expose the named widget, fall back to the per-tool markdown RENDERING block. The directive is host-conditional; the fallback is automatic.';
|
|
1021
|
+
|
|
1022
|
+
// src/telemetry.ts
|
|
1023
|
+
import { PostHog } from "posthog-node";
|
|
1024
|
+
import * as Sentry from "@sentry/node";
|
|
1025
|
+
|
|
1026
|
+
// src/telemetry-constants.ts
|
|
1027
|
+
var EMBEDDED_POSTHOG_KEY = "phc_N9SnA7OULuAlXReQJZ0Y3rPI4eC0mJLpMRbzgqamhHR";
|
|
1028
|
+
var EMBEDDED_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
1029
|
+
var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o4505874436849664.ingest.us.sentry.io/4511419984248832";
|
|
1030
|
+
|
|
1031
|
+
// src/telemetry-events.ts
|
|
1032
|
+
var EV_TOOL_CALL = "mcp tool called";
|
|
1033
|
+
var EV_QUOTA_HIT = "mcp quota hit";
|
|
1034
|
+
var EV_TOPUP_LINK = "mcp topup link created";
|
|
1035
|
+
|
|
1036
|
+
// src/telemetry.ts
|
|
1037
|
+
var NOOP_TELEMETRY = {
|
|
1038
|
+
identify: async () => {
|
|
1039
|
+
},
|
|
1040
|
+
captureToolCall: () => {
|
|
1041
|
+
},
|
|
1042
|
+
captureQuotaHit: () => {
|
|
1043
|
+
},
|
|
1044
|
+
captureTopupLink: () => {
|
|
1045
|
+
},
|
|
1046
|
+
captureException: () => {
|
|
1047
|
+
},
|
|
1048
|
+
shutdown: async () => {
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
function parseTelemetryEnv(raw) {
|
|
1052
|
+
if (raw === void 0 || raw === "") return true;
|
|
1053
|
+
const v = raw.trim().toLowerCase();
|
|
1054
|
+
if (v === "false" || v === "0" || v === "no" || v === "off") return false;
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
function initTelemetry(opts) {
|
|
1058
|
+
if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
|
|
1059
|
+
if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
|
|
1060
|
+
const posthogKey = process.env.LEADBAY_POSTHOG_KEY ?? EMBEDDED_POSTHOG_KEY;
|
|
1061
|
+
const sentryDsn = process.env.LEADBAY_SENTRY_DSN ?? EMBEDDED_SENTRY_DSN;
|
|
1062
|
+
if (!posthogKey && !sentryDsn) return NOOP_TELEMETRY;
|
|
1063
|
+
const { version, logger } = opts;
|
|
1064
|
+
const environment = process.env.LEADBAY_ENV ?? (version.includes("-dev.") ? "dev" : "production");
|
|
1065
|
+
let posthog = null;
|
|
1066
|
+
let sentryReady = false;
|
|
1067
|
+
let initError = null;
|
|
1068
|
+
try {
|
|
1069
|
+
if (posthogKey) {
|
|
1070
|
+
posthog = new PostHog(posthogKey, {
|
|
1071
|
+
host: process.env.LEADBAY_POSTHOG_HOST ?? EMBEDDED_POSTHOG_HOST,
|
|
1072
|
+
flushAt: 20,
|
|
1073
|
+
flushInterval: 1e4,
|
|
1074
|
+
disableGeoip: false
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
initError = err;
|
|
1079
|
+
posthog = null;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
if (sentryDsn) {
|
|
1083
|
+
Sentry.init({
|
|
1084
|
+
dsn: sentryDsn,
|
|
1085
|
+
release: `@leadbay/mcp@${version}`,
|
|
1086
|
+
environment,
|
|
1087
|
+
tracesSampleRate: 0,
|
|
1088
|
+
profilesSampleRate: 0,
|
|
1089
|
+
defaultIntegrations: false,
|
|
1090
|
+
integrations: [Sentry.httpIntegration()],
|
|
1091
|
+
sendDefaultPii: false
|
|
1092
|
+
});
|
|
1093
|
+
sentryReady = true;
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
initError = initError ?? err;
|
|
1097
|
+
sentryReady = false;
|
|
1098
|
+
}
|
|
1099
|
+
if (initError) {
|
|
1100
|
+
logger?.warn?.(`telemetry init failed: ${initError.message ?? initError}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (!posthog && !sentryReady) return NOOP_TELEMETRY;
|
|
1103
|
+
let me = null;
|
|
1104
|
+
let identityPromise = null;
|
|
1105
|
+
const pendingEvents = [];
|
|
1106
|
+
let region = "unknown";
|
|
1107
|
+
const baseProps = () => ({
|
|
1108
|
+
mcp_version: version,
|
|
1109
|
+
node_version: process.versions.node,
|
|
1110
|
+
platform: process.platform,
|
|
1111
|
+
region
|
|
1112
|
+
});
|
|
1113
|
+
const distinctIdFor = () => {
|
|
1114
|
+
if (me?.email) return me.email;
|
|
1115
|
+
if (me?.id) return `mcp:user-${me.id}`;
|
|
1116
|
+
return "mcp:unknown";
|
|
1117
|
+
};
|
|
1118
|
+
const groupsFor = () => {
|
|
1119
|
+
return me?.organization?.id ? { organization: me.organization.id } : void 0;
|
|
1120
|
+
};
|
|
1121
|
+
const doCapture = (event, properties) => {
|
|
1122
|
+
if (!posthog) return;
|
|
1123
|
+
try {
|
|
1124
|
+
posthog.capture({
|
|
1125
|
+
distinctId: distinctIdFor(),
|
|
1126
|
+
event,
|
|
1127
|
+
properties: { ...baseProps(), ...properties },
|
|
1128
|
+
groups: groupsFor()
|
|
1129
|
+
});
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
logger?.warn?.(`posthog capture failed: ${err?.message ?? err}`);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const emit = (event, properties) => {
|
|
1135
|
+
if (!posthog) return;
|
|
1136
|
+
if (!me) {
|
|
1137
|
+
pendingEvents.push({ event, properties });
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
doCapture(event, properties);
|
|
1141
|
+
};
|
|
1142
|
+
const flushPending = () => {
|
|
1143
|
+
if (!posthog || pendingEvents.length === 0) return;
|
|
1144
|
+
const buf = pendingEvents.splice(0, pendingEvents.length);
|
|
1145
|
+
for (const { event, properties } of buf) {
|
|
1146
|
+
doCapture(event, properties);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
return {
|
|
1150
|
+
identify(client) {
|
|
1151
|
+
if (identityPromise) return identityPromise;
|
|
1152
|
+
region = client.region;
|
|
1153
|
+
identityPromise = (async () => {
|
|
1154
|
+
try {
|
|
1155
|
+
const resolved = await client.resolveMe();
|
|
1156
|
+
me = resolved;
|
|
1157
|
+
if (posthog && resolved.email) {
|
|
1158
|
+
try {
|
|
1159
|
+
posthog.identify({
|
|
1160
|
+
distinctId: resolved.email,
|
|
1161
|
+
properties: {
|
|
1162
|
+
email: resolved.email,
|
|
1163
|
+
leadbay_id: resolved.id,
|
|
1164
|
+
leadbay_name: resolved.name,
|
|
1165
|
+
leadbay_organization: resolved.organization?.name,
|
|
1166
|
+
leadbay_organization_id: resolved.organization?.id
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
logger?.warn?.(`posthog identify failed: ${err?.message ?? err}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (sentryReady) {
|
|
1174
|
+
try {
|
|
1175
|
+
Sentry.setUser({
|
|
1176
|
+
id: resolved.id,
|
|
1177
|
+
email: resolved.email,
|
|
1178
|
+
username: resolved.name
|
|
1179
|
+
});
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
logger?.warn?.(`sentry setUser failed: ${err?.message ?? err}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
flushPending();
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
logger?.warn?.(
|
|
1187
|
+
`telemetry identify failed (${err?.message ?? err}); flushing events anonymously`
|
|
1188
|
+
);
|
|
1189
|
+
me = {
|
|
1190
|
+
id: "unknown",
|
|
1191
|
+
organization: { id: "unknown", name: "unknown" }
|
|
1192
|
+
};
|
|
1193
|
+
flushPending();
|
|
1194
|
+
}
|
|
1195
|
+
})();
|
|
1196
|
+
return identityPromise;
|
|
1197
|
+
},
|
|
1198
|
+
captureToolCall(props) {
|
|
1199
|
+
emit(EV_TOOL_CALL, { ...props });
|
|
1200
|
+
},
|
|
1201
|
+
captureQuotaHit(props) {
|
|
1202
|
+
emit(EV_QUOTA_HIT, { ...props });
|
|
1203
|
+
},
|
|
1204
|
+
captureTopupLink(props) {
|
|
1205
|
+
emit(EV_TOPUP_LINK, { ...props });
|
|
1206
|
+
},
|
|
1207
|
+
captureException(err, ctx) {
|
|
1208
|
+
if (!sentryReady) return;
|
|
1209
|
+
try {
|
|
1210
|
+
Sentry.withScope((scope) => {
|
|
1211
|
+
scope.setTag("tool", ctx.tool);
|
|
1212
|
+
if (me?.organization?.id) {
|
|
1213
|
+
scope.setTag("organization", me.organization.id);
|
|
1214
|
+
}
|
|
1215
|
+
Sentry.captureException(err);
|
|
1216
|
+
});
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
logger?.warn?.(`sentry captureException failed: ${e?.message ?? e}`);
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
async shutdown() {
|
|
1222
|
+
const tasks = [];
|
|
1223
|
+
if (posthog) tasks.push(posthog.shutdown(2e3).catch(() => void 0));
|
|
1224
|
+
if (sentryReady) tasks.push(Sentry.close(2e3).catch(() => void 0));
|
|
1225
|
+
await Promise.allSettled(tasks);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
709
1230
|
// src/server.ts
|
|
710
1231
|
var VERIFICATION_MANDATE = "After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.";
|
|
711
1232
|
var MENTAL_MODEL_PARAGRAPH = "How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.";
|
|
1233
|
+
var QUOTA_AND_TOPUP_PARAGRAPH = "Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.";
|
|
712
1234
|
function buildScoringParagraph(has) {
|
|
713
1235
|
const base = "Two scoring layers: every lead has a basic `score` (firmographic \u2014 already decent, usually correlates with AI). Roughly the top 10 of each batch are also AI-qualified (targeted web research + qualification questions \u2192 `ai_agent_lead_score`, surfaced as `qualification_summary` on leadbay_pull_leads). Leads past the top ~10 are not worse \u2014 the system is saving resources.";
|
|
714
1236
|
const deepenTools = [];
|
|
@@ -720,7 +1242,7 @@ function buildScoringParagraph(has) {
|
|
|
720
1242
|
return base;
|
|
721
1243
|
}
|
|
722
1244
|
function buildStartHereParagraph(has) {
|
|
723
|
-
const base = "Start with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use
|
|
1245
|
+
const base = "Start with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead_by_id to dig into one lead deeply (qualification answers, signals, contacts).";
|
|
724
1246
|
const compositeNames = ["bulk_qualify_leads", "adjust_audience", "refine_prompt", "enrich_titles"].filter((n) => has(`leadbay_${n}`));
|
|
725
1247
|
if (compositeNames.length > 0) {
|
|
726
1248
|
return base + ` When the user wants more leads, narrower audience, refined criteria, or contact enrichment, use the matching composite tool (${compositeNames.join(" / ")}) \u2014 they hide lens permissions, region routing, polling, and selection state from you.`;
|
|
@@ -734,6 +1256,7 @@ function buildRhythmParagraph(has) {
|
|
|
734
1256
|
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
1257
|
}
|
|
736
1258
|
var TOOL_REFERENCE_PATTERN = /\bleadbay_[a-z][a-z0-9_]*\b/g;
|
|
1259
|
+
var PROMPT_NAMES = new Set(Object.keys(PROMPT_CATALOG_BULLETS));
|
|
737
1260
|
function buildPromptsCatalogParagraph(has) {
|
|
738
1261
|
const safeBullets = [];
|
|
739
1262
|
for (const [promptName, bullet] of Object.entries(PROMPT_CATALOG_BULLETS)) {
|
|
@@ -741,6 +1264,7 @@ function buildPromptsCatalogParagraph(has) {
|
|
|
741
1264
|
for (const match of bullet.matchAll(TOOL_REFERENCE_PATTERN)) {
|
|
742
1265
|
const name = match[0];
|
|
743
1266
|
if (name === promptName) continue;
|
|
1267
|
+
if (PROMPT_NAMES.has(name)) continue;
|
|
744
1268
|
referencedTools.add(name);
|
|
745
1269
|
}
|
|
746
1270
|
const allExposed = [...referencedTools].every((n) => has(n));
|
|
@@ -801,6 +1325,7 @@ function buildServerInstructions(exposed) {
|
|
|
801
1325
|
parts.push(VERIFICATION_MANDATE);
|
|
802
1326
|
}
|
|
803
1327
|
parts.push(MENTAL_MODEL_PARAGRAPH);
|
|
1328
|
+
parts.push(QUOTA_AND_TOPUP_PARAGRAPH);
|
|
804
1329
|
parts.push(buildScoringParagraph(has));
|
|
805
1330
|
parts.push(buildStartHereParagraph(has));
|
|
806
1331
|
parts.push(buildRhythmParagraph(has));
|
|
@@ -808,6 +1333,7 @@ function buildServerInstructions(exposed) {
|
|
|
808
1333
|
if (promptsCatalog) parts.push(promptsCatalog);
|
|
809
1334
|
parts.push(RESOURCES_PARAGRAPH);
|
|
810
1335
|
parts.push(buildProtocolPrimitivesParagraph(has));
|
|
1336
|
+
parts.push(BUILTIN_WIDGETS_PARAGRAPH);
|
|
811
1337
|
return parts.join("\n\n");
|
|
812
1338
|
}
|
|
813
1339
|
function formatErrorForLLM(err) {
|
|
@@ -938,8 +1464,10 @@ function buildServer(client, opts = {}) {
|
|
|
938
1464
|
});
|
|
939
1465
|
const DEBUG_RAW = process.env.LEADBAY_DEBUG ?? "";
|
|
940
1466
|
const DEBUG_ON = DEBUG_RAW === "1" || DEBUG_RAW.toLowerCase() === "true";
|
|
1467
|
+
const telemetry = opts.telemetry ?? NOOP_TELEMETRY;
|
|
1468
|
+
const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
|
|
941
1469
|
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
942
|
-
const
|
|
1470
|
+
const callStart = Date.now();
|
|
943
1471
|
const name = req.params.name;
|
|
944
1472
|
const tool = toolByName.get(name);
|
|
945
1473
|
if (!tool) {
|
|
@@ -995,9 +1523,33 @@ function buildServer(client, opts = {}) {
|
|
|
995
1523
|
elicit
|
|
996
1524
|
});
|
|
997
1525
|
if (result && typeof result === "object" && result.error === true) {
|
|
1526
|
+
const envText = formatErrorForLLM(result);
|
|
1527
|
+
const envDur = Date.now() - callStart;
|
|
1528
|
+
const envCode = result.code ?? "Error";
|
|
1529
|
+
if (envCode === "QUOTA_EXCEEDED") {
|
|
1530
|
+
telemetry.captureQuotaHit({
|
|
1531
|
+
tool: name,
|
|
1532
|
+
retry_after_s: result._meta?.retry_after,
|
|
1533
|
+
endpoint: result._meta?.endpoint
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
telemetry.captureToolCall({
|
|
1537
|
+
tool: name,
|
|
1538
|
+
ok: false,
|
|
1539
|
+
duration_ms: envDur,
|
|
1540
|
+
format: "error-envelope",
|
|
1541
|
+
bytes: envText.length,
|
|
1542
|
+
error_code: envCode
|
|
1543
|
+
});
|
|
1544
|
+
if (DEBUG_ON) {
|
|
1545
|
+
process.stderr.write(
|
|
1546
|
+
`[leadbay-mcp debug] tool=${name} dur=${envDur}ms ok=false code=${envCode}
|
|
1547
|
+
`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
998
1550
|
return {
|
|
999
1551
|
content: [
|
|
1000
|
-
{ type: "text", text:
|
|
1552
|
+
{ type: "text", text: envText }
|
|
1001
1553
|
],
|
|
1002
1554
|
isError: true
|
|
1003
1555
|
};
|
|
@@ -1011,11 +1563,21 @@ function buildServer(client, opts = {}) {
|
|
|
1011
1563
|
if (tool.outputSchema && env.structured !== null && typeof env.structured === "object" && !Array.isArray(env.structured)) {
|
|
1012
1564
|
out.structuredContent = env.structured;
|
|
1013
1565
|
}
|
|
1566
|
+
const mdDur = Date.now() - callStart;
|
|
1567
|
+
const mdBytes = env.markdown.length;
|
|
1568
|
+
telemetry.captureToolCall({
|
|
1569
|
+
tool: name,
|
|
1570
|
+
ok: true,
|
|
1571
|
+
duration_ms: mdDur,
|
|
1572
|
+
format: "markdown",
|
|
1573
|
+
bytes: mdBytes
|
|
1574
|
+
});
|
|
1575
|
+
if (name === "leadbay_create_topup_link" && typeof env.structured?.url === "string") {
|
|
1576
|
+
telemetry.captureTopupLink({ tool: name });
|
|
1577
|
+
}
|
|
1014
1578
|
if (DEBUG_ON) {
|
|
1015
|
-
const dur = Date.now() - debugStart;
|
|
1016
|
-
const bytes = env.markdown.length;
|
|
1017
1579
|
process.stderr.write(
|
|
1018
|
-
`[leadbay-mcp debug] tool=${name} dur=${
|
|
1580
|
+
`[leadbay-mcp debug] tool=${name} dur=${mdDur}ms ok=true bytes=${mdBytes} format=markdown
|
|
1019
1581
|
`
|
|
1020
1582
|
);
|
|
1021
1583
|
}
|
|
@@ -1029,28 +1591,66 @@ function buildServer(client, opts = {}) {
|
|
|
1029
1591
|
if (tool.outputSchema && result !== null && typeof result === "object" && !Array.isArray(result)) {
|
|
1030
1592
|
response.structuredContent = result;
|
|
1031
1593
|
}
|
|
1594
|
+
const okText = response.content[0]?.text ?? "";
|
|
1595
|
+
const okBytes = typeof okText === "string" ? okText.length : 0;
|
|
1596
|
+
const okDur = Date.now() - callStart;
|
|
1597
|
+
telemetry.captureToolCall({
|
|
1598
|
+
tool: name,
|
|
1599
|
+
ok: true,
|
|
1600
|
+
duration_ms: okDur,
|
|
1601
|
+
format: "json",
|
|
1602
|
+
bytes: okBytes
|
|
1603
|
+
});
|
|
1604
|
+
if (name === "leadbay_create_topup_link" && typeof result?.url === "string") {
|
|
1605
|
+
telemetry.captureTopupLink({ tool: name });
|
|
1606
|
+
}
|
|
1032
1607
|
if (DEBUG_ON) {
|
|
1033
|
-
const dur = Date.now() - debugStart;
|
|
1034
|
-
const text = response.content[0]?.text ?? "";
|
|
1035
|
-
const bytes = typeof text === "string" ? text.length : 0;
|
|
1036
1608
|
process.stderr.write(
|
|
1037
|
-
`[leadbay-mcp debug] tool=${name} dur=${
|
|
1609
|
+
`[leadbay-mcp debug] tool=${name} dur=${okDur}ms ok=true bytes=${okBytes}
|
|
1038
1610
|
`
|
|
1039
1611
|
);
|
|
1040
1612
|
}
|
|
1041
1613
|
return response;
|
|
1042
1614
|
} catch (err) {
|
|
1615
|
+
const errDur = Date.now() - callStart;
|
|
1616
|
+
const errText = formatErrorForLLM(err);
|
|
1617
|
+
const code = err?.code ?? err?.name ?? "Error";
|
|
1618
|
+
if (isLeadbayBusinessError(err)) {
|
|
1619
|
+
if (err.code === "QUOTA_EXCEEDED") {
|
|
1620
|
+
telemetry.captureQuotaHit({
|
|
1621
|
+
tool: name,
|
|
1622
|
+
retry_after_s: err._meta?.retry_after,
|
|
1623
|
+
endpoint: err._meta?.endpoint
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
telemetry.captureToolCall({
|
|
1627
|
+
tool: name,
|
|
1628
|
+
ok: false,
|
|
1629
|
+
duration_ms: errDur,
|
|
1630
|
+
format: "error-envelope",
|
|
1631
|
+
bytes: errText.length,
|
|
1632
|
+
error_code: code
|
|
1633
|
+
});
|
|
1634
|
+
} else {
|
|
1635
|
+
telemetry.captureException(err, { tool: name });
|
|
1636
|
+
telemetry.captureToolCall({
|
|
1637
|
+
tool: name,
|
|
1638
|
+
ok: false,
|
|
1639
|
+
duration_ms: errDur,
|
|
1640
|
+
format: "error-envelope",
|
|
1641
|
+
bytes: errText.length,
|
|
1642
|
+
error_code: code
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1043
1645
|
if (DEBUG_ON) {
|
|
1044
|
-
const dur = Date.now() - debugStart;
|
|
1045
|
-
const code = err?.code ?? err?.name ?? "Error";
|
|
1046
1646
|
process.stderr.write(
|
|
1047
|
-
`[leadbay-mcp debug] tool=${name} dur=${
|
|
1647
|
+
`[leadbay-mcp debug] tool=${name} dur=${errDur}ms ok=false code=${code}
|
|
1048
1648
|
`
|
|
1049
1649
|
);
|
|
1050
1650
|
}
|
|
1051
1651
|
return {
|
|
1052
1652
|
content: [
|
|
1053
|
-
{ type: "text", text:
|
|
1653
|
+
{ type: "text", text: errText }
|
|
1054
1654
|
],
|
|
1055
1655
|
isError: true
|
|
1056
1656
|
};
|
|
@@ -1061,7 +1661,7 @@ function buildServer(client, opts = {}) {
|
|
|
1061
1661
|
|
|
1062
1662
|
// src/bin.ts
|
|
1063
1663
|
import { createRequire } from "module";
|
|
1064
|
-
var VERSION = "0.
|
|
1664
|
+
var VERSION = "0.10.0";
|
|
1065
1665
|
var HELP = `
|
|
1066
1666
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
1067
1667
|
|
|
@@ -1097,6 +1697,13 @@ ENV VARS
|
|
|
1097
1697
|
journaled in-process and return {mocked: true, would_call: {...}}.
|
|
1098
1698
|
LEADBAY_MOCK_DIR (optional) Fixture directory. Default: ./.context/leadbay-live-shapes/
|
|
1099
1699
|
LEADBAY_LOG_LEVEL (optional) "debug" | "info" | "error" (default "error"). Logs to stderr.
|
|
1700
|
+
LEADBAY_TELEMETRY_ENABLED (optional) Default "true". Sends product usage events
|
|
1701
|
+
(tool name, duration, ok flag, error code) to PostHog and
|
|
1702
|
+
unexpected errors to Sentry, helping Leadbay improve the MCP.
|
|
1703
|
+
Events are tied to your Leadbay account email (so MCP usage
|
|
1704
|
+
consolidates with web-app usage in our analytics). Tool
|
|
1705
|
+
arguments, response bodies, and lead PII are NEVER captured.
|
|
1706
|
+
Set to "false" to opt out. See README "Privacy & telemetry".
|
|
1100
1707
|
|
|
1101
1708
|
EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json)
|
|
1102
1709
|
{
|
|
@@ -1106,7 +1713,8 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
|
|
|
1106
1713
|
"args": ["-y", "@leadbay/mcp@0.3"],
|
|
1107
1714
|
"env": {
|
|
1108
1715
|
"LEADBAY_TOKEN": "lb_...",
|
|
1109
|
-
"LEADBAY_REGION": "us"
|
|
1716
|
+
"LEADBAY_REGION": "us",
|
|
1717
|
+
"LEADBAY_TELEMETRY_ENABLED": "true"
|
|
1110
1718
|
}
|
|
1111
1719
|
}
|
|
1112
1720
|
}
|
|
@@ -1350,7 +1958,7 @@ async function runLogin(args) {
|
|
|
1350
1958
|
let result;
|
|
1351
1959
|
try {
|
|
1352
1960
|
if (pinnedRegion && !allowFallback) {
|
|
1353
|
-
const { REGIONS } = await import("./dist-
|
|
1961
|
+
const { REGIONS } = await import("./dist-BHLIJAIH.js");
|
|
1354
1962
|
const baseUrl = REGIONS[pinnedRegion];
|
|
1355
1963
|
const c = createClient({ region: pinnedRegion });
|
|
1356
1964
|
const token = await loginAt(baseUrl, email, password);
|
|
@@ -1677,7 +2285,7 @@ async function readChoice(prompt, def = true) {
|
|
|
1677
2285
|
process.stdin.on("data", onData);
|
|
1678
2286
|
});
|
|
1679
2287
|
}
|
|
1680
|
-
function buildClaudeCodeAddArgs(token, region, includeWrite) {
|
|
2288
|
+
function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled) {
|
|
1681
2289
|
const args = [
|
|
1682
2290
|
"mcp",
|
|
1683
2291
|
"add",
|
|
@@ -1687,15 +2295,20 @@ function buildClaudeCodeAddArgs(token, region, includeWrite) {
|
|
|
1687
2295
|
"--env",
|
|
1688
2296
|
`LEADBAY_TOKEN=${token}`,
|
|
1689
2297
|
"--env",
|
|
1690
|
-
`LEADBAY_REGION=${region}
|
|
2298
|
+
`LEADBAY_REGION=${region}`,
|
|
2299
|
+
// Always written explicitly (not just when opting out) so MCP-client
|
|
2300
|
+
// config UIs can render it as a toggle the user can flip without
|
|
2301
|
+
// editing the file by hand.
|
|
2302
|
+
"--env",
|
|
2303
|
+
`LEADBAY_TELEMETRY_ENABLED=${telemetryEnabled ? "true" : "false"}`
|
|
1691
2304
|
];
|
|
1692
2305
|
if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
|
|
1693
2306
|
args.push("--", "npx", "-y", "@leadbay/mcp@0.3");
|
|
1694
2307
|
return args;
|
|
1695
2308
|
}
|
|
1696
|
-
async function installInClaudeCode(token, region, includeWrite) {
|
|
2309
|
+
async function installInClaudeCode(token, region, includeWrite, telemetryEnabled) {
|
|
1697
2310
|
const cp = await import("child_process");
|
|
1698
|
-
const args = buildClaudeCodeAddArgs(token, region, includeWrite);
|
|
2311
|
+
const args = buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled);
|
|
1699
2312
|
return await new Promise((resolve) => {
|
|
1700
2313
|
const child = cp.spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1701
2314
|
let stderr = "";
|
|
@@ -1713,7 +2326,7 @@ async function installInClaudeCode(token, region, includeWrite) {
|
|
|
1713
2326
|
);
|
|
1714
2327
|
});
|
|
1715
2328
|
}
|
|
1716
|
-
async function installInJsonConfig(configPath, token, region, includeWrite) {
|
|
2329
|
+
async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled) {
|
|
1717
2330
|
try {
|
|
1718
2331
|
const { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } = await import("fs");
|
|
1719
2332
|
const { dirname } = await import("path");
|
|
@@ -1733,7 +2346,9 @@ async function installInJsonConfig(configPath, token, region, includeWrite) {
|
|
|
1733
2346
|
parsed.mcpServers = parsed.mcpServers ?? {};
|
|
1734
2347
|
const env = {
|
|
1735
2348
|
LEADBAY_TOKEN: token,
|
|
1736
|
-
LEADBAY_REGION: region
|
|
2349
|
+
LEADBAY_REGION: region,
|
|
2350
|
+
// Always written so MCP-client config UIs can render it as a toggle.
|
|
2351
|
+
LEADBAY_TELEMETRY_ENABLED: telemetryEnabled ? "true" : "false"
|
|
1737
2352
|
};
|
|
1738
2353
|
if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
|
|
1739
2354
|
parsed.mcpServers.leadbay = {
|
|
@@ -1761,7 +2376,7 @@ async function runInstall(args) {
|
|
|
1761
2376
|
const email = parseFlag(args, "email");
|
|
1762
2377
|
if (!email) {
|
|
1763
2378
|
process.stderr.write(
|
|
1764
|
-
"Usage: leadbay-mcp install --email you@example.com [--region us|fr]\n [--allow-region-fallback] [--no-write]\n [--target claude-code,claude-desktop,cursor]\n [--yes] [--force-legacy]\n Mints a token AND registers the MCP server with your installed clients (at user scope).\n --target Comma-separated subset; default = all detected.\n --no-write Disable composite write tools (refine_prompt, report_outreach,\n adjust_audience, etc.). They are ON by default since 0.3.0;\n pass --no-write for read-only agents.\n --include-write (deprecated since 0.3.0; now a no-op \u2014 writes are on by default).\n --yes Don't ask before installing into each detected client.\n --force-legacy Write to claude_desktop_config.json even when Claude Desktop 2026\n DXT is detected. Not recommended \u2014 the app overwrites that file.\n Use the .dxt bundle instead: https://github.com/leadbay/leadclaw/releases\n"
|
|
2379
|
+
"Usage: leadbay-mcp install --email you@example.com [--region us|fr]\n [--allow-region-fallback] [--no-write] [--no-telemetry]\n [--target claude-code,claude-desktop,cursor]\n [--yes] [--force-legacy]\n Mints a token AND registers the MCP server with your installed clients (at user scope).\n --target Comma-separated subset; default = all detected.\n --no-write Disable composite write tools (refine_prompt, report_outreach,\n adjust_audience, etc.). They are ON by default since 0.3.0;\n pass --no-write for read-only agents.\n --no-telemetry Opt out of product usage events (PostHog + Sentry).\n Defaults to ON: helps Leadbay improve the MCP. Events are\n tied to your Leadbay email; tool args, response bodies,\n and lead PII are NEVER captured.\n --include-write (deprecated since 0.3.0; now a no-op \u2014 writes are on by default).\n --yes Don't ask before installing into each detected client.\n --force-legacy Write to claude_desktop_config.json even when Claude Desktop 2026\n DXT is detected. Not recommended \u2014 the app overwrites that file.\n Use the .dxt bundle instead: https://github.com/leadbay/leadclaw/releases\n"
|
|
1765
2380
|
);
|
|
1766
2381
|
return 2;
|
|
1767
2382
|
}
|
|
@@ -1842,7 +2457,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
1842
2457
|
let region;
|
|
1843
2458
|
try {
|
|
1844
2459
|
if (pinnedRegion && !allowFallback) {
|
|
1845
|
-
const { REGIONS } = await import("./dist-
|
|
2460
|
+
const { REGIONS } = await import("./dist-BHLIJAIH.js");
|
|
1846
2461
|
const baseUrl = REGIONS[pinnedRegion];
|
|
1847
2462
|
token = await loginAt(baseUrl, email, password);
|
|
1848
2463
|
region = pinnedRegion;
|
|
@@ -1869,6 +2484,16 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
1869
2484
|
"Composite write tools DISABLED (read-only agent). Re-run without --no-write to enable.\n\n"
|
|
1870
2485
|
);
|
|
1871
2486
|
}
|
|
2487
|
+
const telemetryEnabled = !hasFlag(args, "no-telemetry");
|
|
2488
|
+
if (telemetryEnabled) {
|
|
2489
|
+
process.stderr.write(
|
|
2490
|
+
"Product usage events ENABLED \u2014 helps Leadbay improve the MCP. We capture\n per-tool-call metrics (name, duration, ok/error code) and unexpected exceptions.\n Events are tied to your Leadbay email (so MCP usage consolidates with web-app\n usage in our analytics) \u2014 they are NOT anonymous. Tool arguments, response\n bodies, and lead PII are NEVER sent. Flip the toggle\n LEADBAY_TELEMETRY_ENABLED=false in your client's env block to opt out anytime.\n\n"
|
|
2491
|
+
);
|
|
2492
|
+
} else {
|
|
2493
|
+
process.stderr.write(
|
|
2494
|
+
"Product usage events DISABLED. Re-run without --no-telemetry to enable.\n\n"
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
1872
2497
|
const skipPrompts = hasFlag(args, "yes");
|
|
1873
2498
|
const results = [];
|
|
1874
2499
|
for (const c of chosen) {
|
|
@@ -1888,10 +2513,10 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
1888
2513
|
}
|
|
1889
2514
|
let res;
|
|
1890
2515
|
if (c.id === "claude-code") {
|
|
1891
|
-
res = await installInClaudeCode(token, region, includeWrite);
|
|
2516
|
+
res = await installInClaudeCode(token, region, includeWrite, telemetryEnabled);
|
|
1892
2517
|
} else {
|
|
1893
2518
|
const path = c.detail.split(" ")[0];
|
|
1894
|
-
res = await installInJsonConfig(path, token, region, includeWrite);
|
|
2519
|
+
res = await installInJsonConfig(path, token, region, includeWrite, telemetryEnabled);
|
|
1895
2520
|
}
|
|
1896
2521
|
results.push({ id: c.id, label: c.label, ...res });
|
|
1897
2522
|
}
|
|
@@ -1978,7 +2603,9 @@ async function main() {
|
|
|
1978
2603
|
process.exit(await runDoctor());
|
|
1979
2604
|
}
|
|
1980
2605
|
const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
|
|
2606
|
+
const telemetry = initTelemetry({ version: VERSION, logger });
|
|
1981
2607
|
const client = await resolveClientFromEnv(logger);
|
|
2608
|
+
telemetry.identify(client);
|
|
1982
2609
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
1983
2610
|
const includeWrite = parseWriteEnv();
|
|
1984
2611
|
const bulkTracker = await createDefaultBulkStore({ logger });
|
|
@@ -1987,13 +2614,24 @@ async function main() {
|
|
|
1987
2614
|
includeWrite,
|
|
1988
2615
|
logger,
|
|
1989
2616
|
bulkTracker,
|
|
1990
|
-
version: VERSION
|
|
2617
|
+
version: VERSION,
|
|
2618
|
+
telemetry
|
|
1991
2619
|
});
|
|
1992
2620
|
const transport = new StdioServerTransport();
|
|
1993
2621
|
logger.info?.(
|
|
1994
2622
|
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability})`
|
|
1995
2623
|
);
|
|
1996
2624
|
await server.connect(transport);
|
|
2625
|
+
const shutdown = async (code) => {
|
|
2626
|
+
try {
|
|
2627
|
+
await telemetry.shutdown();
|
|
2628
|
+
} finally {
|
|
2629
|
+
process.exit(code);
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
process.once("SIGINT", () => void shutdown(130));
|
|
2633
|
+
process.once("SIGTERM", () => void shutdown(143));
|
|
2634
|
+
process.stdin.once("end", () => void shutdown(0));
|
|
1997
2635
|
}
|
|
1998
2636
|
var isEntrypoint = (() => {
|
|
1999
2637
|
try {
|
|
@@ -2006,9 +2644,15 @@ var isEntrypoint = (() => {
|
|
|
2006
2644
|
}
|
|
2007
2645
|
})();
|
|
2008
2646
|
if (isEntrypoint) {
|
|
2009
|
-
main().catch((err) => {
|
|
2647
|
+
main().catch(async (err) => {
|
|
2010
2648
|
process.stderr.write(`leadbay-mcp: ${err?.message ?? err}
|
|
2011
2649
|
`);
|
|
2650
|
+
try {
|
|
2651
|
+
const bootTelemetry = initTelemetry({ version: VERSION });
|
|
2652
|
+
bootTelemetry.captureException(err, { tool: "__bootstrap__" });
|
|
2653
|
+
await bootTelemetry.shutdown();
|
|
2654
|
+
} catch {
|
|
2655
|
+
}
|
|
2012
2656
|
process.exit(1);
|
|
2013
2657
|
});
|
|
2014
2658
|
}
|