@leadbay/mcp 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +19 -53
- package/dist/bin.js +177 -98
- package/dist/http-server.js +569 -104
- package/dist/installer-electron.js +2 -2
- package/dist/installer-gui.js +2 -2
- package/package.json +1 -1
package/dist/http-server.js
CHANGED
|
@@ -408,6 +408,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
|
|
|
408
408
|
| Observation | Suggest | Calls |
|
|
409
409
|
|------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
|
|
410
410
|
| \u2265 5 leads returned (any batch) | "Build an interactive lead triage board for this batch" | emit antArtifact from data in hand (do NOT re-call leadbay_pull_leads) |
|
|
411
|
+
| \u2265 1 lead returned (any batch) | "Enrich top leads" (reveal decision-maker email/phone on the top leads) | leadbay_enrich_titles({ leadIds: shown leads[].id, lensId }) \u2014 scope to the leads JUST shown; OMIT \`titles\` so it runs the no-spend discovery preview. Confirm titles + channels, then re-call with titles + confirm to launch |
|
|
411
412
|
| \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
|
|
412
413
|
| \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
|
|
413
414
|
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
|
|
@@ -1038,30 +1039,18 @@ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ship
|
|
|
1038
1039
|
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.
|
|
1039
1040
|
|
|
1040
1041
|
|
|
1041
|
-
Research the company
|
|
1042
|
+
Research the company name or domain '{{arg:domain}}' for me using Leadbay.
|
|
1042
1043
|
|
|
1043
|
-
# PHASE 1 \u2014
|
|
1044
|
-
Call \`
|
|
1044
|
+
# PHASE 1 \u2014 RESOLVE + DEEP DIVE
|
|
1045
|
+
Call \`leadbay_research_lead_by_name_fuzzy\` with
|
|
1046
|
+
\`companyName:'{{arg:domain}}'\`. Omit \`lensId\`: the default search deliberately
|
|
1047
|
+
covers the user's visible Discover, Monitor, and Activate corpus, including
|
|
1048
|
+
other lenses and leads outside the active lens's first page. The composite
|
|
1049
|
+
resolves the lead and returns the full deep-research payload in one call.
|
|
1045
1050
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
**Header \u2014 single line, choose by status:**
|
|
1051
|
-
|
|
1052
|
-
- Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
|
|
1053
|
-
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
1054
|
-
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
1055
|
-
|
|
1056
|
-
**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*"\`.
|
|
1057
|
-
|
|
1058
|
-
**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.
|
|
1059
|
-
|
|
1060
|
-
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
# PHASE 2 \u2014 DEEP DIVE
|
|
1064
|
-
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:
|
|
1051
|
+
Render the result using the canonical single-record card layout \u2014 detect MODE A
|
|
1052
|
+
(Discovery) since the user asked to research a company rather than prepare
|
|
1053
|
+
outreach:
|
|
1065
1054
|
|
|
1066
1055
|
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
1067
1056
|
|
|
@@ -1133,10 +1122,16 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
1133
1122
|
|
|
1134
1123
|
|
|
1135
1124
|
|
|
1125
|
+
# PHASE 2 \u2014 NOT FOUND
|
|
1126
|
+
If the resolver returns \`LEAD_NOT_FOUND\`, say that the existing visible corpus
|
|
1127
|
+
was searched. **Do NOT call \`leadbay_import_and_qualify\` automatically.** Offer
|
|
1128
|
+
to import and qualify the company as a separate, explicit next step; only call
|
|
1129
|
+
it after the user agrees.
|
|
1130
|
+
|
|
1136
1131
|
# PHASE 3 \u2014 SUMMARY
|
|
1137
1132
|
Place a 2\u20133 sentence summary ABOVE the card with:
|
|
1138
1133
|
- Who is this company (1 sentence)
|
|
1139
|
-
- Their fit (cite specific
|
|
1134
|
+
- Their fit (cite specific qualification answers or signals from the research response)
|
|
1140
1135
|
- Which contact would I email first (one short clause \u2014 the card's contacts table carries the rest)
|
|
1141
1136
|
|
|
1142
1137
|
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.
|
|
@@ -1312,7 +1307,7 @@ should I follow up on" to "I'll send via lemlist".
|
|
|
1312
1307
|
`, "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)"] },
|
|
1313
1308
|
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"] },
|
|
1314
1309
|
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"] },
|
|
1315
|
-
leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "
|
|
1310
|
+
leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Resolve a company by name or domain across the user's visible Discover,\nMonitor, and Activate corpus, then return everything Leadbay knows about it.\n", "arguments": [{ "name": "domain", "description": "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.", "required": true }], "expected_calls": ["leadbay_research_lead_by_name_fuzzy"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Calls leadbay_import_and_qualify before searching the existing visible corpus", "Treats the active lens as the entire search universe when the user did not request a lens scope", "Imports a missing company without the user's explicit permission", "Renders the research 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)"] },
|
|
1316
1311
|
leadbay_setup_team_prospecting: { "name": "leadbay_setup_team_prospecting", "short_description": "Manager-led prospecting setup: conversationally turn a natural-language\naudience ask into a Leadbay lens, validate the candidate leads, and\npersist them as one or more named campaigns the rep(s) can work\nthrough. Closes #3630 US3 end-to-end (within the current\ncreator-scoped campaign visibility model).\n", "arguments": [{ "name": "audience", "description": "Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime'). The lens-creation step (`leadbay_refine_prompt` \u2192 `leadbay_create_lens`) interprets it.", "required": true }, { "name": "rep_split", "description": "Optional: how to split the validated leads into per-rep campaigns. Free text \u2014 e.g. 'split by city' or 'one campaign per rep: John gets Tulsa, Sarah gets OKC'.", "required": false }], "expected_calls": ["leadbay_refine_prompt", "leadbay_create_lens", "leadbay_promote_lens", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_create_campaign", "leadbay_add_leads_to_campaign"], "failure_modes": ["Skips the validation step \u2014 creates a campaign of unvetted leads from a freshly-created lens without giving the manager a chance to drop weak fits", "Creates ONE campaign for all reps without asking about the split \u2014 the user explicitly mentioned per-rep distribution and the prompt should honor it", "Pretends the backend supports cross-user assignment \u2014 campaigns are owned by the caller (creator-scoped). Surface this honestly instead of fabricating an assignment model", "Asks ALL clarifying questions inline before tool calls \u2014 instead, run the lens refinement loop with `leadbay_refine_prompt` which handles the clarification protocol natively"] },
|
|
1317
1312
|
leadbay_work_campaign: { "name": "leadbay_work_campaign", "short_description": "Work a campaign as a real outreach session: pick the campaign,\nassess what the user has (phones / emails / coords), then PROPOSE\nthe right session mode (call sheet, email sheet, enrich titles\nfirst, map). After they pick, render \u2014 and as they dictate\noutcomes per lead, record both note + epilogue via\n`leadbay_report_outreach` in one round trip.\n", "arguments": [{ "name": "campaign", "description": "Campaign name (fuzzy match against your own campaigns) or campaign UUID. Omit to list and pick interactively.", "required": false }, { "name": "mode", "description": "Optional: skip the readiness-assessment proposal and jump directly into 'call_sheet' / 'email_sheet' / 'map' / 'enrich_first'. Omit (recommended) and let the prompt propose based on the data.", "required": false }], "expected_calls": ["leadbay_list_campaigns", "leadbay_campaign_call_sheet", "leadbay_enrich_titles", "leadbay_report_outreach"], "failure_modes": ["Renders the call sheet immediately without proposing the right mode \u2014 if 60% of leads have no contacts, calling is futile; enrich first. Always assess `readiness` first.", "Auto-renders the map widget without asking \u2014 maps are intrusive when the user just wants to scroll a list. Map mode is a proposed option, not a default.", "Proposes map mode after the user has previously said they don't like maps \u2014 check conversation memory before adding 'View on a map' to the options list.", "Calls `leadbay_campaign_progression` instead of `leadbay_campaign_call_sheet` \u2014 progression has counts but no phones / LinkedIn / call-ready data; the user can't actually dial from progression rows.", "Renders contacts WITHOUT making the phone number a `[bare](tel:URL)` link \u2014 on mobile that breaks one-tap calling, which is the whole point of the cheat sheet.", "Records outreach WITHOUT epilogue_status \u2014 leaves the lead's pipeline state unchanged; the rep then sees the same lead surfaced again next session.", "Records outreach WITHOUT verification \u2014 verification.source/ref is REQUIRED. For calls, pass `{source: 'user_confirmed', ref: <user's exact words>}`.", "Loops through ALL leads in a 50-lead campaign before recording any outreach \u2014 the call-then-record loop must be per-lead, not batched."] }
|
|
1318
1313
|
};
|
|
@@ -1328,7 +1323,7 @@ var PROMPT_CATALOG_BULLETS = {
|
|
|
1328
1323
|
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".`,
|
|
1329
1324
|
leadbay_qualify_top_n: `- \`leadbay_qualify_top_n\` (optional args: count): Bulk-qualify the top N un-qualified leads in the active lens. Uses leadbay_bulk_qualify_leads with a sensible default budget.`,
|
|
1330
1325
|
leadbay_refine_audience: `- \`leadbay_refine_audience\` (required args: instruction): Refine the kind of leads Leadbay surfaces beyond firmographics, with a free-text instruction. Handles the clarification round-trip if the new prompt is ambiguous.`,
|
|
1331
|
-
leadbay_research_a_domain: `- \`leadbay_research_a_domain\` (required args: domain):
|
|
1326
|
+
leadbay_research_a_domain: `- \`leadbay_research_a_domain\` (required args: domain): Resolve a company by name or domain across the user's visible Discover, Monitor, and Activate corpus, then return everything Leadbay knows about it.`,
|
|
1332
1327
|
leadbay_setup_team_prospecting: `- \`leadbay_setup_team_prospecting\` (required args: audience; optional args: rep_split): Manager-led prospecting setup: conversationally turn a natural-language audience ask into a Leadbay lens, validate the candidate leads, and persist them as one or more named campaigns the rep(s) can work through. Closes #3630 US3 end-to-end (within the current creator-scoped campaign visibility model).`,
|
|
1333
1328
|
leadbay_work_campaign: `- \`leadbay_work_campaign\` (optional args: campaign, mode): Work a campaign as a real outreach session: pick the campaign, assess what the user has (phones / emails / coords), then PROPOSE the right session mode (call sheet, email sheet, enrich titles first, map). After they pick, render \u2014 and as they dictate outcomes per lead, record both note + epilogue via \`leadbay_report_outreach\` in one round trip.`
|
|
1334
1329
|
};
|
|
@@ -1363,7 +1358,7 @@ var CATALOG = [
|
|
|
1363
1358
|
arguments: [
|
|
1364
1359
|
{
|
|
1365
1360
|
name: "domain",
|
|
1366
|
-
description: "
|
|
1361
|
+
description: "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.",
|
|
1367
1362
|
required: true
|
|
1368
1363
|
}
|
|
1369
1364
|
],
|
|
@@ -9333,6 +9328,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
|
|
|
9333
9328
|
| Observation | Suggest | Calls |
|
|
9334
9329
|
|------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
|
|
9335
9330
|
| \u2265 5 leads returned (any batch) | "Build an interactive lead triage board for this batch" | emit antArtifact from data in hand (do NOT re-call leadbay_pull_leads) |
|
|
9331
|
+
| \u2265 1 lead returned (any batch) | "Enrich top leads" (reveal decision-maker email/phone on the top leads) | leadbay_enrich_titles({ leadIds: shown leads[].id, lensId }) \u2014 scope to the leads JUST shown; OMIT \`titles\` so it runs the no-spend discovery preview. Confirm titles + channels, then re-call with titles + confirm to launch |
|
|
9336
9332
|
| \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
|
|
9337
9333
|
| \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
|
|
9338
9334
|
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
|
|
@@ -9779,30 +9775,23 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
9779
9775
|
|
|
9780
9776
|
---
|
|
9781
9777
|
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
as \`_by_id\`, with two additions on \`_meta\`:
|
|
9778
|
+
Resolves \`companyName\` across visible Discover, Monitor, and Activate leads,
|
|
9779
|
+
then delegates to **leadbay_research_lead_by_id**. Supplying \`lensId\`
|
|
9780
|
+
deliberately restricts the backend search to that lens. The result matches
|
|
9781
|
+
\`_by_id\`, plus:
|
|
9787
9782
|
|
|
9788
|
-
- \`_meta.resolved_from\`: \`"companyName"\`
|
|
9789
|
-
- \`_meta.resolved_query\`: the original
|
|
9790
|
-
- \`_meta.match_candidates[]\`: up to 4
|
|
9791
|
-
\`{leadId, name, score}\` \u2014 surface these when ambiguity is plausible so the
|
|
9792
|
-
user can redirect.
|
|
9783
|
+
- \`_meta.resolved_from\`: \`"companyName"\`
|
|
9784
|
+
- \`_meta.resolved_query\`: the original query
|
|
9785
|
+
- \`_meta.match_candidates[]\`: up to 4 \`{leadId, name, score}\` alternatives
|
|
9793
9786
|
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
"did you mean\u2026" disambiguation.
|
|
9787
|
+
\`LEAD_NOT_FOUND\` identifies whether the complete visible corpus, an explicit
|
|
9788
|
+
lens, or only a degraded active-lens fallback was searched.
|
|
9797
9789
|
|
|
9798
|
-
WHEN TO USE:
|
|
9799
|
-
|
|
9800
|
-
non-empty, offer the next-best matches in NEXT STEPS so the user can correct
|
|
9801
|
-
a wrong fuzzy hit.
|
|
9790
|
+
WHEN TO USE: for a company/domain/contact reference
|
|
9791
|
+
without a \`lead_id\`. Offer \`_meta.match_candidates\` when present.
|
|
9802
9792
|
|
|
9803
|
-
WHEN NOT TO USE:
|
|
9804
|
-
leadbay_research_lead_by_id directly.
|
|
9805
|
-
\`discoverLeads\` round-trip; skipping it when you can is cheaper.
|
|
9793
|
+
WHEN NOT TO USE: with a UUID; call
|
|
9794
|
+
leadbay_research_lead_by_id directly.
|
|
9806
9795
|
|
|
9807
9796
|
---
|
|
9808
9797
|
|
|
@@ -9943,9 +9932,9 @@ out?"\`
|
|
|
9943
9932
|
|
|
9944
9933
|
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
9945
9934
|
|
|
9946
|
-
| Observation
|
|
9947
|
-
|
|
9948
|
-
|
|
|
9935
|
+
| Observation | Suggest | Calls |
|
|
9936
|
+
|---|---|---|
|
|
9937
|
+
| Alternatives found | "Did you mean **<next-best.name>**?" | leadbay_research_lead_by_id(leadId=<next-best.leadId>) |
|
|
9949
9938
|
`;
|
|
9950
9939
|
var leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Leadbay before file import. The tool sends each row's available identity signals to \`POST /leads/resolve\`, returns matched lead IDs or ambiguous candidate IDs, and produces \`records_for_import\` plus a SAFE identity-only \`mappings_for_import\` starting point for leadbay_import_leads / leadbay_import_and_qualify. This tool deliberately does not try to understand every CSV dialect; the agent should inspect the file, derive clean helper columns when useful, pass explicit \`identity_mappings\`, and build the final CRM mapping from \`mapping_guidance\`.
|
|
9951
9940
|
|
|
@@ -15350,6 +15339,11 @@ function buildPullLeadsNextSteps(args) {
|
|
|
15350
15339
|
description: "Build an interactive lead triage board to sort and filter this batch.",
|
|
15351
15340
|
kind: "build_artifact"
|
|
15352
15341
|
});
|
|
15342
|
+
options.push({
|
|
15343
|
+
label: "Enrich top leads",
|
|
15344
|
+
description: "Enrich the top leads",
|
|
15345
|
+
kind: "enrich_top_leads"
|
|
15346
|
+
});
|
|
15353
15347
|
options.push({
|
|
15354
15348
|
label: "Deepen qualification",
|
|
15355
15349
|
description: "Run deeper AI qualification on these leads.",
|
|
@@ -15435,7 +15429,7 @@ var pullLeads = {
|
|
|
15435
15429
|
},
|
|
15436
15430
|
next_steps: {
|
|
15437
15431
|
type: ["object", "null"],
|
|
15438
|
-
description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty. When the batch is empty but the lens is still computing (computing_wishlist/computing_scores true), this carries a 'Re-pull in ~30s' option (kind:repull_computing) plus 'Refine audience' \u2014 render the widget so the user waits rather than seeing 'no leads.' null only when the batch is empty AND nothing is computing (a genuinely empty / over-narrow lens).",
|
|
15432
|
+
description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty; options[1] is the enrich offer (kind:enrich_top_leads \u2014 route it to leadbay_enrich_titles scoped to the leadIds JUST shown (pass leads[].id + the pinned lens.id) with NO titles, so it runs the no-spend discovery preview; quota is only spent after the user picks titles + confirms channels on a follow-up call). When the batch is empty but the lens is still computing (computing_wishlist/computing_scores true), this carries a 'Re-pull in ~30s' option (kind:repull_computing) plus 'Refine audience' \u2014 render the widget so the user waits rather than seeing 'no leads.' null only when the batch is empty AND nothing is computing (a genuinely empty / over-narrow lens).",
|
|
15439
15433
|
properties: {
|
|
15440
15434
|
question: { type: "string" },
|
|
15441
15435
|
options: {
|
|
@@ -17224,6 +17218,41 @@ function rankSubstringMatches(needle, candidates) {
|
|
|
17224
17218
|
});
|
|
17225
17219
|
return hits;
|
|
17226
17220
|
}
|
|
17221
|
+
function parseLensId(value) {
|
|
17222
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
17223
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
17224
|
+
}
|
|
17225
|
+
function suggestionName(suggestion) {
|
|
17226
|
+
const companyName = (suggestion.company_name ?? suggestion.companyName)?.trim();
|
|
17227
|
+
return companyName || suggestion.text.trim();
|
|
17228
|
+
}
|
|
17229
|
+
function suggestionLeadId(suggestion) {
|
|
17230
|
+
return suggestion.lead_id ?? suggestion.leadId;
|
|
17231
|
+
}
|
|
17232
|
+
function isLeadbayError(error) {
|
|
17233
|
+
return typeof error === "object" && error !== null && error.error === true && typeof error.code === "string" && typeof error.message === "string" && typeof error.hint === "string";
|
|
17234
|
+
}
|
|
17235
|
+
async function resolveWithinLens(client, query, lensId) {
|
|
17236
|
+
const results = await client.request("GET", `/lenses/${lensId}/leads/wishlist?q=${encodeURIComponent(query)}&count=50&page=0&contacts=false`);
|
|
17237
|
+
return results.items.map((lead) => ({
|
|
17238
|
+
id: lead.id,
|
|
17239
|
+
name: lead.name,
|
|
17240
|
+
score: lead.score,
|
|
17241
|
+
lensId
|
|
17242
|
+
}));
|
|
17243
|
+
}
|
|
17244
|
+
async function resolveAcrossVisibleCorpus(client, query) {
|
|
17245
|
+
const suggestions = await client.request("GET", `/search/suggest?q=${encodeURIComponent(query)}`);
|
|
17246
|
+
return suggestions.map((suggestion) => {
|
|
17247
|
+
const id = suggestionLeadId(suggestion);
|
|
17248
|
+
return {
|
|
17249
|
+
id: id ?? "",
|
|
17250
|
+
name: suggestionName(suggestion),
|
|
17251
|
+
score: null,
|
|
17252
|
+
lensId: parseLensId(suggestion.lens_id ?? suggestion.lensId)
|
|
17253
|
+
};
|
|
17254
|
+
}).filter((suggestion) => suggestion.id !== "" && suggestion.name !== "");
|
|
17255
|
+
}
|
|
17227
17256
|
var researchLeadByNameFuzzy = {
|
|
17228
17257
|
name: "leadbay_research_lead_by_name_fuzzy",
|
|
17229
17258
|
annotations: {
|
|
@@ -17239,11 +17268,11 @@ var researchLeadByNameFuzzy = {
|
|
|
17239
17268
|
properties: {
|
|
17240
17269
|
companyName: {
|
|
17241
17270
|
type: "string",
|
|
17242
|
-
description: "Company name
|
|
17271
|
+
description: "Company name, domain, or contact name to resolve across visible Leadbay leads in Discover, Monitor, and Activate."
|
|
17243
17272
|
},
|
|
17244
17273
|
lensId: {
|
|
17245
17274
|
type: "number",
|
|
17246
|
-
description: "
|
|
17275
|
+
description: "Optional strict scope. When supplied, search only this lens's wishlist; normally omit to search all visible Leadbay leads."
|
|
17247
17276
|
},
|
|
17248
17277
|
concise: {
|
|
17249
17278
|
type: "boolean",
|
|
@@ -17268,18 +17297,34 @@ var researchLeadByNameFuzzy = {
|
|
|
17268
17297
|
additionalProperties: true
|
|
17269
17298
|
},
|
|
17270
17299
|
execute: async (client, params, ctx) => {
|
|
17271
|
-
if (!params.companyName || typeof params.companyName !== "string") {
|
|
17300
|
+
if (!params.companyName || typeof params.companyName !== "string" || params.companyName.trim() === "") {
|
|
17272
17301
|
throw client.makeError("INVALID_PARAMS", "companyName is required", "Pass the company name as a string. If you already have the lead UUID, call leadbay_research_lead_by_id directly.");
|
|
17273
17302
|
}
|
|
17274
|
-
const
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17303
|
+
const query = params.companyName.trim();
|
|
17304
|
+
let ranked;
|
|
17305
|
+
let lensId = params.lensId;
|
|
17306
|
+
let usedActiveLensFallback = false;
|
|
17307
|
+
if (lensId !== void 0) {
|
|
17308
|
+
ranked = await resolveWithinLens(client, query, lensId);
|
|
17309
|
+
} else {
|
|
17310
|
+
try {
|
|
17311
|
+
ranked = await resolveAcrossVisibleCorpus(client, query);
|
|
17312
|
+
} catch (error) {
|
|
17313
|
+
if (isLeadbayError(error))
|
|
17314
|
+
throw error;
|
|
17315
|
+
lensId = await client.resolveDefaultLens();
|
|
17316
|
+
usedActiveLensFallback = true;
|
|
17317
|
+
ctx?.logger?.warn?.("Cross-tab company search was unavailable; falling back to the active lens for this lookup.");
|
|
17318
|
+
ranked = rankSubstringMatches(query, await resolveWithinLens(client, query, lensId));
|
|
17319
|
+
}
|
|
17320
|
+
}
|
|
17278
17321
|
if (ranked.length === 0) {
|
|
17279
|
-
const
|
|
17280
|
-
|
|
17322
|
+
const scope = usedActiveLensFallback ? `in active lens ${lensId}; cross-tab search was unavailable` : params.lensId === void 0 ? "across your visible Leadbay leads" : `in lens ${params.lensId}`;
|
|
17323
|
+
const hint = usedActiveLensFallback ? `Only active lens ${lensId} was checked because cross-tab search was unavailable. Retry later before concluding the company is missing or offering to import it.` : params.lensId === void 0 ? "Search checks company names, domains, and contact names across Discover, Monitor, and Activate. Confirm the spelling or domain, or add/import the company first." : "This lookup was intentionally restricted to the supplied lens. Omit lensId to search visible leads across Discover, Monitor, and Activate.";
|
|
17324
|
+
throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${query}" ${scope}`, hint);
|
|
17281
17325
|
}
|
|
17282
17326
|
const [primary, ...rest] = ranked;
|
|
17327
|
+
lensId = primary.lensId ?? lensId ?? await client.resolveDefaultLens();
|
|
17283
17328
|
const candidates = rest.slice(0, 4).map((m) => ({
|
|
17284
17329
|
leadId: m.id,
|
|
17285
17330
|
name: m.name,
|
|
@@ -17292,7 +17337,7 @@ var researchLeadByNameFuzzy = {
|
|
|
17292
17337
|
response_format: params.response_format,
|
|
17293
17338
|
_resolved: {
|
|
17294
17339
|
from: "companyName",
|
|
17295
|
-
query
|
|
17340
|
+
query,
|
|
17296
17341
|
candidates
|
|
17297
17342
|
}
|
|
17298
17343
|
}, ctx);
|
|
@@ -23154,18 +23199,41 @@ var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown
|
|
|
23154
23199
|
// src/telemetry.ts
|
|
23155
23200
|
import { PostHog } from "posthog-node";
|
|
23156
23201
|
import * as Sentry from "@sentry/node";
|
|
23202
|
+
|
|
23203
|
+
// src/telemetry-constants.ts
|
|
23204
|
+
var EMBEDDED_POSTHOG_KEY = "phc_N9SnA7OULuAlXReQJZ0Y3rPI4eC0mJLpMRbzgqamhHR";
|
|
23205
|
+
var EMBEDDED_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
23206
|
+
var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o4505874436849664.ingest.us.sentry.io/4511419984248832";
|
|
23207
|
+
|
|
23208
|
+
// src/telemetry-events.ts
|
|
23209
|
+
var EV_TOOL_CALL = "mcp tool called";
|
|
23210
|
+
var EV_QUOTA_HIT = "mcp quota hit";
|
|
23211
|
+
var EV_TOPUP_LINK = "mcp topup link created";
|
|
23212
|
+
var EV_STARTUP = "mcp startup";
|
|
23213
|
+
var EV_MCP_UPDATE_CHECK = "mcp update check";
|
|
23214
|
+
var EV_MCP_UPDATE_PROMPTED = "mcp update prompted";
|
|
23215
|
+
var EV_MCP_UPDATE_INSTALL_CLICKED = "mcp update install_clicked";
|
|
23216
|
+
var EV_MCP_UPDATE_DISMISSED = "mcp update dismissed";
|
|
23217
|
+
var EV_MCP_VERSION_UPDATED = "mcp version updated";
|
|
23218
|
+
var EV_AGENT_MEMORY_CAPTURED = "agent_memory_captured";
|
|
23219
|
+
var EV_AGENT_MEMORY_RECALLED = "agent_memory_recalled";
|
|
23220
|
+
var EV_AGENT_MEMORY_PRUNED = "agent_memory_pruned";
|
|
23221
|
+
var EV_FRICTION_REPORTED = "mcp friction reported";
|
|
23222
|
+
var EV_COMPOSITE_CALL = "mcp composite call";
|
|
23223
|
+
|
|
23224
|
+
// src/telemetry.ts
|
|
23157
23225
|
var NOOP_TELEMETRY = {
|
|
23158
23226
|
identify: async () => {
|
|
23159
23227
|
},
|
|
23160
|
-
captureToolCall: () => {
|
|
23228
|
+
captureToolCall: (_props, _identity) => {
|
|
23161
23229
|
},
|
|
23162
|
-
captureCompositeCall: () => {
|
|
23230
|
+
captureCompositeCall: (_props, _identity) => {
|
|
23163
23231
|
},
|
|
23164
|
-
captureQuotaHit: () => {
|
|
23232
|
+
captureQuotaHit: (_props, _identity) => {
|
|
23165
23233
|
},
|
|
23166
|
-
captureTopupLink: () => {
|
|
23234
|
+
captureTopupLink: (_props, _identity) => {
|
|
23167
23235
|
},
|
|
23168
|
-
captureStartup: () => {
|
|
23236
|
+
captureStartup: (_props, _identity) => {
|
|
23169
23237
|
},
|
|
23170
23238
|
captureAgentMemoryCaptured: () => {
|
|
23171
23239
|
},
|
|
@@ -23191,6 +23259,320 @@ var NOOP_TELEMETRY = {
|
|
|
23191
23259
|
shutdown: async () => {
|
|
23192
23260
|
}
|
|
23193
23261
|
};
|
|
23262
|
+
function parseTelemetryEnv(raw) {
|
|
23263
|
+
if (raw === void 0 || raw === "") return true;
|
|
23264
|
+
const v = raw.trim().toLowerCase();
|
|
23265
|
+
if (v === "false" || v === "0" || v === "no" || v === "off") return false;
|
|
23266
|
+
return true;
|
|
23267
|
+
}
|
|
23268
|
+
function initTelemetry(opts) {
|
|
23269
|
+
if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
|
|
23270
|
+
if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
|
|
23271
|
+
const posthogKey = process.env.LEADBAY_POSTHOG_KEY ?? EMBEDDED_POSTHOG_KEY;
|
|
23272
|
+
const sentryDsn = process.env.LEADBAY_SENTRY_DSN ?? EMBEDDED_SENTRY_DSN;
|
|
23273
|
+
if (!posthogKey && !sentryDsn) return NOOP_TELEMETRY;
|
|
23274
|
+
const { version, logger: logger2 } = opts;
|
|
23275
|
+
const flushAt = opts.flushAt ?? 20;
|
|
23276
|
+
const flushInterval = opts.flushInterval ?? 1e4;
|
|
23277
|
+
const environment = process.env.LEADBAY_ENV ?? (version.includes("-dev.") ? "dev" : "production");
|
|
23278
|
+
let posthog = null;
|
|
23279
|
+
let sentryReady = false;
|
|
23280
|
+
let initError = null;
|
|
23281
|
+
try {
|
|
23282
|
+
if (posthogKey) {
|
|
23283
|
+
posthog = new PostHog(posthogKey, {
|
|
23284
|
+
host: process.env.LEADBAY_POSTHOG_HOST ?? EMBEDDED_POSTHOG_HOST,
|
|
23285
|
+
flushAt,
|
|
23286
|
+
flushInterval,
|
|
23287
|
+
disableGeoip: false
|
|
23288
|
+
});
|
|
23289
|
+
}
|
|
23290
|
+
} catch (err) {
|
|
23291
|
+
initError = err;
|
|
23292
|
+
posthog = null;
|
|
23293
|
+
}
|
|
23294
|
+
try {
|
|
23295
|
+
if (sentryDsn) {
|
|
23296
|
+
Sentry.init({
|
|
23297
|
+
dsn: sentryDsn,
|
|
23298
|
+
release: `@leadbay/mcp@${version}`,
|
|
23299
|
+
environment,
|
|
23300
|
+
tracesSampleRate: 0,
|
|
23301
|
+
profilesSampleRate: 0,
|
|
23302
|
+
defaultIntegrations: false,
|
|
23303
|
+
integrations: [Sentry.httpIntegration()],
|
|
23304
|
+
sendDefaultPii: false,
|
|
23305
|
+
// Tag every captured event with the surface so Sentry views can
|
|
23306
|
+
// split MCP issues from web-app issues without per-call work.
|
|
23307
|
+
// Version is also encoded in `release` above, but a dedicated
|
|
23308
|
+
// `mcp_version` tag is filterable from Sentry's issue list without
|
|
23309
|
+
// expanding the release dropdown — load-bearing when triaging
|
|
23310
|
+
// "errors at reinstall on @0.13" vs older clients still on @0.11.
|
|
23311
|
+
initialScope: {
|
|
23312
|
+
tags: {
|
|
23313
|
+
source: "mcp",
|
|
23314
|
+
mcp_version: version,
|
|
23315
|
+
node_version: process.versions.node,
|
|
23316
|
+
platform: process.platform
|
|
23317
|
+
}
|
|
23318
|
+
}
|
|
23319
|
+
});
|
|
23320
|
+
sentryReady = true;
|
|
23321
|
+
}
|
|
23322
|
+
} catch (err) {
|
|
23323
|
+
initError = initError ?? err;
|
|
23324
|
+
sentryReady = false;
|
|
23325
|
+
}
|
|
23326
|
+
if (initError) {
|
|
23327
|
+
logger2?.warn?.(`telemetry init failed: ${initError.message ?? initError}`);
|
|
23328
|
+
}
|
|
23329
|
+
if (!posthog && !sentryReady) return NOOP_TELEMETRY;
|
|
23330
|
+
let me = null;
|
|
23331
|
+
let identityPromise = null;
|
|
23332
|
+
const pendingEvents = [];
|
|
23333
|
+
let region = "unknown";
|
|
23334
|
+
const baseProps = () => ({
|
|
23335
|
+
// Always tag MCP-originated events so PostHog dashboards can split
|
|
23336
|
+
// MCP usage from the web app and any future surfaces. The value
|
|
23337
|
+
// ("mcp") is the canonical source identifier — match it in any
|
|
23338
|
+
// PostHog filter or insight that should isolate the MCP surface.
|
|
23339
|
+
source: "mcp",
|
|
23340
|
+
mcp_version: version,
|
|
23341
|
+
node_version: process.versions.node,
|
|
23342
|
+
platform: process.platform,
|
|
23343
|
+
region
|
|
23344
|
+
});
|
|
23345
|
+
const distinctIdFor = () => {
|
|
23346
|
+
if (me?.email) return me.email;
|
|
23347
|
+
if (me?.id) return `mcp:user-${me.id}`;
|
|
23348
|
+
return "mcp:unknown";
|
|
23349
|
+
};
|
|
23350
|
+
const groupsFor = () => {
|
|
23351
|
+
return me?.organization?.id ? { organization: me.organization.id } : void 0;
|
|
23352
|
+
};
|
|
23353
|
+
const doCapture = (event, properties, identity) => {
|
|
23354
|
+
if (!posthog) return;
|
|
23355
|
+
try {
|
|
23356
|
+
const props = identity?.region ? { ...baseProps(), region: identity.region, ...properties } : { ...baseProps(), ...properties };
|
|
23357
|
+
posthog.capture({
|
|
23358
|
+
distinctId: identity ? identity.distinctId : distinctIdFor(),
|
|
23359
|
+
event,
|
|
23360
|
+
properties: props,
|
|
23361
|
+
groups: identity ? identity.groups : groupsFor()
|
|
23362
|
+
});
|
|
23363
|
+
} catch (err) {
|
|
23364
|
+
logger2?.warn?.(`posthog capture failed: ${err?.message ?? err}`);
|
|
23365
|
+
}
|
|
23366
|
+
};
|
|
23367
|
+
const emit = (event, properties, identity) => {
|
|
23368
|
+
if (!posthog) return;
|
|
23369
|
+
if (identity) {
|
|
23370
|
+
doCapture(event, properties, identity);
|
|
23371
|
+
return;
|
|
23372
|
+
}
|
|
23373
|
+
if (!me) {
|
|
23374
|
+
pendingEvents.push({ event, properties });
|
|
23375
|
+
return;
|
|
23376
|
+
}
|
|
23377
|
+
doCapture(event, properties);
|
|
23378
|
+
};
|
|
23379
|
+
const flushPending = () => {
|
|
23380
|
+
if (!posthog || pendingEvents.length === 0) return;
|
|
23381
|
+
const buf = pendingEvents.splice(0, pendingEvents.length);
|
|
23382
|
+
for (const { event, properties } of buf) {
|
|
23383
|
+
doCapture(event, properties);
|
|
23384
|
+
}
|
|
23385
|
+
};
|
|
23386
|
+
return {
|
|
23387
|
+
identify(client) {
|
|
23388
|
+
if (identityPromise) return identityPromise;
|
|
23389
|
+
region = client.region;
|
|
23390
|
+
identityPromise = (async () => {
|
|
23391
|
+
try {
|
|
23392
|
+
const resolved = await client.resolveMe();
|
|
23393
|
+
me = resolved;
|
|
23394
|
+
if (posthog && resolved.email) {
|
|
23395
|
+
try {
|
|
23396
|
+
posthog.identify({
|
|
23397
|
+
distinctId: resolved.email,
|
|
23398
|
+
properties: {
|
|
23399
|
+
email: resolved.email,
|
|
23400
|
+
leadbay_id: resolved.id,
|
|
23401
|
+
leadbay_name: resolved.name,
|
|
23402
|
+
leadbay_organization: resolved.organization?.name,
|
|
23403
|
+
leadbay_organization_id: resolved.organization?.id
|
|
23404
|
+
}
|
|
23405
|
+
});
|
|
23406
|
+
} catch (err) {
|
|
23407
|
+
logger2?.warn?.(`posthog identify failed: ${err?.message ?? err}`);
|
|
23408
|
+
}
|
|
23409
|
+
}
|
|
23410
|
+
if (sentryReady) {
|
|
23411
|
+
try {
|
|
23412
|
+
Sentry.setUser({
|
|
23413
|
+
id: resolved.id,
|
|
23414
|
+
email: resolved.email,
|
|
23415
|
+
username: resolved.name
|
|
23416
|
+
});
|
|
23417
|
+
} catch (err) {
|
|
23418
|
+
logger2?.warn?.(`sentry setUser failed: ${err?.message ?? err}`);
|
|
23419
|
+
}
|
|
23420
|
+
}
|
|
23421
|
+
flushPending();
|
|
23422
|
+
} catch (err) {
|
|
23423
|
+
logger2?.warn?.(
|
|
23424
|
+
`telemetry identify failed (${err?.message ?? err}); flushing events anonymously`
|
|
23425
|
+
);
|
|
23426
|
+
me = {
|
|
23427
|
+
id: "unknown",
|
|
23428
|
+
organization: { id: "unknown", name: "unknown" }
|
|
23429
|
+
};
|
|
23430
|
+
flushPending();
|
|
23431
|
+
}
|
|
23432
|
+
})();
|
|
23433
|
+
return identityPromise;
|
|
23434
|
+
},
|
|
23435
|
+
captureToolCall(props, identity) {
|
|
23436
|
+
emit(EV_TOOL_CALL, { ...props }, identity);
|
|
23437
|
+
},
|
|
23438
|
+
captureCompositeCall(props, identity) {
|
|
23439
|
+
emit(EV_COMPOSITE_CALL, { ...props }, identity);
|
|
23440
|
+
},
|
|
23441
|
+
captureQuotaHit(props, identity) {
|
|
23442
|
+
emit(EV_QUOTA_HIT, { ...props }, identity);
|
|
23443
|
+
},
|
|
23444
|
+
captureTopupLink(props, identity) {
|
|
23445
|
+
emit(EV_TOPUP_LINK, { ...props }, identity);
|
|
23446
|
+
},
|
|
23447
|
+
captureStartup(props, identity) {
|
|
23448
|
+
emit(EV_STARTUP, { ...props }, identity);
|
|
23449
|
+
},
|
|
23450
|
+
captureAgentMemoryCaptured(props, identity) {
|
|
23451
|
+
emit(EV_AGENT_MEMORY_CAPTURED, { ...props }, identity);
|
|
23452
|
+
},
|
|
23453
|
+
captureAgentMemoryRecalled(props, identity) {
|
|
23454
|
+
emit(EV_AGENT_MEMORY_RECALLED, { ...props }, identity);
|
|
23455
|
+
},
|
|
23456
|
+
captureAgentMemoryPruned(props, identity) {
|
|
23457
|
+
emit(EV_AGENT_MEMORY_PRUNED, { ...props }, identity);
|
|
23458
|
+
},
|
|
23459
|
+
captureFrictionReported(props, identity) {
|
|
23460
|
+
emit(EV_FRICTION_REPORTED, { ...props }, identity);
|
|
23461
|
+
},
|
|
23462
|
+
captureUpdateCheck(props) {
|
|
23463
|
+
emit(EV_MCP_UPDATE_CHECK, { ...props });
|
|
23464
|
+
},
|
|
23465
|
+
captureUpdatePrompted(props) {
|
|
23466
|
+
emit(EV_MCP_UPDATE_PROMPTED, { ...props });
|
|
23467
|
+
},
|
|
23468
|
+
captureUpdateInstallClicked(props) {
|
|
23469
|
+
emit(EV_MCP_UPDATE_INSTALL_CLICKED, { ...props });
|
|
23470
|
+
},
|
|
23471
|
+
captureUpdateDismissed(props) {
|
|
23472
|
+
emit(EV_MCP_UPDATE_DISMISSED, { ...props });
|
|
23473
|
+
},
|
|
23474
|
+
captureVersionUpdated(props) {
|
|
23475
|
+
emit(EV_MCP_VERSION_UPDATED, { ...props });
|
|
23476
|
+
},
|
|
23477
|
+
captureException(err, ctx) {
|
|
23478
|
+
if (!sentryReady) return;
|
|
23479
|
+
try {
|
|
23480
|
+
Sentry.withScope((scope) => {
|
|
23481
|
+
scope.setTag("tool", ctx.tool);
|
|
23482
|
+
if (ctx.code) scope.setTag("error_code", ctx.code);
|
|
23483
|
+
if (ctx.endpoint) scope.setTag("endpoint", ctx.endpoint);
|
|
23484
|
+
if (ctx.region) scope.setTag("region", ctx.region);
|
|
23485
|
+
if (ctx.http_status !== void 0) {
|
|
23486
|
+
scope.setTag("http_status", String(ctx.http_status));
|
|
23487
|
+
}
|
|
23488
|
+
if (ctx.source) scope.setTag("source", ctx.source);
|
|
23489
|
+
if (me?.organization?.id) {
|
|
23490
|
+
scope.setTag("organization", me.organization.id);
|
|
23491
|
+
}
|
|
23492
|
+
if (ctx.message) scope.setExtra("message", ctx.message);
|
|
23493
|
+
if (ctx.hint) scope.setExtra("hint", ctx.hint);
|
|
23494
|
+
if (ctx.triggered_by) scope.setExtra("triggered_by", ctx.triggered_by);
|
|
23495
|
+
if (ctx.latency_ms !== void 0 && ctx.latency_ms !== null) {
|
|
23496
|
+
scope.setExtra("latency_ms", ctx.latency_ms);
|
|
23497
|
+
}
|
|
23498
|
+
if (ctx.retry_after !== void 0 && ctx.retry_after !== null) {
|
|
23499
|
+
scope.setExtra("retry_after", ctx.retry_after);
|
|
23500
|
+
}
|
|
23501
|
+
if (ctx.code && ctx.source === "business") {
|
|
23502
|
+
scope.setFingerprint(["mcp", ctx.tool, ctx.code]);
|
|
23503
|
+
}
|
|
23504
|
+
Sentry.captureException(err);
|
|
23505
|
+
});
|
|
23506
|
+
} catch (e) {
|
|
23507
|
+
logger2?.warn?.(`sentry captureException failed: ${e?.message ?? e}`);
|
|
23508
|
+
}
|
|
23509
|
+
},
|
|
23510
|
+
async captureFeedback(message, opts2, identity) {
|
|
23511
|
+
if (!sentryReady) return false;
|
|
23512
|
+
const trimmed = (message ?? "").trim();
|
|
23513
|
+
if (!trimmed) return false;
|
|
23514
|
+
if (!identity && identityPromise) {
|
|
23515
|
+
let waitTimer;
|
|
23516
|
+
try {
|
|
23517
|
+
await Promise.race([
|
|
23518
|
+
identityPromise,
|
|
23519
|
+
new Promise((resolve) => {
|
|
23520
|
+
waitTimer = setTimeout(resolve, 2e3);
|
|
23521
|
+
})
|
|
23522
|
+
]);
|
|
23523
|
+
} catch {
|
|
23524
|
+
} finally {
|
|
23525
|
+
if (waitTimer) clearTimeout(waitTimer);
|
|
23526
|
+
}
|
|
23527
|
+
}
|
|
23528
|
+
const fbName = identity?.name ?? me?.name;
|
|
23529
|
+
const fbEmail = identity?.email ?? me?.email;
|
|
23530
|
+
try {
|
|
23531
|
+
Sentry.captureFeedback({
|
|
23532
|
+
message: trimmed,
|
|
23533
|
+
...fbName ? { name: fbName } : {},
|
|
23534
|
+
...fbEmail ? { email: fbEmail } : {},
|
|
23535
|
+
...opts2?.associatedEventId ? { associatedEventId: opts2.associatedEventId } : {}
|
|
23536
|
+
});
|
|
23537
|
+
const flushed = await Sentry.flush(4e3);
|
|
23538
|
+
if (!flushed) {
|
|
23539
|
+
logger2?.warn?.("sentry feedback flush timed out (event may be buffered)");
|
|
23540
|
+
}
|
|
23541
|
+
return flushed;
|
|
23542
|
+
} catch (e) {
|
|
23543
|
+
logger2?.warn?.(`sentry captureFeedback failed: ${e?.message ?? e}`);
|
|
23544
|
+
return false;
|
|
23545
|
+
}
|
|
23546
|
+
},
|
|
23547
|
+
async shutdown() {
|
|
23548
|
+
if (!me && identityPromise) {
|
|
23549
|
+
let waitTimer;
|
|
23550
|
+
try {
|
|
23551
|
+
await Promise.race([
|
|
23552
|
+
identityPromise,
|
|
23553
|
+
new Promise((resolve) => {
|
|
23554
|
+
waitTimer = setTimeout(resolve, 1500);
|
|
23555
|
+
})
|
|
23556
|
+
]);
|
|
23557
|
+
} catch {
|
|
23558
|
+
} finally {
|
|
23559
|
+
if (waitTimer) clearTimeout(waitTimer);
|
|
23560
|
+
}
|
|
23561
|
+
}
|
|
23562
|
+
if (!me) {
|
|
23563
|
+
me = {
|
|
23564
|
+
id: "unknown",
|
|
23565
|
+
organization: { id: "unknown", name: "unknown" }
|
|
23566
|
+
};
|
|
23567
|
+
flushPending();
|
|
23568
|
+
}
|
|
23569
|
+
const tasks = [];
|
|
23570
|
+
if (posthog) tasks.push(posthog.shutdown(2e3).catch(() => void 0));
|
|
23571
|
+
if (sentryReady) tasks.push(Sentry.close(2e3).catch(() => void 0));
|
|
23572
|
+
await Promise.allSettled(tasks);
|
|
23573
|
+
}
|
|
23574
|
+
};
|
|
23575
|
+
}
|
|
23194
23576
|
|
|
23195
23577
|
// src/update-check.ts
|
|
23196
23578
|
var cachedInfo = null;
|
|
@@ -23838,7 +24220,7 @@ function buildServer(client, opts = {}) {
|
|
|
23838
24220
|
});
|
|
23839
24221
|
const DEBUG_RAW = process.env.LEADBAY_DEBUG ?? "";
|
|
23840
24222
|
const DEBUG_ON = DEBUG_RAW === "1" || DEBUG_RAW.toLowerCase() === "true";
|
|
23841
|
-
const
|
|
24223
|
+
const telemetry2 = opts.telemetry ?? NOOP_TELEMETRY;
|
|
23842
24224
|
const promptedVersionsThisSession = /* @__PURE__ */ new Set();
|
|
23843
24225
|
const serverVersion = opts.version ?? "0.0.0-dev";
|
|
23844
24226
|
const UPDATE_CHECK_DISABLED = process.env.LEADBAY_UPDATE_CHECK_DISABLED === "1";
|
|
@@ -23848,7 +24230,7 @@ function buildServer(client, opts = {}) {
|
|
|
23848
24230
|
void checkForUpdate({
|
|
23849
24231
|
currentVersion: serverVersion,
|
|
23850
24232
|
stateStore: opts.updateStateStore,
|
|
23851
|
-
telemetry,
|
|
24233
|
+
telemetry: telemetry2,
|
|
23852
24234
|
logger: opts.logger
|
|
23853
24235
|
}).catch((err) => {
|
|
23854
24236
|
opts.logger?.warn?.(
|
|
@@ -23898,7 +24280,7 @@ function buildServer(client, opts = {}) {
|
|
|
23898
24280
|
}
|
|
23899
24281
|
if (!alreadyPrompted) {
|
|
23900
24282
|
promptedVersionsThisSession.add(info.latest_version);
|
|
23901
|
-
|
|
24283
|
+
telemetry2.captureUpdatePrompted?.({
|
|
23902
24284
|
current_version: serverVersion,
|
|
23903
24285
|
latest_version: info.latest_version
|
|
23904
24286
|
});
|
|
@@ -23945,7 +24327,7 @@ function buildServer(client, opts = {}) {
|
|
|
23945
24327
|
if (typeof fr.category !== "string" || typeof fr.user_quote !== "string") {
|
|
23946
24328
|
return;
|
|
23947
24329
|
}
|
|
23948
|
-
|
|
24330
|
+
telemetry2.captureFrictionReported({
|
|
23949
24331
|
category: fr.category,
|
|
23950
24332
|
user_quote: fr.user_quote,
|
|
23951
24333
|
...typeof fr.tool_called === "string" ? { tool_called: fr.tool_called } : {},
|
|
@@ -23957,7 +24339,7 @@ function buildServer(client, opts = {}) {
|
|
|
23957
24339
|
if (!result || typeof result !== "object") return;
|
|
23958
24340
|
const meta = result._meta ?? {};
|
|
23959
24341
|
if (toolName === "leadbay_agent_memory_capture") {
|
|
23960
|
-
|
|
24342
|
+
telemetry2.captureAgentMemoryCaptured({
|
|
23961
24343
|
source: result.captured?.source ?? meta.source,
|
|
23962
24344
|
scope: result.captured?.scope ?? meta.scope,
|
|
23963
24345
|
key: result.captured?.key,
|
|
@@ -23965,13 +24347,13 @@ function buildServer(client, opts = {}) {
|
|
|
23965
24347
|
account_id_hash: meta.account_id_hash
|
|
23966
24348
|
});
|
|
23967
24349
|
} else if (toolName === "leadbay_agent_memory_recall") {
|
|
23968
|
-
|
|
24350
|
+
telemetry2.captureAgentMemoryRecalled({
|
|
23969
24351
|
entries_returned: result.entries_returned,
|
|
23970
24352
|
total_active: result.total_active,
|
|
23971
24353
|
account_id_hash: meta.account_id_hash
|
|
23972
24354
|
});
|
|
23973
24355
|
} else if (toolName === "leadbay_agent_memory_review" && result.changed === true && (result.action === "retract" || result.action === "prune")) {
|
|
23974
|
-
|
|
24356
|
+
telemetry2.captureAgentMemoryPruned({
|
|
23975
24357
|
action: result.action,
|
|
23976
24358
|
account_id_hash: meta.account_id_hash
|
|
23977
24359
|
});
|
|
@@ -24058,7 +24440,7 @@ ${url}
|
|
|
24058
24440
|
};
|
|
24059
24441
|
const pendingText = formatErrorForLLM(envelope);
|
|
24060
24442
|
const pendingDur = Date.now() - callStart;
|
|
24061
|
-
|
|
24443
|
+
telemetry2.captureToolCall({
|
|
24062
24444
|
tool: name,
|
|
24063
24445
|
ok: false,
|
|
24064
24446
|
duration_ms: pendingDur,
|
|
@@ -24087,7 +24469,7 @@ ${url}
|
|
|
24087
24469
|
};
|
|
24088
24470
|
const guardText = formatErrorForLLM(envelope);
|
|
24089
24471
|
const guardDur = Date.now() - callStart;
|
|
24090
|
-
|
|
24472
|
+
telemetry2.captureToolCall({
|
|
24091
24473
|
tool: name,
|
|
24092
24474
|
ok: false,
|
|
24093
24475
|
duration_ms: guardDur,
|
|
@@ -24096,7 +24478,7 @@ ${url}
|
|
|
24096
24478
|
error_code: envelope.code,
|
|
24097
24479
|
triggered_by
|
|
24098
24480
|
});
|
|
24099
|
-
|
|
24481
|
+
telemetry2.captureCompositeCall({
|
|
24100
24482
|
tool: name,
|
|
24101
24483
|
last_prompt: triggered_by ?? "",
|
|
24102
24484
|
ok: false,
|
|
@@ -24128,7 +24510,7 @@ ${url}
|
|
|
24128
24510
|
// Route leadbay_send_feedback to Sentry's feedback inbox (same place
|
|
24129
24511
|
// the web app's form lands). NOOP_TELEMETRY returns false, so the
|
|
24130
24512
|
// tool reports honestly when telemetry is off.
|
|
24131
|
-
sendFeedback: (message, fbOpts) =>
|
|
24513
|
+
sendFeedback: (message, fbOpts) => telemetry2.captureFeedback(message, fbOpts)
|
|
24132
24514
|
});
|
|
24133
24515
|
await maybeAttachUpdate(name, result);
|
|
24134
24516
|
maybeAttachNotifications(result);
|
|
@@ -24137,13 +24519,13 @@ ${url}
|
|
|
24137
24519
|
const envDur = Date.now() - callStart;
|
|
24138
24520
|
const envCode = result.code ?? "Error";
|
|
24139
24521
|
if (envCode === "QUOTA_EXCEEDED") {
|
|
24140
|
-
|
|
24522
|
+
telemetry2.captureQuotaHit({
|
|
24141
24523
|
tool: name,
|
|
24142
24524
|
retry_after_s: result._meta?.retry_after,
|
|
24143
24525
|
endpoint: result._meta?.endpoint
|
|
24144
24526
|
});
|
|
24145
24527
|
}
|
|
24146
|
-
|
|
24528
|
+
telemetry2.captureToolCall({
|
|
24147
24529
|
tool: name,
|
|
24148
24530
|
ok: false,
|
|
24149
24531
|
duration_ms: envDur,
|
|
@@ -24153,7 +24535,7 @@ ${url}
|
|
|
24153
24535
|
triggered_by
|
|
24154
24536
|
});
|
|
24155
24537
|
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24156
|
-
|
|
24538
|
+
telemetry2.captureCompositeCall({
|
|
24157
24539
|
tool: name,
|
|
24158
24540
|
last_prompt: triggered_by ?? "",
|
|
24159
24541
|
ok: false,
|
|
@@ -24161,7 +24543,7 @@ ${url}
|
|
|
24161
24543
|
error_code: envCode
|
|
24162
24544
|
});
|
|
24163
24545
|
}
|
|
24164
|
-
|
|
24546
|
+
telemetry2.captureException(
|
|
24165
24547
|
result,
|
|
24166
24548
|
buildBusinessCtx(name, result, triggered_by)
|
|
24167
24549
|
);
|
|
@@ -24189,7 +24571,7 @@ ${url}
|
|
|
24189
24571
|
}
|
|
24190
24572
|
const mdDur = Date.now() - callStart;
|
|
24191
24573
|
const mdBytes = env.markdown.length;
|
|
24192
|
-
|
|
24574
|
+
telemetry2.captureToolCall({
|
|
24193
24575
|
tool: name,
|
|
24194
24576
|
ok: true,
|
|
24195
24577
|
duration_ms: mdDur,
|
|
@@ -24198,7 +24580,7 @@ ${url}
|
|
|
24198
24580
|
triggered_by
|
|
24199
24581
|
});
|
|
24200
24582
|
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24201
|
-
|
|
24583
|
+
telemetry2.captureCompositeCall({
|
|
24202
24584
|
tool: name,
|
|
24203
24585
|
last_prompt: triggered_by ?? "",
|
|
24204
24586
|
ok: true,
|
|
@@ -24208,7 +24590,7 @@ ${url}
|
|
|
24208
24590
|
captureAgentMemoryTelemetry(name, env.structured);
|
|
24209
24591
|
captureFrictionTelemetry(name, env.structured);
|
|
24210
24592
|
if (name === "leadbay_create_topup_link" && typeof env.structured?.url === "string") {
|
|
24211
|
-
|
|
24593
|
+
telemetry2.captureTopupLink({ tool: name });
|
|
24212
24594
|
}
|
|
24213
24595
|
if (DEBUG_ON) {
|
|
24214
24596
|
process.stderr.write(
|
|
@@ -24229,7 +24611,7 @@ ${url}
|
|
|
24229
24611
|
const okText = response.content[0]?.text ?? "";
|
|
24230
24612
|
const okBytes = typeof okText === "string" ? okText.length : 0;
|
|
24231
24613
|
const okDur = Date.now() - callStart;
|
|
24232
|
-
|
|
24614
|
+
telemetry2.captureToolCall({
|
|
24233
24615
|
tool: name,
|
|
24234
24616
|
ok: true,
|
|
24235
24617
|
duration_ms: okDur,
|
|
@@ -24238,7 +24620,7 @@ ${url}
|
|
|
24238
24620
|
triggered_by
|
|
24239
24621
|
});
|
|
24240
24622
|
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24241
|
-
|
|
24623
|
+
telemetry2.captureCompositeCall({
|
|
24242
24624
|
tool: name,
|
|
24243
24625
|
last_prompt: triggered_by ?? "",
|
|
24244
24626
|
ok: true,
|
|
@@ -24248,7 +24630,7 @@ ${url}
|
|
|
24248
24630
|
captureAgentMemoryTelemetry(name, result);
|
|
24249
24631
|
captureFrictionTelemetry(name, result);
|
|
24250
24632
|
if (name === "leadbay_create_topup_link" && typeof result?.url === "string") {
|
|
24251
|
-
|
|
24633
|
+
telemetry2.captureTopupLink({ tool: name });
|
|
24252
24634
|
}
|
|
24253
24635
|
if (DEBUG_ON) {
|
|
24254
24636
|
process.stderr.write(
|
|
@@ -24263,14 +24645,14 @@ ${url}
|
|
|
24263
24645
|
const code = err?.code ?? err?.name ?? "Error";
|
|
24264
24646
|
if (isLeadbayBusinessError(err)) {
|
|
24265
24647
|
if (err.code === "QUOTA_EXCEEDED") {
|
|
24266
|
-
|
|
24648
|
+
telemetry2.captureQuotaHit({
|
|
24267
24649
|
tool: name,
|
|
24268
24650
|
retry_after_s: err._meta?.retry_after,
|
|
24269
24651
|
endpoint: err._meta?.endpoint
|
|
24270
24652
|
});
|
|
24271
24653
|
}
|
|
24272
24654
|
const httpStatus2 = err._meta?.http_status;
|
|
24273
|
-
|
|
24655
|
+
telemetry2.captureToolCall({
|
|
24274
24656
|
tool: name,
|
|
24275
24657
|
ok: false,
|
|
24276
24658
|
duration_ms: errDur,
|
|
@@ -24281,7 +24663,7 @@ ${url}
|
|
|
24281
24663
|
triggered_by
|
|
24282
24664
|
});
|
|
24283
24665
|
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24284
|
-
|
|
24666
|
+
telemetry2.captureCompositeCall({
|
|
24285
24667
|
tool: name,
|
|
24286
24668
|
last_prompt: triggered_by ?? "",
|
|
24287
24669
|
ok: false,
|
|
@@ -24290,15 +24672,15 @@ ${url}
|
|
|
24290
24672
|
...typeof httpStatus2 === "number" ? { http_status: httpStatus2 } : {}
|
|
24291
24673
|
});
|
|
24292
24674
|
}
|
|
24293
|
-
|
|
24675
|
+
telemetry2.captureException(err, buildBusinessCtx(name, err, triggered_by));
|
|
24294
24676
|
} else {
|
|
24295
|
-
|
|
24677
|
+
telemetry2.captureException(err, {
|
|
24296
24678
|
tool: name,
|
|
24297
24679
|
source: "unexpected",
|
|
24298
24680
|
message: typeof err?.message === "string" ? err.message : void 0,
|
|
24299
24681
|
triggered_by
|
|
24300
24682
|
});
|
|
24301
|
-
|
|
24683
|
+
telemetry2.captureToolCall({
|
|
24302
24684
|
tool: name,
|
|
24303
24685
|
ok: false,
|
|
24304
24686
|
duration_ms: errDur,
|
|
@@ -24308,7 +24690,7 @@ ${url}
|
|
|
24308
24690
|
triggered_by
|
|
24309
24691
|
});
|
|
24310
24692
|
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24311
|
-
|
|
24693
|
+
telemetry2.captureCompositeCall({
|
|
24312
24694
|
tool: name,
|
|
24313
24695
|
last_prompt: triggered_by ?? "",
|
|
24314
24696
|
ok: false,
|
|
@@ -24358,7 +24740,7 @@ function makeBrokenClient(stubError, region) {
|
|
|
24358
24740
|
|
|
24359
24741
|
// src/auth-http.ts
|
|
24360
24742
|
async function resolveClientFromToken(token, opts = {}) {
|
|
24361
|
-
const { region, baseUrl, logger } = opts;
|
|
24743
|
+
const { region, baseUrl, logger: logger2 } = opts;
|
|
24362
24744
|
if (!token || token.length === 0) {
|
|
24363
24745
|
const fallbackRegion = region === "fr" ? "fr" : "us";
|
|
24364
24746
|
return {
|
|
@@ -24380,10 +24762,10 @@ async function resolveClientFromToken(token, opts = {}) {
|
|
|
24380
24762
|
if (region) config.region = region;
|
|
24381
24763
|
return { client: createClient(config), authState: "ok" };
|
|
24382
24764
|
}
|
|
24383
|
-
|
|
24765
|
+
logger2?.info?.("hosted MCP: region unpinned, probing api-us + api-fr in parallel");
|
|
24384
24766
|
const probe = async (r) => {
|
|
24385
24767
|
const c = createClient({ token, region: r });
|
|
24386
|
-
await c.
|
|
24768
|
+
await c.resolveMe();
|
|
24387
24769
|
return c;
|
|
24388
24770
|
};
|
|
24389
24771
|
try {
|
|
@@ -24448,9 +24830,63 @@ function parseWriteEnv(env = process.env) {
|
|
|
24448
24830
|
}
|
|
24449
24831
|
|
|
24450
24832
|
// src/http-server.ts
|
|
24451
|
-
var VERSION = true ? "0.24.
|
|
24833
|
+
var VERSION = true ? "0.24.1" : "0.0.0-dev";
|
|
24452
24834
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
24453
24835
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
24836
|
+
var logger = {
|
|
24837
|
+
info: (m) => process.stderr.write(`[leadbay-mcp-http info] ${m}
|
|
24838
|
+
`),
|
|
24839
|
+
warn: (m) => process.stderr.write(`[leadbay-mcp-http warn] ${m}
|
|
24840
|
+
`),
|
|
24841
|
+
error: (m) => process.stderr.write(`[leadbay-mcp-http error] ${m}
|
|
24842
|
+
`)
|
|
24843
|
+
};
|
|
24844
|
+
var telemetry = initTelemetry({ version: VERSION, logger });
|
|
24845
|
+
var IDENTITY_RESOLVE_TIMEOUT_MS = 1500;
|
|
24846
|
+
async function resolveIdentity(client) {
|
|
24847
|
+
const region = client.region;
|
|
24848
|
+
try {
|
|
24849
|
+
const me = await Promise.race([
|
|
24850
|
+
client.resolveMe(),
|
|
24851
|
+
new Promise(
|
|
24852
|
+
(resolve) => setTimeout(() => resolve(null), IDENTITY_RESOLVE_TIMEOUT_MS)
|
|
24853
|
+
)
|
|
24854
|
+
]);
|
|
24855
|
+
if (!me) return { distinctId: "mcp:unknown", region };
|
|
24856
|
+
const distinctId = me.email ?? (me.id ? `mcp:user-${me.id}` : "mcp:unknown");
|
|
24857
|
+
return {
|
|
24858
|
+
distinctId,
|
|
24859
|
+
groups: me.organization?.id ? { organization: me.organization.id } : void 0,
|
|
24860
|
+
region,
|
|
24861
|
+
// name/email so leadbay_send_feedback attributes correctly on HTTP — the
|
|
24862
|
+
// module-scoped `me` is never populated here (Codex P2).
|
|
24863
|
+
...me.name ? { name: me.name } : {},
|
|
24864
|
+
...me.email ? { email: me.email } : {}
|
|
24865
|
+
};
|
|
24866
|
+
} catch (err) {
|
|
24867
|
+
logger.warn?.(`telemetry identity resolve failed: ${err?.message ?? err}`);
|
|
24868
|
+
return { distinctId: "mcp:unknown", region };
|
|
24869
|
+
}
|
|
24870
|
+
}
|
|
24871
|
+
function bindTelemetryIdentity(base, identity) {
|
|
24872
|
+
return {
|
|
24873
|
+
...base,
|
|
24874
|
+
captureToolCall: (p) => base.captureToolCall(p, identity),
|
|
24875
|
+
captureCompositeCall: (p) => base.captureCompositeCall(p, identity),
|
|
24876
|
+
captureQuotaHit: (p) => base.captureQuotaHit(p, identity),
|
|
24877
|
+
captureTopupLink: (p) => base.captureTopupLink(p, identity),
|
|
24878
|
+
captureStartup: (p) => base.captureStartup(p, identity),
|
|
24879
|
+
captureAgentMemoryCaptured: (p) => base.captureAgentMemoryCaptured(p, identity),
|
|
24880
|
+
captureAgentMemoryRecalled: (p) => base.captureAgentMemoryRecalled(p, identity),
|
|
24881
|
+
captureAgentMemoryPruned: (p) => base.captureAgentMemoryPruned(p, identity),
|
|
24882
|
+
captureFrictionReported: (p) => base.captureFrictionReported(p, identity),
|
|
24883
|
+
captureFeedback: (message, opts) => base.captureFeedback(message, opts, identity),
|
|
24884
|
+
identify: async () => {
|
|
24885
|
+
},
|
|
24886
|
+
shutdown: async () => {
|
|
24887
|
+
}
|
|
24888
|
+
};
|
|
24889
|
+
}
|
|
24454
24890
|
var sseSessions = /* @__PURE__ */ new Map();
|
|
24455
24891
|
var SSE_SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
24456
24892
|
setInterval(() => {
|
|
@@ -24470,10 +24906,16 @@ function extractBearer(authHeader) {
|
|
|
24470
24906
|
const m = /^Bearer\s+(.+)$/i.exec(authHeader);
|
|
24471
24907
|
return m ? m[1].trim() : void 0;
|
|
24472
24908
|
}
|
|
24473
|
-
function buildServerFromClient(client) {
|
|
24909
|
+
function buildServerFromClient(client, requestTelemetry) {
|
|
24474
24910
|
const includeWrite = parseWriteEnv();
|
|
24475
24911
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
24476
|
-
return buildServer(client, {
|
|
24912
|
+
return buildServer(client, {
|
|
24913
|
+
version: VERSION,
|
|
24914
|
+
includeWrite,
|
|
24915
|
+
includeAdvanced,
|
|
24916
|
+
logger,
|
|
24917
|
+
telemetry: requestTelemetry
|
|
24918
|
+
});
|
|
24477
24919
|
}
|
|
24478
24920
|
var PRM_PREFIX = "/.well-known/oauth-protected-resource";
|
|
24479
24921
|
var RESOURCE_PATHS = ["/mcp", "/fr/mcp", "/sse", "/fr/sse"];
|
|
@@ -24549,11 +24991,15 @@ async function handleStreamable(c, resourcePath) {
|
|
|
24549
24991
|
const foreign = rejectForeignOrigin(c);
|
|
24550
24992
|
if (foreign) return foreign;
|
|
24551
24993
|
const token = extractBearer(c.req.header("authorization"));
|
|
24552
|
-
const resolved = await resolveClientFromToken(token);
|
|
24994
|
+
const resolved = await resolveClientFromToken(token, { logger });
|
|
24553
24995
|
if (resolved.authState === "missing" || resolved.authState === "expired") {
|
|
24554
24996
|
return sendChallenge(c, resourcePath, resolved.authState);
|
|
24555
24997
|
}
|
|
24556
|
-
const
|
|
24998
|
+
const identity = await resolveIdentity(resolved.client);
|
|
24999
|
+
const server = buildServerFromClient(
|
|
25000
|
+
resolved.client,
|
|
25001
|
+
bindTelemetryIdentity(telemetry, identity)
|
|
25002
|
+
);
|
|
24557
25003
|
const transport = new StreamableHTTPServerTransport({
|
|
24558
25004
|
sessionIdGenerator: void 0,
|
|
24559
25005
|
// Return JSON responses instead of SSE so non-SSE clients (e.g. Codex) work.
|
|
@@ -24596,13 +25042,17 @@ async function handleSse(c, resourcePath) {
|
|
|
24596
25042
|
const foreign = rejectForeignOrigin(c);
|
|
24597
25043
|
if (foreign) return foreign;
|
|
24598
25044
|
const token = extractBearer(c.req.header("authorization"));
|
|
24599
|
-
const resolved = await resolveClientFromToken(token);
|
|
25045
|
+
const resolved = await resolveClientFromToken(token, { logger });
|
|
24600
25046
|
if (resolved.authState === "missing" || resolved.authState === "expired") {
|
|
24601
25047
|
return sendChallenge(c, resourcePath, resolved.authState);
|
|
24602
25048
|
}
|
|
25049
|
+
const identity = await resolveIdentity(resolved.client);
|
|
24603
25050
|
const env = c.env;
|
|
24604
25051
|
const transport = new SSEServerTransport("/messages", env.outgoing);
|
|
24605
|
-
const server = buildServerFromClient(
|
|
25052
|
+
const server = buildServerFromClient(
|
|
25053
|
+
resolved.client,
|
|
25054
|
+
bindTelemetryIdentity(telemetry, identity)
|
|
25055
|
+
);
|
|
24606
25056
|
await server.connect(transport);
|
|
24607
25057
|
const sessionId = transport.sessionId;
|
|
24608
25058
|
sseSessions.set(sessionId, { transport, server, createdAt: Date.now() });
|
|
@@ -24649,8 +25099,23 @@ if (isEntrypoint) {
|
|
|
24649
25099
|
`leadbay-mcp-http ${VERSION} listening on http://${info.address}:${info.port} (boot=${_boot})
|
|
24650
25100
|
`
|
|
24651
25101
|
);
|
|
25102
|
+
telemetry.captureStartup(
|
|
25103
|
+
{ auth_state: "ok", region: "unknown" },
|
|
25104
|
+
{ distinctId: "mcp:http-server", region: "unknown" }
|
|
25105
|
+
);
|
|
24652
25106
|
});
|
|
25107
|
+
const gracefulShutdown = async () => {
|
|
25108
|
+
try {
|
|
25109
|
+
await telemetry.shutdown();
|
|
25110
|
+
} finally {
|
|
25111
|
+
process.exit(0);
|
|
25112
|
+
}
|
|
25113
|
+
};
|
|
25114
|
+
process.once("SIGTERM", () => void gracefulShutdown());
|
|
25115
|
+
process.once("SIGINT", () => void gracefulShutdown());
|
|
24653
25116
|
}
|
|
24654
25117
|
export {
|
|
24655
|
-
app
|
|
25118
|
+
app,
|
|
25119
|
+
bindTelemetryIdentity,
|
|
25120
|
+
resolveIdentity
|
|
24656
25121
|
};
|