@leadbay/mcp 0.9.1 → 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 +62 -0
- package/README.md +43 -3
- package/dist/bin.js +385 -53
- package/dist/{chunk-ZRTAIXEJ.js → chunk-F3EWCHME.js} +1599 -244
- package/dist/{dist-X5AOPZ2V.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";
|
|
@@ -37,7 +37,7 @@ Run the Leadbay daily check-in for me. Treat this prompt the same way for any eq
|
|
|
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
|
|
|
@@ -122,7 +122,7 @@ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is nul
|
|
|
122
122
|
**Column 2 \u2014 Why it fits**
|
|
123
123
|
|
|
124
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 \`
|
|
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
126
|
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
127
127
|
- No bullet lists, no line breaks inside the cell.
|
|
128
128
|
|
|
@@ -164,7 +164,7 @@ If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bu
|
|
|
164
164
|
|
|
165
165
|
# PHASE 4 \u2014 DEEP DIVE (every promising lead)
|
|
166
166
|
|
|
167
|
-
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.
|
|
168
168
|
|
|
169
169
|
For each researched lead surface:
|
|
170
170
|
- what makes it promising (1\u20132 sentences citing signals from the research)
|
|
@@ -372,7 +372,7 @@ You are working with Leadbay through the \`leadbay_*\` MCP tools. This prompt or
|
|
|
372
372
|
|
|
373
373
|
# Resilience rules for Leadbay long-running tools
|
|
374
374
|
|
|
375
|
-
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.**
|
|
376
376
|
|
|
377
377
|
## Rule 1 \u2014 Pin the lens
|
|
378
378
|
|
|
@@ -382,9 +382,9 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
382
382
|
|
|
383
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.
|
|
384
384
|
|
|
385
|
-
## Rule 3 \u2014 Serialize \`
|
|
385
|
+
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
386
386
|
|
|
387
|
-
\`
|
|
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.
|
|
388
388
|
|
|
389
389
|
## Rule 4 \u2014 Retry, don't replan
|
|
390
390
|
|
|
@@ -412,8 +412,8 @@ leadbay_pull_leads leadbay_pull_followups
|
|
|
412
412
|
leadbay_daily_check_in leadbay_followup_check_in
|
|
413
413
|
\u2502 \u2502
|
|
414
414
|
\u2502 optional: \u2502 filters by user phrasing:
|
|
415
|
-
\u2502
|
|
416
|
-
\u2502
|
|
415
|
+
\u2502 research_lead_by_name_fuzzy / \u2502 geo, sector, recency,
|
|
416
|
+
\u2502 research_lead_by_id \u2502 liked / pushback / outcome
|
|
417
417
|
\u2502 (deepen profile) \u2502
|
|
418
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
|
|
419
419
|
\u25BC
|
|
@@ -579,7 +579,7 @@ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is nul
|
|
|
579
579
|
**Column 2 \u2014 Why it fits**
|
|
580
580
|
|
|
581
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 \`
|
|
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
583
|
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
584
584
|
- No bullet lists, no line breaks inside the cell.
|
|
585
585
|
|
|
@@ -618,7 +618,7 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
618
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.
|
|
619
619
|
|
|
620
620
|
# PHASE 4 \u2014 RECOMMEND
|
|
621
|
-
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.
|
|
622
622
|
`;
|
|
623
623
|
var leadbay_refine_audience = `
|
|
624
624
|
Refine the Leadbay audience prompt to: {{arg:instruction}}
|
|
@@ -661,17 +661,17 @@ The response carries either a completed result or an async handle. Render a brie
|
|
|
661
661
|
|
|
662
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
663
|
|
|
664
|
-
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`
|
|
664
|
+
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
|
|
665
665
|
|
|
666
666
|
|
|
667
667
|
# PHASE 2 \u2014 DEEP DIVE
|
|
668
|
-
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
669
|
|
|
670
670
|
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
671
671
|
|
|
672
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
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 \`
|
|
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
675
|
|
|
676
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
677
|
|
|
@@ -758,7 +758,7 @@ on "leadbay leads", "best NEW leads", "what's new today", "show me the
|
|
|
758
758
|
day's batch", "let's prospect". Do NOT trigger on follow-up phrasings
|
|
759
759
|
("what should I follow up on", "before my trip") \u2014 those go to
|
|
760
760
|
\`leadbay_followup_check_in\`.
|
|
761
|
-
`, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "
|
|
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
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
763
|
Monitor view that need re-engagement today, ranked by AI urgency,
|
|
764
764
|
with the canonical pull_followups table layout. Trigger when the
|
|
@@ -766,19 +766,19 @@ user asks "follow up", "already known leads", "leads I haven't
|
|
|
766
766
|
contacted", "leads in [city]", "before my trip", "this week",
|
|
767
767
|
"this month", "what's overdue", "who should I re-engage", or
|
|
768
768
|
anything that implies pre-existing pipeline context.
|
|
769
|
-
`, "arguments": [], "expected_calls": ["leadbay_pull_followups", "
|
|
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)'] },
|
|
770
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"] },
|
|
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
|
|
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"] },
|
|
772
772
|
leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
|
|
773
773
|
follow-up, the outreach loop, outcome recording, imports, pushback /
|
|
774
774
|
snooze, and the connected-outreach-tool registry. Trigger when the
|
|
775
775
|
conversation involves Leadbay leads, prospecting, pipeline, follow-up,
|
|
776
776
|
outreach, or lens / ICP \u2014 anything from "show me my leads" to "what
|
|
777
777
|
should I follow up on" to "I'll send via lemlist".
|
|
778
|
-
`, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_pull_followups", "
|
|
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", "
|
|
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"] },
|
|
780
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"] },
|
|
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", "
|
|
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"] }
|
|
782
782
|
};
|
|
783
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.`;
|
|
784
784
|
var PROMPT_CATALOG_BULLETS = {
|
|
@@ -882,7 +882,7 @@ var CATALOG = [
|
|
|
882
882
|
arguments: [
|
|
883
883
|
{
|
|
884
884
|
name: "lead_id",
|
|
885
|
-
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.",
|
|
886
886
|
required: true
|
|
887
887
|
},
|
|
888
888
|
{
|
|
@@ -965,7 +965,7 @@ function listResourceTemplates() {
|
|
|
965
965
|
{
|
|
966
966
|
uriTemplate: "lead://{uuid}/profile",
|
|
967
967
|
name: "Lead profile",
|
|
968
|
-
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.",
|
|
969
969
|
mimeType: "application/json"
|
|
970
970
|
},
|
|
971
971
|
{
|
|
@@ -1016,9 +1016,221 @@ async function readResource(uri, client) {
|
|
|
1016
1016
|
);
|
|
1017
1017
|
}
|
|
1018
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
|
+
|
|
1019
1230
|
// src/server.ts
|
|
1020
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.";
|
|
1021
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.";
|
|
1022
1234
|
function buildScoringParagraph(has) {
|
|
1023
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.";
|
|
1024
1236
|
const deepenTools = [];
|
|
@@ -1030,7 +1242,7 @@ function buildScoringParagraph(has) {
|
|
|
1030
1242
|
return base;
|
|
1031
1243
|
}
|
|
1032
1244
|
function buildStartHereParagraph(has) {
|
|
1033
|
-
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).";
|
|
1034
1246
|
const compositeNames = ["bulk_qualify_leads", "adjust_audience", "refine_prompt", "enrich_titles"].filter((n) => has(`leadbay_${n}`));
|
|
1035
1247
|
if (compositeNames.length > 0) {
|
|
1036
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.`;
|
|
@@ -1113,6 +1325,7 @@ function buildServerInstructions(exposed) {
|
|
|
1113
1325
|
parts.push(VERIFICATION_MANDATE);
|
|
1114
1326
|
}
|
|
1115
1327
|
parts.push(MENTAL_MODEL_PARAGRAPH);
|
|
1328
|
+
parts.push(QUOTA_AND_TOPUP_PARAGRAPH);
|
|
1116
1329
|
parts.push(buildScoringParagraph(has));
|
|
1117
1330
|
parts.push(buildStartHereParagraph(has));
|
|
1118
1331
|
parts.push(buildRhythmParagraph(has));
|
|
@@ -1120,6 +1333,7 @@ function buildServerInstructions(exposed) {
|
|
|
1120
1333
|
if (promptsCatalog) parts.push(promptsCatalog);
|
|
1121
1334
|
parts.push(RESOURCES_PARAGRAPH);
|
|
1122
1335
|
parts.push(buildProtocolPrimitivesParagraph(has));
|
|
1336
|
+
parts.push(BUILTIN_WIDGETS_PARAGRAPH);
|
|
1123
1337
|
return parts.join("\n\n");
|
|
1124
1338
|
}
|
|
1125
1339
|
function formatErrorForLLM(err) {
|
|
@@ -1250,8 +1464,10 @@ function buildServer(client, opts = {}) {
|
|
|
1250
1464
|
});
|
|
1251
1465
|
const DEBUG_RAW = process.env.LEADBAY_DEBUG ?? "";
|
|
1252
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";
|
|
1253
1469
|
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
1254
|
-
const
|
|
1470
|
+
const callStart = Date.now();
|
|
1255
1471
|
const name = req.params.name;
|
|
1256
1472
|
const tool = toolByName.get(name);
|
|
1257
1473
|
if (!tool) {
|
|
@@ -1307,9 +1523,33 @@ function buildServer(client, opts = {}) {
|
|
|
1307
1523
|
elicit
|
|
1308
1524
|
});
|
|
1309
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
|
+
}
|
|
1310
1550
|
return {
|
|
1311
1551
|
content: [
|
|
1312
|
-
{ type: "text", text:
|
|
1552
|
+
{ type: "text", text: envText }
|
|
1313
1553
|
],
|
|
1314
1554
|
isError: true
|
|
1315
1555
|
};
|
|
@@ -1323,11 +1563,21 @@ function buildServer(client, opts = {}) {
|
|
|
1323
1563
|
if (tool.outputSchema && env.structured !== null && typeof env.structured === "object" && !Array.isArray(env.structured)) {
|
|
1324
1564
|
out.structuredContent = env.structured;
|
|
1325
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
|
+
}
|
|
1326
1578
|
if (DEBUG_ON) {
|
|
1327
|
-
const dur = Date.now() - debugStart;
|
|
1328
|
-
const bytes = env.markdown.length;
|
|
1329
1579
|
process.stderr.write(
|
|
1330
|
-
`[leadbay-mcp debug] tool=${name} dur=${
|
|
1580
|
+
`[leadbay-mcp debug] tool=${name} dur=${mdDur}ms ok=true bytes=${mdBytes} format=markdown
|
|
1331
1581
|
`
|
|
1332
1582
|
);
|
|
1333
1583
|
}
|
|
@@ -1341,28 +1591,66 @@ function buildServer(client, opts = {}) {
|
|
|
1341
1591
|
if (tool.outputSchema && result !== null && typeof result === "object" && !Array.isArray(result)) {
|
|
1342
1592
|
response.structuredContent = result;
|
|
1343
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
|
+
}
|
|
1344
1607
|
if (DEBUG_ON) {
|
|
1345
|
-
const dur = Date.now() - debugStart;
|
|
1346
|
-
const text = response.content[0]?.text ?? "";
|
|
1347
|
-
const bytes = typeof text === "string" ? text.length : 0;
|
|
1348
1608
|
process.stderr.write(
|
|
1349
|
-
`[leadbay-mcp debug] tool=${name} dur=${
|
|
1609
|
+
`[leadbay-mcp debug] tool=${name} dur=${okDur}ms ok=true bytes=${okBytes}
|
|
1350
1610
|
`
|
|
1351
1611
|
);
|
|
1352
1612
|
}
|
|
1353
1613
|
return response;
|
|
1354
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
|
+
}
|
|
1355
1645
|
if (DEBUG_ON) {
|
|
1356
|
-
const dur = Date.now() - debugStart;
|
|
1357
|
-
const code = err?.code ?? err?.name ?? "Error";
|
|
1358
1646
|
process.stderr.write(
|
|
1359
|
-
`[leadbay-mcp debug] tool=${name} dur=${
|
|
1647
|
+
`[leadbay-mcp debug] tool=${name} dur=${errDur}ms ok=false code=${code}
|
|
1360
1648
|
`
|
|
1361
1649
|
);
|
|
1362
1650
|
}
|
|
1363
1651
|
return {
|
|
1364
1652
|
content: [
|
|
1365
|
-
{ type: "text", text:
|
|
1653
|
+
{ type: "text", text: errText }
|
|
1366
1654
|
],
|
|
1367
1655
|
isError: true
|
|
1368
1656
|
};
|
|
@@ -1373,7 +1661,7 @@ function buildServer(client, opts = {}) {
|
|
|
1373
1661
|
|
|
1374
1662
|
// src/bin.ts
|
|
1375
1663
|
import { createRequire } from "module";
|
|
1376
|
-
var VERSION = "0.
|
|
1664
|
+
var VERSION = "0.10.0";
|
|
1377
1665
|
var HELP = `
|
|
1378
1666
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
1379
1667
|
|
|
@@ -1409,6 +1697,13 @@ ENV VARS
|
|
|
1409
1697
|
journaled in-process and return {mocked: true, would_call: {...}}.
|
|
1410
1698
|
LEADBAY_MOCK_DIR (optional) Fixture directory. Default: ./.context/leadbay-live-shapes/
|
|
1411
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".
|
|
1412
1707
|
|
|
1413
1708
|
EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json)
|
|
1414
1709
|
{
|
|
@@ -1418,7 +1713,8 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
|
|
|
1418
1713
|
"args": ["-y", "@leadbay/mcp@0.3"],
|
|
1419
1714
|
"env": {
|
|
1420
1715
|
"LEADBAY_TOKEN": "lb_...",
|
|
1421
|
-
"LEADBAY_REGION": "us"
|
|
1716
|
+
"LEADBAY_REGION": "us",
|
|
1717
|
+
"LEADBAY_TELEMETRY_ENABLED": "true"
|
|
1422
1718
|
}
|
|
1423
1719
|
}
|
|
1424
1720
|
}
|
|
@@ -1662,7 +1958,7 @@ async function runLogin(args) {
|
|
|
1662
1958
|
let result;
|
|
1663
1959
|
try {
|
|
1664
1960
|
if (pinnedRegion && !allowFallback) {
|
|
1665
|
-
const { REGIONS } = await import("./dist-
|
|
1961
|
+
const { REGIONS } = await import("./dist-BHLIJAIH.js");
|
|
1666
1962
|
const baseUrl = REGIONS[pinnedRegion];
|
|
1667
1963
|
const c = createClient({ region: pinnedRegion });
|
|
1668
1964
|
const token = await loginAt(baseUrl, email, password);
|
|
@@ -1989,7 +2285,7 @@ async function readChoice(prompt, def = true) {
|
|
|
1989
2285
|
process.stdin.on("data", onData);
|
|
1990
2286
|
});
|
|
1991
2287
|
}
|
|
1992
|
-
function buildClaudeCodeAddArgs(token, region, includeWrite) {
|
|
2288
|
+
function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled) {
|
|
1993
2289
|
const args = [
|
|
1994
2290
|
"mcp",
|
|
1995
2291
|
"add",
|
|
@@ -1999,15 +2295,20 @@ function buildClaudeCodeAddArgs(token, region, includeWrite) {
|
|
|
1999
2295
|
"--env",
|
|
2000
2296
|
`LEADBAY_TOKEN=${token}`,
|
|
2001
2297
|
"--env",
|
|
2002
|
-
`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"}`
|
|
2003
2304
|
];
|
|
2004
2305
|
if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
|
|
2005
2306
|
args.push("--", "npx", "-y", "@leadbay/mcp@0.3");
|
|
2006
2307
|
return args;
|
|
2007
2308
|
}
|
|
2008
|
-
async function installInClaudeCode(token, region, includeWrite) {
|
|
2309
|
+
async function installInClaudeCode(token, region, includeWrite, telemetryEnabled) {
|
|
2009
2310
|
const cp = await import("child_process");
|
|
2010
|
-
const args = buildClaudeCodeAddArgs(token, region, includeWrite);
|
|
2311
|
+
const args = buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled);
|
|
2011
2312
|
return await new Promise((resolve) => {
|
|
2012
2313
|
const child = cp.spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2013
2314
|
let stderr = "";
|
|
@@ -2025,7 +2326,7 @@ async function installInClaudeCode(token, region, includeWrite) {
|
|
|
2025
2326
|
);
|
|
2026
2327
|
});
|
|
2027
2328
|
}
|
|
2028
|
-
async function installInJsonConfig(configPath, token, region, includeWrite) {
|
|
2329
|
+
async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled) {
|
|
2029
2330
|
try {
|
|
2030
2331
|
const { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } = await import("fs");
|
|
2031
2332
|
const { dirname } = await import("path");
|
|
@@ -2045,7 +2346,9 @@ async function installInJsonConfig(configPath, token, region, includeWrite) {
|
|
|
2045
2346
|
parsed.mcpServers = parsed.mcpServers ?? {};
|
|
2046
2347
|
const env = {
|
|
2047
2348
|
LEADBAY_TOKEN: token,
|
|
2048
|
-
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"
|
|
2049
2352
|
};
|
|
2050
2353
|
if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
|
|
2051
2354
|
parsed.mcpServers.leadbay = {
|
|
@@ -2073,7 +2376,7 @@ async function runInstall(args) {
|
|
|
2073
2376
|
const email = parseFlag(args, "email");
|
|
2074
2377
|
if (!email) {
|
|
2075
2378
|
process.stderr.write(
|
|
2076
|
-
"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"
|
|
2077
2380
|
);
|
|
2078
2381
|
return 2;
|
|
2079
2382
|
}
|
|
@@ -2154,7 +2457,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
2154
2457
|
let region;
|
|
2155
2458
|
try {
|
|
2156
2459
|
if (pinnedRegion && !allowFallback) {
|
|
2157
|
-
const { REGIONS } = await import("./dist-
|
|
2460
|
+
const { REGIONS } = await import("./dist-BHLIJAIH.js");
|
|
2158
2461
|
const baseUrl = REGIONS[pinnedRegion];
|
|
2159
2462
|
token = await loginAt(baseUrl, email, password);
|
|
2160
2463
|
region = pinnedRegion;
|
|
@@ -2181,6 +2484,16 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
2181
2484
|
"Composite write tools DISABLED (read-only agent). Re-run without --no-write to enable.\n\n"
|
|
2182
2485
|
);
|
|
2183
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
|
+
}
|
|
2184
2497
|
const skipPrompts = hasFlag(args, "yes");
|
|
2185
2498
|
const results = [];
|
|
2186
2499
|
for (const c of chosen) {
|
|
@@ -2200,10 +2513,10 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
2200
2513
|
}
|
|
2201
2514
|
let res;
|
|
2202
2515
|
if (c.id === "claude-code") {
|
|
2203
|
-
res = await installInClaudeCode(token, region, includeWrite);
|
|
2516
|
+
res = await installInClaudeCode(token, region, includeWrite, telemetryEnabled);
|
|
2204
2517
|
} else {
|
|
2205
2518
|
const path = c.detail.split(" ")[0];
|
|
2206
|
-
res = await installInJsonConfig(path, token, region, includeWrite);
|
|
2519
|
+
res = await installInJsonConfig(path, token, region, includeWrite, telemetryEnabled);
|
|
2207
2520
|
}
|
|
2208
2521
|
results.push({ id: c.id, label: c.label, ...res });
|
|
2209
2522
|
}
|
|
@@ -2290,7 +2603,9 @@ async function main() {
|
|
|
2290
2603
|
process.exit(await runDoctor());
|
|
2291
2604
|
}
|
|
2292
2605
|
const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
|
|
2606
|
+
const telemetry = initTelemetry({ version: VERSION, logger });
|
|
2293
2607
|
const client = await resolveClientFromEnv(logger);
|
|
2608
|
+
telemetry.identify(client);
|
|
2294
2609
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
2295
2610
|
const includeWrite = parseWriteEnv();
|
|
2296
2611
|
const bulkTracker = await createDefaultBulkStore({ logger });
|
|
@@ -2299,13 +2614,24 @@ async function main() {
|
|
|
2299
2614
|
includeWrite,
|
|
2300
2615
|
logger,
|
|
2301
2616
|
bulkTracker,
|
|
2302
|
-
version: VERSION
|
|
2617
|
+
version: VERSION,
|
|
2618
|
+
telemetry
|
|
2303
2619
|
});
|
|
2304
2620
|
const transport = new StdioServerTransport();
|
|
2305
2621
|
logger.info?.(
|
|
2306
2622
|
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability})`
|
|
2307
2623
|
);
|
|
2308
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));
|
|
2309
2635
|
}
|
|
2310
2636
|
var isEntrypoint = (() => {
|
|
2311
2637
|
try {
|
|
@@ -2318,9 +2644,15 @@ var isEntrypoint = (() => {
|
|
|
2318
2644
|
}
|
|
2319
2645
|
})();
|
|
2320
2646
|
if (isEntrypoint) {
|
|
2321
|
-
main().catch((err) => {
|
|
2647
|
+
main().catch(async (err) => {
|
|
2322
2648
|
process.stderr.write(`leadbay-mcp: ${err?.message ?? err}
|
|
2323
2649
|
`);
|
|
2650
|
+
try {
|
|
2651
|
+
const bootTelemetry = initTelemetry({ version: VERSION });
|
|
2652
|
+
bootTelemetry.captureException(err, { tool: "__bootstrap__" });
|
|
2653
|
+
await bootTelemetry.shutdown();
|
|
2654
|
+
} catch {
|
|
2655
|
+
}
|
|
2324
2656
|
process.exit(1);
|
|
2325
2657
|
});
|
|
2326
2658
|
}
|