@leadbay/mcp 0.21.0 → 0.21.2

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.
@@ -606,6 +606,49 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` (the
606
606
 
607
607
  You don't need to memorize every tool here \u2014 each tool's own description carries a RENDERING block (how to present the response) and a NEXT STEPS block (observation \u2192 suggestion table). Read the relevant tool's description in full when the user picks an entry point. This overview just gets you to the right starting tool.
608
608
 
609
+ ## Proposing a next step \u2014 only when it genuinely helps
610
+
611
+ After reporting account state, you MAY propose a concrete next step \u2014 but only when one is genuinely useful, not by reflex. A reflexive "want me to also\u2026?" on every turn is noise; the user notices and it erodes trust.
612
+
613
+ **Propose a next step when** the overview surfaced an obvious unfinished thread or a blocker the user would want resolved \u2014 a fresh discovery batch waiting, follow-ups due today, or a quota/auth blocker with a specific unblock action. In those cases the next move is real and worth offering.
614
+
615
+ **Skip it when** there's no clear unfinished thread, the user only wanted the status (a bare "where do I stand?"), or the work they asked for is plainly done. A status read that ends cleanly is a complete answer \u2014 don't manufacture a next step just to have one.
616
+
617
+ **Lean on memory.** Check the \`_meta.agent_memory.summary\` for prior signal on how this user reacts to next-step offers. If the memory shows they routinely dismiss them, default to NOT proposing (let them ask). If they routinely act on them, lean toward proposing. When the user dismisses or accepts a proposal this turn, that's a material signal \u2014 call \`leadbay_agent_memory_capture\` (\`source:"inferred"\`, low confidence) so the preference compounds across sessions.
618
+
619
+ **When you do propose, the proposal IS a native choice dialog \u2014 never a prose "let me know if\u2026".** Route 2\u20134 mutually-exclusive next moves into your host's next-step widget (\`ask_user_input_v0\` on Claude chat / ChatGPT, \`AskUserQuestion\` on Claude cowork / Claude Code). The widget is the question; do not also list the same options as prose.
620
+
621
+ **ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
622
+
623
+ **If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
624
+
625
+ **One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
626
+ - Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
627
+ - Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
628
+
629
+ Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
630
+ - \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
631
+ - \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
632
+
633
+ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
634
+
635
+ ---
636
+
637
+
638
+
639
+ The overview itself returns no \`next_steps\` object, so when you DO propose, build the options from this table \u2014 pick the 2\u20134 rows that match what the account state actually showed. If none apply cleanly, propose none (the status read was complete) rather than inventing an option.
640
+
641
+ All \`Calls\` below are agent-callable \`leadbay_*\` tools (never an MCP prompt name like \`leadbay_daily_check_in\` \u2014 the agent cannot invoke a prompt from a turn; route to the underlying tool instead).
642
+
643
+ | Observation | Suggest | Calls |
644
+ |---------------------------------------------------------------------|--------------------------------------------------------|--------------------------------------------------------------|
645
+ | Fresh discovery batch waiting / user wants new leads | "See today's best new leads" | leadbay_pull_leads(lensId = pinned) |
646
+ | Follow-ups due / known leads to re-engage | "Show follow-ups due now" | leadbay_pull_followups |
647
+ | Quota/credit read shows low or exhausted balance | "Review what's eating your quota" | leadbay_account_status (deeper read) |
648
+ | Auth/connection blocker (e.g. 401 / AUTH_EXPIRED on a read) | "Reconnect Leadbay to unblock actions" | (guide the user to re-authenticate \u2014 no tool call) |
649
+ | Lens audience looks mismatched (batch is off-ICP) | "Adjust the lens audience to match your ICP" | ASK first \u2014 collect the target sectors / sizes / exclusions, THEN leadbay_adjust_audience(...) with those params. NEVER call it with no args (an empty call writes the current filter / may clone the default lens \u2014 a no-op or unwanted change). |
650
+ | Status is healthy and nothing is pending | propose nothing \u2014 the overview is a complete answer | \u2014 |
651
+
609
652
  GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
610
653
 
611
654
  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.
@@ -11524,6 +11567,22 @@ function coerceCell(client, v, path) {
11524
11567
  return String(v);
11525
11568
  throw client.makeError("IMPORT_INVALID_CELL_TYPE", `Cell at ${path} is ${Array.isArray(v) ? "an array" : typeof v}, expected string|number|boolean|null`, `Convert the value to a string before passing.`, "POST /imports");
11526
11569
  }
11570
+ var LEAD_STATUSES = [
11571
+ "DEFAULT",
11572
+ "INBOUND",
11573
+ "UNWANTED",
11574
+ "WANTED",
11575
+ "LOST",
11576
+ "WON"
11577
+ ];
11578
+ var LEAD_STATUS_SET = new Set(LEAD_STATUSES);
11579
+ function enforceLeadStatus(client, raw, path) {
11580
+ const canonical = String(raw).trim().toUpperCase();
11581
+ if (!LEAD_STATUS_SET.has(canonical)) {
11582
+ throw client.makeError("IMPORT_INVALID_STATUS", `${path} ${JSON.stringify(raw)} is not a valid lead status`, `Use one of ${LEAD_STATUSES.join(", ")} (case-insensitive), or omit it.`, "POST /imports");
11583
+ }
11584
+ return canonical;
11585
+ }
11527
11586
  function prepareDomainsMode(client, inputs) {
11528
11587
  const validInputs = [];
11529
11588
  const malformedDomains = [];
@@ -11645,6 +11704,12 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
11645
11704
  if (normDomain && !byDomain.has(normDomain))
11646
11705
  byDomain.set(normDomain, idx);
11647
11706
  });
11707
+ const statuses = {};
11708
+ for (const [cell, status] of Object.entries(mappings.statuses ?? {})) {
11709
+ statuses[cell] = enforceLeadStatus(client, status, `mappings.statuses[${JSON.stringify(cell)}]`);
11710
+ }
11711
+ const rawDefault = mappings.default_status;
11712
+ const default_status = rawDefault == null || String(rawDefault).trim() === "" ? null : enforceLeadStatus(client, rawDefault, "mappings.default_status");
11648
11713
  return {
11649
11714
  mode: "records",
11650
11715
  validInputs,
@@ -11654,8 +11719,8 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
11654
11719
  header,
11655
11720
  mappings: {
11656
11721
  fields: { ...normalizedFields },
11657
- statuses: mappings.statuses ?? {},
11658
- default_status: mappings.default_status ?? null
11722
+ statuses,
11723
+ default_status
11659
11724
  }
11660
11725
  };
11661
11726
  }
@@ -12048,11 +12113,12 @@ var importLeads = {
12048
12113
  },
12049
12114
  statuses: {
12050
12115
  type: "object",
12051
- description: "Optional status string mapping (rarely needed). Defaults to {}."
12116
+ description: `Optional map of raw CSV status-cell text \u2192 lead status (rarely needed). Keys are the verbatim cell strings; values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to {}.`
12052
12117
  },
12053
12118
  default_status: {
12054
12119
  type: ["string", "null"],
12055
- description: "Optional default status. Defaults to null."
12120
+ enum: [...LEAD_STATUSES, null],
12121
+ description: `Optional default lead status applied to rows without an explicit status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to null.`
12056
12122
  }
12057
12123
  },
12058
12124
  required: ["fields"]
@@ -17627,8 +17693,15 @@ var importAndQualify = {
17627
17693
  type: "object",
17628
17694
  description: "Ergonomic shorthand: `{CsvColumn: <number-id>}` or `{CsvColumn: '<field-name>'}` for custom-field mappings. Resolved against /crm/custom_fields catalog."
17629
17695
  },
17630
- statuses: { type: "object", description: "Optional status string mapping." },
17631
- default_status: { type: ["string", "null"], description: "Optional default status." }
17696
+ statuses: {
17697
+ type: "object",
17698
+ description: `Optional map of raw CSV status-cell text \u2192 lead status. Values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive); keys are the verbatim cell strings.`
17699
+ },
17700
+ default_status: {
17701
+ type: ["string", "null"],
17702
+ enum: [...LEAD_STATUSES, null],
17703
+ description: `Optional default lead status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive).`
17704
+ }
17632
17705
  },
17633
17706
  // mappings has a closed shape (fields/custom_fields/statuses/default_status).
17634
17707
  // Inner objects (fields, custom_fields, statuses) keep open shapes
@@ -22095,6 +22168,56 @@ function buildServer(client, opts = {}) {
22095
22168
  };
22096
22169
  };
22097
22170
  try {
22171
+ const bootstrapState = opts.bootstrapStatus?.() ?? { done: true };
22172
+ if (!bootstrapState.done) {
22173
+ const url = bootstrapState.signInUrl;
22174
+ const envelope = bootstrapState.failureMessage ? {
22175
+ error: true,
22176
+ code: "AUTH_FAILED",
22177
+ message: "Couldn't sign you in to Leadbay.",
22178
+ hint: `Sign-in failed: ${bootstrapState.failureMessage}
22179
+
22180
+ Restart the Leadbay extension in Claude Desktop to retry. If it keeps failing, check your network/region and that Leadbay is reachable.`
22181
+ } : url ? {
22182
+ // Prefer surfacing the live sign-in URL — the spawned MCP process
22183
+ // often can't open a GUI browser itself (no DISPLAY / sanitized
22184
+ // env), so a clickable link the agent renders is the reliable path.
22185
+ error: true,
22186
+ code: "AUTH_REQUIRED",
22187
+ message: "Sign in to Leadbay to finish connecting.",
22188
+ hint: `Open this link to authorize Leadbay, then re-run this tool:
22189
+
22190
+ ${url}
22191
+
22192
+ ` + (bootstrapState.openFailed ? "(The extension couldn't open your browser automatically.)" : "(A browser may have opened automatically \u2014 if not, use the link above.)")
22193
+ } : {
22194
+ error: true,
22195
+ code: "AUTH_PENDING",
22196
+ message: "Signing you in to Leadbay \u2014 a browser window should have opened. Authorize there, then try again.",
22197
+ hint: "Complete the Leadbay sign-in in your browser, then re-run this tool."
22198
+ };
22199
+ const pendingText = formatErrorForLLM(envelope);
22200
+ const pendingDur = Date.now() - callStart;
22201
+ telemetry.captureToolCall({
22202
+ tool: name,
22203
+ ok: false,
22204
+ duration_ms: pendingDur,
22205
+ format: "error-envelope",
22206
+ bytes: pendingText.length,
22207
+ error_code: envelope.code,
22208
+ triggered_by
22209
+ });
22210
+ if (DEBUG_ON) {
22211
+ process.stderr.write(
22212
+ `[leadbay-mcp debug] tool=${name} dur=${pendingDur}ms ok=false code=${envelope.code} (auth-bootstrap, no-sentry)
22213
+ `
22214
+ );
22215
+ }
22216
+ return {
22217
+ content: [{ type: "text", text: pendingText }],
22218
+ isError: true
22219
+ };
22220
+ }
22098
22221
  if (COMPOSITE_FILE_TOOL_NAMES.has(name) && !triggered_by) {
22099
22222
  const envelope = {
22100
22223
  error: true,
@@ -22461,7 +22584,7 @@ function parseWriteEnv(env = process.env) {
22461
22584
  }
22462
22585
 
22463
22586
  // src/http-server.ts
22464
- var VERSION = true ? "0.21.0" : "0.0.0-dev";
22587
+ var VERSION = true ? "0.21.2" : "0.0.0-dev";
22465
22588
  var PORT = Number(process.env.PORT ?? 8080);
22466
22589
  var HOST = process.env.HOST ?? "0.0.0.0";
22467
22590
  var sseSessions = /* @__PURE__ */ new Map();