@leadbay/mcp 0.2.1 → 0.3.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/dist/bin.js CHANGED
@@ -3,12 +3,16 @@ import {
3
3
  compositeReadTools,
4
4
  compositeWriteTools,
5
5
  createClient,
6
+ createDefaultBulkStore,
7
+ formatLoginError,
6
8
  granularReadTools,
7
9
  granularWriteTools,
8
10
  resolveRegion
9
- } from "./chunk-BGJ6JWIO.js";
11
+ } from "./chunk-NED7ATJI.js";
10
12
 
11
13
  // src/bin.ts
14
+ import { realpathSync } from "fs";
15
+ import { fileURLToPath } from "url";
12
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
17
 
14
18
  // src/server.ts
@@ -17,7 +21,44 @@ import {
17
21
  CallToolRequestSchema,
18
22
  ListToolsRequestSchema
19
23
  } from "@modelcontextprotocol/sdk/types.js";
20
- var SERVER_INSTRUCTIONS = "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.\n\nStart with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead to dig into one lead deeply (qualification answers, signals, contacts). When the user wants more leads, narrower audience, refined criteria, or contact enrichment, use the matching composite tool (bulk_qualify_leads / adjust_audience / refine_prompt / enrich_titles) \u2014 they hide lens permissions, region routing, polling, and selection state from you.";
24
+ 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.";
25
+ 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.";
26
+ function buildScoringParagraph(has) {
27
+ 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.";
28
+ const deepenTools = [];
29
+ if (has("leadbay_bulk_qualify_leads")) deepenTools.push("leadbay_bulk_qualify_leads for deeper qualification");
30
+ if (has("leadbay_enrich_titles")) deepenTools.push("leadbay_enrich_titles for contacts");
31
+ if (deepenTools.length > 0) {
32
+ return base + ` Call ${deepenTools.join(" or ")} on any lead that looks worth it.`;
33
+ }
34
+ return base;
35
+ }
36
+ function buildStartHereParagraph(has) {
37
+ 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 to dig into one lead deeply (qualification answers, signals, contacts).";
38
+ const compositeNames = ["bulk_qualify_leads", "adjust_audience", "refine_prompt", "enrich_titles"].filter((n) => has(`leadbay_${n}`));
39
+ if (compositeNames.length > 0) {
40
+ 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.`;
41
+ }
42
+ return base + " When the user asks for refinement, contact enrichment, audience changes, or outreach reporting, tell them: those actions require write tools, currently disabled. Re-enable by removing `LEADBAY_MCP_WRITE=0` from your MCP client config and restarting the client. Also: do not promise to log outreach \u2014 the report_outreach tool is not available in this configuration.";
43
+ }
44
+ function buildRhythmParagraph(has) {
45
+ if (has("leadbay_report_outreach")) {
46
+ return "Suggested rhythm: a healthy agent pattern is a daily check-in \u2014 pull fresh leads, skim the auto-qualified top, deepen 1-3 promising ones, propose outreach to the user, then leadbay_report_outreach on what actually got sent. If your host supports scheduling, offer to set up a daily run.";
47
+ }
48
+ return "Suggested rhythm: a healthy agent pattern is a daily check-in \u2014 pull fresh leads, skim the auto-qualified top, deepen 1-3 promising ones, propose outreach to the user. If your host supports scheduling, offer to set up a daily run.";
49
+ }
50
+ function buildServerInstructions(exposed) {
51
+ const has = (name) => exposed.has(name);
52
+ const parts = [];
53
+ if (has("leadbay_report_outreach")) {
54
+ parts.push(VERIFICATION_MANDATE);
55
+ }
56
+ parts.push(MENTAL_MODEL_PARAGRAPH);
57
+ parts.push(buildScoringParagraph(has));
58
+ parts.push(buildStartHereParagraph(has));
59
+ parts.push(buildRhythmParagraph(has));
60
+ return parts.join("\n\n");
61
+ }
21
62
  function formatErrorForLLM(err) {
22
63
  if (err && typeof err === "object" && err.error === true) {
23
64
  const parts = [`${err.message}.`, err.hint];
@@ -42,13 +83,6 @@ function toolsListPayload(tools) {
42
83
  }));
43
84
  }
44
85
  function buildServer(client, opts = {}) {
45
- const server = new Server(
46
- { name: "leadbay", version: "0.2.0" },
47
- {
48
- capabilities: { tools: {} },
49
- instructions: SERVER_INSTRUCTIONS
50
- }
51
- );
52
86
  const exposedTools = [];
53
87
  exposedTools.push(...compositeReadTools);
54
88
  if (opts.includeWrite) {
@@ -66,6 +100,14 @@ function buildServer(client, opts = {}) {
66
100
  toolByName.set(t.name, t);
67
101
  }
68
102
  }
103
+ const exposedNames = new Set(toolByName.keys());
104
+ const server = new Server(
105
+ { name: "leadbay", version: "0.3.0" },
106
+ {
107
+ capabilities: { tools: {} },
108
+ instructions: buildServerInstructions(exposedNames)
109
+ }
110
+ );
69
111
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
70
112
  tools: toolsListPayload([...toolByName.values()])
71
113
  }));
@@ -85,7 +127,10 @@ function buildServer(client, opts = {}) {
85
127
  }
86
128
  const args = req.params.arguments ?? {};
87
129
  try {
88
- const result = await tool.execute(client, args, { logger: opts.logger });
130
+ const result = await tool.execute(client, args, {
131
+ logger: opts.logger,
132
+ bulkTracker: opts.bulkTracker
133
+ });
89
134
  if (result && typeof result === "object" && result.error === true) {
90
135
  return {
91
136
  content: [
@@ -113,7 +158,7 @@ function buildServer(client, opts = {}) {
113
158
 
114
159
  // src/bin.ts
115
160
  import { createRequire } from "module";
116
- var VERSION = "0.2.1";
161
+ var VERSION = "0.3.0";
117
162
  var HELP = `
118
163
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
119
164
 
@@ -132,15 +177,17 @@ USAGE
132
177
  leadbay-mcp --help Print this help
133
178
 
134
179
  ENV VARS
135
- LEADBAY_TOKEN (required) Bearer token from https://app.leadbay.ai/settings/api-tokens
180
+ LEADBAY_TOKEN (required) Bearer token (run \`leadbay-mcp install\` to mint one).
136
181
  LEADBAY_REGION (optional) "us" or "fr". Auto-detected from /users/me if unset.
137
182
  LEADBAY_BASE_URL (optional) Override API base URL (for staging/dev).
138
183
  LEADBAY_MCP_ADVANCED (optional) Set to "1" to expose granular API tools alongside
139
184
  the composite workflow tools. Most users don't need this.
140
- LEADBAY_MCP_WRITE (optional) Set to "1" to expose write composites (refine_prompt,
141
- report_outreach, adjust_audience, etc.) and write granulars.
142
- Defaults off \u2014 read composites are exposed by default; mutations
143
- require explicit opt-in.
185
+ LEADBAY_MCP_WRITE (optional) Default "1" (ON) since 0.3.0: exposes write composites
186
+ (refine_prompt, report_outreach, adjust_audience, bulk_qualify_leads,
187
+ enrich_titles, answer_clarification, import_leads). Set to "0" /
188
+ "false" / "no" / "off" for read-only mode. Note: in 0.2.x, only
189
+ "1" turned writes ON; "true" / "yes" / "on" were treated as OFF.
190
+ The 0.3.0 parser accepts all those values as truthy. See MIGRATION.md.
144
191
  LEADBAY_MOCK (optional) Set to "1" to serve all responses from on-disk fixtures
145
192
  (no network, no real auth). Useful for agent-author dry-running.
146
193
  GETs are matched against fixture JSON files; POSTs/DELETEs are
@@ -153,7 +200,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
153
200
  "mcpServers": {
154
201
  "leadbay": {
155
202
  "command": "npx",
156
- "args": ["-y", "@leadbay/mcp@0.1"],
203
+ "args": ["-y", "@leadbay/mcp@0.3"],
157
204
  "env": {
158
205
  "LEADBAY_TOKEN": "lb_...",
159
206
  "LEADBAY_REGION": "us"
@@ -187,9 +234,21 @@ function parseLogLevel(raw) {
187
234
  if (raw === "debug" || raw === "info") return raw;
188
235
  return "error";
189
236
  }
237
+ function parseWriteEnv() {
238
+ const raw = process.env.LEADBAY_MCP_WRITE;
239
+ if (raw === void 0 || raw === "") return true;
240
+ const v = raw.trim().toLowerCase();
241
+ if (v === "0" || v === "false" || v === "no" || v === "off") return false;
242
+ if (v === "1" || v === "true" || v === "yes" || v === "on") return true;
243
+ process.stderr.write(
244
+ `[leadbay-mcp warn] LEADBAY_MCP_WRITE='${raw}' not recognized; defaulting to ON. Use 1/0.
245
+ `
246
+ );
247
+ return true;
248
+ }
190
249
  function exitWithTokenError() {
191
250
  process.stderr.write(
192
- "leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n 1. Create a token at https://app.leadbay.ai/settings/api-tokens\n 2. Set it in your MCP client config (e.g. claude_desktop_config.json).\n\nRun `leadbay-mcp --help` for the full config template.\n"
251
+ "leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n 1. Run: npx -y @leadbay/mcp install --email <you> --region <us|fr>\n 2. Set it in your MCP client config (e.g. claude_desktop_config.json).\n\nRun `leadbay-mcp --help` for the full config template.\n"
193
252
  );
194
253
  process.exit(1);
195
254
  }
@@ -293,11 +352,73 @@ function parseFlag(args, name) {
293
352
  function hasFlag(args, name) {
294
353
  return args.some((a) => a === `--${name}`);
295
354
  }
355
+ function resolveDefaultCredentialsPath() {
356
+ const fs = require_("node:fs");
357
+ const path = require_("node:path");
358
+ const legacyPath = path.join(require_("node:os").homedir(), ".leadbay-mcp.json");
359
+ if (fs.existsSync(legacyPath)) {
360
+ return { path: legacyPath, legacy: true };
361
+ }
362
+ return { path: computeFreshDefaultPath(), legacy: false };
363
+ }
364
+ function checkLoginCollision(existingConfig, email, region) {
365
+ if (!existingConfig || typeof existingConfig !== "object") {
366
+ return "existing file is not valid JSON";
367
+ }
368
+ const cfg = existingConfig;
369
+ const existingEmail = typeof cfg.email === "string" && cfg.email.length > 0 ? cfg.email : void 0;
370
+ const existingRegion = typeof cfg.mcpServers?.leadbay?.env?.LEADBAY_REGION === "string" ? cfg.mcpServers.leadbay.env.LEADBAY_REGION : void 0;
371
+ if (existingEmail !== void 0 && existingEmail !== email) {
372
+ return `existing email=${existingEmail} (this login is email=${email})`;
373
+ }
374
+ if (existingRegion !== void 0 && existingRegion !== region) {
375
+ return `existing region=${existingRegion} (this login is region=${region})`;
376
+ }
377
+ return null;
378
+ }
379
+ function computeFreshDefaultPath() {
380
+ const os = require_("node:os");
381
+ const path = require_("node:path");
382
+ const home = os.homedir();
383
+ const xdg = process.env.XDG_CONFIG_HOME;
384
+ if (xdg && xdg.length > 0) {
385
+ return path.join(xdg, "leadbay", "credentials.json");
386
+ }
387
+ if (process.platform === "darwin") {
388
+ return path.join(home, "Library", "Application Support", "leadbay", "credentials.json");
389
+ }
390
+ if (process.platform === "win32") {
391
+ const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
392
+ return path.join(appdata, "leadbay", "credentials.json");
393
+ }
394
+ return path.join(home, ".config", "leadbay", "credentials.json");
395
+ }
296
396
  async function runLogin(args) {
297
397
  const email = parseFlag(args, "email");
398
+ const defaultPathPreview = (() => {
399
+ try {
400
+ return resolveDefaultCredentialsPath().path;
401
+ } catch {
402
+ return "<HOME>/.config/leadbay/credentials.json";
403
+ }
404
+ })();
298
405
  if (!email) {
299
406
  process.stderr.write(
300
- "Usage: leadbay-mcp login --email you@example.com [--region us|fr] [--allow-region-fallback] [--write-config PATH] [--quiet]\n Then enter your password (hidden), or pipe it via stdin / set $LEADBAY_PASSWORD.\n --region Pin the backend (us|fr); avoids sending your password to a backend you don't use.\n Defaults to $LEADBAY_REGION if set; otherwise asks you to pass --allow-region-fallback.\n --allow-region-fallback Try us, then fr (or fr, then us). Your password hits BOTH backends if the\n first 401s. Only do this if you're OK with that.\n --write-config PATH Write the resulting MCP-client JSON to PATH with 0600 permissions instead\n of stdout. Recommended \u2014 keeps the token out of terminal scrollback / CI logs.\n --quiet With --write-config, suppress the printed Claude-Code one-liner that includes the token.\n"
407
+ `Usage: leadbay-mcp login --email you@example.com [--region us|fr] [--allow-region-fallback]
408
+ [--write-config PATH] [--unsafe-print-token] [--force] [--quiet]
409
+ Then enter your password (hidden), or pipe it via stdin / set $LEADBAY_PASSWORD.
410
+ --region Pin the backend (us|fr); avoids sending your password to a backend you don't use.
411
+ Defaults to $LEADBAY_REGION if set; otherwise asks you to pass --allow-region-fallback.
412
+ --allow-region-fallback Try us, then fr (or fr, then us). Your password hits BOTH backends if the
413
+ first 401s. Only do this if you're OK with that.
414
+ Default behavior (0.3.0+): writes the MCP-client JSON to the platform-correct credentials path:
415
+ ${defaultPathPreview} (mode 0600).
416
+ --write-config PATH Override the default path with PATH (mode 0600).
417
+ --unsafe-print-token Print the token to stdout (legacy 0.2.x behavior). Use only for CI flows that
418
+ scrape stdout. The token will end up in scrollback / logs.
419
+ --force Overwrite the credentials file even if it already contains a different token/region.
420
+ --quiet With --write-config / default file-write, suppress the printed Claude-Code one-liner.
421
+ `
301
422
  );
302
423
  return 2;
303
424
  }
@@ -326,7 +447,7 @@ async function runLogin(args) {
326
447
  let result;
327
448
  try {
328
449
  if (pinnedRegion && !allowFallback) {
329
- const { REGIONS } = await import("./dist-PIXZN6N4.js");
450
+ const { REGIONS } = await import("./dist-YMZYFHZK.js");
330
451
  const baseUrl = REGIONS[pinnedRegion];
331
452
  const c = createClient({ region: pinnedRegion });
332
453
  const token = await loginAt(baseUrl, email, password);
@@ -341,10 +462,11 @@ async function runLogin(args) {
341
462
  return 1;
342
463
  }
343
464
  const config = {
465
+ email,
344
466
  mcpServers: {
345
467
  leadbay: {
346
468
  command: "npx",
347
- args: ["-y", "@leadbay/mcp@0.2"],
469
+ args: ["-y", "@leadbay/mcp@0.3"],
348
470
  env: {
349
471
  LEADBAY_TOKEN: result.token,
350
472
  LEADBAY_REGION: result.region
@@ -354,61 +476,150 @@ async function runLogin(args) {
354
476
  };
355
477
  const writeConfigPath = parseFlag(args, "write-config");
356
478
  const quiet = hasFlag(args, "quiet");
479
+ const force = hasFlag(args, "force");
480
+ const unsafePrint = hasFlag(args, "unsafe-print-token");
481
+ const printTokenLegacy = hasFlag(args, "print-token");
482
+ if (printTokenLegacy && !unsafePrint) {
483
+ process.stderr.write(
484
+ "[leadbay-mcp warn] --print-token is deprecated since 0.3.0; renaming to --unsafe-print-token. The flag still works for one release.\n"
485
+ );
486
+ }
487
+ const printToStdout = unsafePrint || printTokenLegacy;
488
+ if (printToStdout) {
489
+ process.stderr.write(
490
+ `
491
+ Logged in to ${result.region.toUpperCase()} backend (${result.verified ? "verified" : "UNVERIFIED \u2014 check your email"}).
492
+
493
+ \u26A0\uFE0F About to print your bearer token to STDOUT.
494
+ Treat it like a password. Do NOT paste this into chat, screen-share, or commit it.
495
+ For safer handling, re-run without --unsafe-print-token (default writes a 0600 file).
496
+
497
+ Add this to your MCP client config:
498
+
499
+ `
500
+ );
501
+ process.stdout.write(JSON.stringify(config, null, 2) + "\n");
502
+ process.stderr.write(
503
+ `
504
+ Or for Claude Code (token included \u2014 same warning applies):
505
+
506
+ claude mcp add leadbay --scope user \\
507
+ --env LEADBAY_TOKEN=${result.token} \\
508
+ --env LEADBAY_REGION=${result.region} \\
509
+ -- npx -y @leadbay/mcp@0.3
510
+
511
+ Restart your MCP client to pick up the new server.
512
+ `
513
+ );
514
+ return 0;
515
+ }
516
+ let targetPath;
517
+ let usingLegacyPath = false;
357
518
  if (writeConfigPath) {
358
- const { writeFileSync, chmodSync } = await import("fs");
359
- writeFileSync(writeConfigPath, JSON.stringify(config, null, 2) + "\n", {
519
+ targetPath = writeConfigPath;
520
+ } else {
521
+ const resolved = resolveDefaultCredentialsPath();
522
+ targetPath = resolved.path;
523
+ usingLegacyPath = resolved.legacy;
524
+ }
525
+ try {
526
+ const { existsSync, readFileSync } = await import("fs");
527
+ if (existsSync(targetPath) && !force) {
528
+ let existing;
529
+ try {
530
+ existing = JSON.parse(readFileSync(targetPath, "utf8"));
531
+ } catch {
532
+ process.stderr.write(
533
+ `leadbay-mcp login: ${targetPath} exists but is not valid JSON. Pass --force to overwrite.
534
+ `
535
+ );
536
+ return 1;
537
+ }
538
+ const collision = checkLoginCollision(existing, email, result.region);
539
+ if (collision) {
540
+ process.stderr.write(
541
+ `leadbay-mcp login: refusing to overwrite ${targetPath} \u2014 ${collision}.
542
+ Pass --force to overwrite, or --write-config /some/other/path.json to keep both.
543
+ `
544
+ );
545
+ return 1;
546
+ }
547
+ }
548
+ } catch (err) {
549
+ process.stderr.write(`leadbay-mcp login: ${err?.message ?? String(err)}
550
+ `);
551
+ return 1;
552
+ }
553
+ let actualMode;
554
+ try {
555
+ const { writeFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } = await import("fs");
556
+ const { dirname } = await import("path");
557
+ mkdirSync(dirname(targetPath), { recursive: true });
558
+ const tmp = targetPath + ".tmp." + process.pid;
559
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", {
360
560
  encoding: "utf8",
361
561
  mode: 384
362
562
  });
363
563
  try {
364
- chmodSync(writeConfigPath, 384);
564
+ chmodSync(tmp, 384);
365
565
  } catch {
366
566
  }
367
- process.stderr.write(
368
- `
369
- Logged in to ${result.region.toUpperCase()} backend (${result.verified ? "verified" : "UNVERIFIED \u2014 check your email"}).
370
- Wrote MCP config to ${writeConfigPath} (mode 0600). Token NOT printed to terminal.
371
- `
372
- );
373
- if (!quiet) {
567
+ renameSync(tmp, targetPath);
568
+ try {
569
+ actualMode = statSync(targetPath).mode & 511;
570
+ } catch {
571
+ }
572
+ try {
573
+ unlinkSync(tmp);
574
+ } catch {
575
+ }
576
+ } catch (err) {
577
+ const code = err?.code;
578
+ if (code === "EACCES" || code === "EROFS" || code === "ENOENT") {
374
579
  process.stderr.write(
375
- `
376
- For Claude Code, run:
377
- claude mcp add leadbay --env LEADBAY_TOKEN=$(jq -r .mcpServers.leadbay.env.LEADBAY_TOKEN ${writeConfigPath}) --env LEADBAY_REGION=${result.region} -- npx -y @leadbay/mcp@0.2
580
+ `leadbay-mcp login: cannot write ${targetPath} (${code}).
581
+ Use --write-config /tmp/leadbay-mcp.json (or another writable path),
582
+ or --unsafe-print-token (last resort \u2014 token in stdout).
378
583
  `
379
584
  );
585
+ return 1;
380
586
  }
381
- process.stderr.write(
382
- `
383
- TREAT THE TOKEN AS A SECRET. It grants full access to your Leadbay account.
384
- Delete the config file once your MCP client has it loaded, or keep it 0600.
385
- `
386
- );
387
- return 0;
587
+ process.stderr.write(`leadbay-mcp login: ${err?.message ?? String(err)}
588
+ `);
589
+ return 1;
388
590
  }
591
+ const modeNote = actualMode === 384 ? "(mode 0600)" : actualMode !== void 0 ? `(mode 0${actualMode.toString(8)} \u2014 chmod 0600 failed; treat the file as sensitive)` : "(mode unknown)";
389
592
  process.stderr.write(
390
593
  `
391
594
  Logged in to ${result.region.toUpperCase()} backend (${result.verified ? "verified" : "UNVERIFIED \u2014 check your email"}).
392
-
393
- \u26A0\uFE0F About to print your bearer token to STDOUT.
394
- Treat it like a password. Do NOT paste this into chat, screen-share, or commit it.
395
- For safer handling, re-run with --write-config /path/to/config.json (writes 0600).
396
-
397
- Add this to your MCP client config:
398
-
595
+ Wrote MCP config to ${targetPath} ${modeNote}. Token NOT printed to terminal.
399
596
  `
400
597
  );
401
- process.stdout.write(JSON.stringify(config, null, 2) + "\n");
598
+ if (usingLegacyPath) {
599
+ const newPath = computeFreshDefaultPath();
600
+ process.stderr.write(
601
+ `
602
+ [leadbay-mcp note] Used the legacy 0.2.x path ${targetPath}. The 0.3.0 default is ${newPath}.
603
+ Move the file there at your convenience (no code change required \u2014 both paths are read).
604
+ `
605
+ );
606
+ }
607
+ if (!quiet) {
608
+ const quotedPath = `'${targetPath.replace(/'/g, `'\\''`)}'`;
609
+ process.stderr.write(
610
+ `
611
+ For Claude Code, run:
612
+ claude mcp add leadbay --scope user \\
613
+ --env LEADBAY_TOKEN=$(jq -r .mcpServers.leadbay.env.LEADBAY_TOKEN ${quotedPath}) \\
614
+ --env LEADBAY_REGION=${result.region} \\
615
+ -- npx -y @leadbay/mcp@0.3
616
+ `
617
+ );
618
+ }
402
619
  process.stderr.write(
403
620
  `
404
- Or for Claude Code (token included \u2014 same warning applies):
405
-
406
- claude mcp add leadbay \\
407
- --env LEADBAY_TOKEN=${result.token} \\
408
- --env LEADBAY_REGION=${result.region} \\
409
- -- npx -y @leadbay/mcp@0.2
410
-
411
- Restart your MCP client to pick up the new server.
621
+ TREAT THE TOKEN AS A SECRET. It grants full access to your Leadbay account.
622
+ Delete the config file once your MCP client has it loaded, or keep it 0600.
412
623
  `
413
624
  );
414
625
  return 0;
@@ -444,9 +655,7 @@ async function loginAt(baseUrl, email, password) {
444
655
  }
445
656
  }
446
657
  reject(
447
- new Error(
448
- `login failed (${res.statusCode}) at ${baseUrl}: ${raw.slice(0, 200)}`
449
- )
658
+ new Error(formatLoginError(res.statusCode ?? 0, raw, baseUrl))
450
659
  );
451
660
  });
452
661
  }
@@ -456,6 +665,31 @@ async function loginAt(baseUrl, email, password) {
456
665
  r.end();
457
666
  });
458
667
  }
668
+ function detectClaudeDesktopMode(claudeSupportDir) {
669
+ const { existsSync, readFileSync } = require_("node:fs");
670
+ const { join } = require_("node:path");
671
+ const markers = [];
672
+ const legacy = existsSync(join(claudeSupportDir, "claude_desktop_config.json"));
673
+ if (existsSync(join(claudeSupportDir, "Claude Extensions"))) {
674
+ markers.push("Claude Extensions/");
675
+ }
676
+ if (existsSync(join(claudeSupportDir, "extensions-installations.json"))) {
677
+ markers.push("extensions-installations.json");
678
+ }
679
+ const cfgPath = join(claudeSupportDir, "config.json");
680
+ if (existsSync(cfgPath)) {
681
+ try {
682
+ const raw = readFileSync(cfgPath, "utf8");
683
+ const parsed = JSON.parse(raw);
684
+ if (parsed && typeof parsed === "object") {
685
+ const hasDxtKey = Object.keys(parsed).some((k) => k.startsWith("dxt:"));
686
+ if (hasDxtKey) markers.push("config.json (dxt:* keys)");
687
+ }
688
+ } catch {
689
+ }
690
+ }
691
+ return { legacy, dxt: markers.length > 0, markers };
692
+ }
459
693
  async function detectClients() {
460
694
  const out = [];
461
695
  const { existsSync } = await import("fs");
@@ -476,9 +710,16 @@ async function detectClients() {
476
710
  out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
477
711
  }
478
712
  const home = os.homedir();
479
- const cdPath = process.platform === "win32" ? `${process.env.APPDATA ?? `${home}\\AppData\\Roaming`}\\Claude\\claude_desktop_config.json` : process.platform === "darwin" ? `${home}/Library/Application Support/Claude/claude_desktop_config.json` : `${home}/.config/Claude/claude_desktop_config.json`;
480
- if (existsSync(cdPath)) {
481
- out.push({ id: "claude-desktop", label: "Claude Desktop", detail: cdPath });
713
+ const claudeSupportDir = process.platform === "win32" ? `${process.env.APPDATA ?? `${home}\\AppData\\Roaming`}\\Claude` : process.platform === "darwin" ? `${home}/Library/Application Support/Claude` : `${home}/.config/Claude`;
714
+ const cdPath = process.platform === "win32" ? `${claudeSupportDir}\\claude_desktop_config.json` : `${claudeSupportDir}/claude_desktop_config.json`;
715
+ const mode = detectClaudeDesktopMode(claudeSupportDir);
716
+ if (mode.legacy || mode.dxt) {
717
+ out.push({
718
+ id: "claude-desktop",
719
+ label: "Claude Desktop",
720
+ detail: cdPath,
721
+ mode
722
+ });
482
723
  }
483
724
  const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
484
725
  if (existsSync(cursorPath)) {
@@ -533,19 +774,25 @@ async function readChoice(prompt, def = true) {
533
774
  process.stdin.on("data", onData);
534
775
  });
535
776
  }
536
- async function installInClaudeCode(token, region, includeWrite) {
537
- const cp = await import("child_process");
777
+ function buildClaudeCodeAddArgs(token, region, includeWrite) {
538
778
  const args = [
539
779
  "mcp",
540
780
  "add",
541
781
  "leadbay",
782
+ "--scope",
783
+ "user",
542
784
  "--env",
543
785
  `LEADBAY_TOKEN=${token}`,
544
786
  "--env",
545
787
  `LEADBAY_REGION=${region}`
546
788
  ];
547
- if (includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=1`);
548
- args.push("--", "npx", "-y", "@leadbay/mcp@0.2");
789
+ if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
790
+ args.push("--", "npx", "-y", "@leadbay/mcp@0.3");
791
+ return args;
792
+ }
793
+ async function installInClaudeCode(token, region, includeWrite) {
794
+ const cp = await import("child_process");
795
+ const args = buildClaudeCodeAddArgs(token, region, includeWrite);
549
796
  return await new Promise((resolve) => {
550
797
  const child = cp.spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
551
798
  let stderr = "";
@@ -585,10 +832,10 @@ async function installInJsonConfig(configPath, token, region, includeWrite) {
585
832
  LEADBAY_TOKEN: token,
586
833
  LEADBAY_REGION: region
587
834
  };
588
- if (includeWrite) env.LEADBAY_MCP_WRITE = "1";
835
+ if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
589
836
  parsed.mcpServers.leadbay = {
590
837
  command: "npx",
591
- args: ["-y", "@leadbay/mcp@0.2"],
838
+ args: ["-y", "@leadbay/mcp@0.3"],
592
839
  env
593
840
  };
594
841
  const tmp = configPath + ".tmp";
@@ -611,10 +858,15 @@ async function runInstall(args) {
611
858
  const email = parseFlag(args, "email");
612
859
  if (!email) {
613
860
  process.stderr.write(
614
- "Usage: leadbay-mcp install --email you@example.com [--region us|fr]\n [--allow-region-fallback] [--include-write]\n [--target claude-code,claude-desktop,cursor] [--yes]\n Mints a token AND registers the MCP server with your installed clients.\n --target Comma-separated subset; default = all detected.\n --include-write Enable LEADBAY_MCP_WRITE=1 (composite write tools \u2014 refine_prompt,\n report_outreach, adjust_audience). Off by default; off means the\n agent can read your Leadbay account but not mutate it.\n --yes Don't ask before installing into each detected client.\n"
861
+ "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"
615
862
  );
616
863
  return 2;
617
864
  }
865
+ if (hasFlag(args, "include-write")) {
866
+ process.stderr.write(
867
+ "[leadbay-mcp warn] --include-write is the default since 0.3.0; the flag is now a no-op.\n Composite write tools (refine_prompt, report_outreach, adjust_audience, etc.) are ON by default.\n Pass --no-write to install in read-only mode.\n\n"
868
+ );
869
+ }
618
870
  const regionArg = parseFlag(args, "region");
619
871
  const regionEnv = process.env.LEADBAY_REGION;
620
872
  const allowFallback = hasFlag(args, "allow-region-fallback");
@@ -657,9 +909,27 @@ async function runInstall(args) {
657
909
  leadbay-mcp install \u2014 detected MCP clients on this machine:
658
910
  `
659
911
  );
660
- for (const c of chosen) process.stderr.write(` \u2022 ${c.label.padEnd(16)} ${c.detail}
912
+ for (const c of chosen) {
913
+ const dxtSuffix = c.mode?.dxt ? " [DXT \u2014 legacy write will be skipped]" : "";
914
+ process.stderr.write(` \u2022 ${c.label.padEnd(16)} ${c.detail}${dxtSuffix}
661
915
  `);
916
+ }
662
917
  process.stderr.write("\n");
918
+ const forceLegacy = hasFlag(args, "force-legacy");
919
+ const hasDxtClient = chosen.some((c) => c.id === "claude-desktop" && c.mode?.dxt);
920
+ if (hasDxtClient && !forceLegacy) {
921
+ const dxtClient = chosen.find((c) => c.id === "claude-desktop" && c.mode?.dxt);
922
+ process.stderr.write(
923
+ `\u26A0\uFE0F Claude Desktop 2026 DXT detected (markers: ${dxtClient.mode.markers.join(", ")}).
924
+ The legacy claude_desktop_config.json is UI-prefs-only in this version \u2014
925
+ Claude Desktop will overwrite any \`mcpServers\` block written there.
926
+ Install the Leadbay .dxt instead (drag-drop into Settings \u2192 Extensions):
927
+ https://github.com/leadbay/leadclaw/releases/latest
928
+ Override with --force-legacy to write the legacy file anyway (not recommended).
929
+
930
+ `
931
+ );
932
+ }
663
933
  const password = await readPassword();
664
934
  if (!password) {
665
935
  process.stderr.write("leadbay-mcp install: empty password\n");
@@ -669,7 +939,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
669
939
  let region;
670
940
  try {
671
941
  if (pinnedRegion && !allowFallback) {
672
- const { REGIONS } = await import("./dist-PIXZN6N4.js");
942
+ const { REGIONS } = await import("./dist-YMZYFHZK.js");
673
943
  const baseUrl = REGIONS[pinnedRegion];
674
944
  token = await loginAt(baseUrl, email, password);
675
945
  region = pinnedRegion;
@@ -686,15 +956,28 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
686
956
  process.stderr.write(`Logged in to ${region.toUpperCase()} backend.
687
957
 
688
958
  `);
689
- const includeWrite = hasFlag(args, "include-write");
690
- if (!includeWrite) {
959
+ const includeWrite = !hasFlag(args, "no-write");
960
+ if (includeWrite) {
961
+ process.stderr.write(
962
+ "Composite write tools ENABLED (bulk_qualify_leads, enrich_titles, refine_prompt,\n report_outreach, adjust_audience, answer_clarification, import_leads).\n To disable: set LEADBAY_MCP_WRITE=0 in the env block, or re-run install with --no-write.\n\n"
963
+ );
964
+ } else {
691
965
  process.stderr.write(
692
- "Note: write tools (refine_prompt, report_outreach, adjust_audience, etc.) are NOT enabled.\n Re-run with --include-write to enable them.\n\n"
966
+ "Composite write tools DISABLED (read-only agent). Re-run without --no-write to enable.\n\n"
693
967
  );
694
968
  }
695
969
  const skipPrompts = hasFlag(args, "yes");
696
970
  const results = [];
697
971
  for (const c of chosen) {
972
+ if (c.id === "claude-desktop" && c.mode?.dxt && !forceLegacy) {
973
+ results.push({
974
+ id: c.id,
975
+ label: c.label,
976
+ ok: false,
977
+ message: "skipped (DXT detected \u2014 install the .dxt bundle instead)"
978
+ });
979
+ continue;
980
+ }
698
981
  const ok = skipPrompts || await readChoice(`Install into ${c.label} (${c.detail})?`, true);
699
982
  if (!ok) {
700
983
  results.push({ id: c.id, label: c.label, ok: false, message: "skipped by user" });
@@ -719,9 +1002,9 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
719
1002
  process.stderr.write(
720
1003
  `
721
1004
  The token was written into client config files but never printed to your terminal.
722
- Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.2 doctor
1005
+ Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.3 doctor
723
1006
  Restart your MCP client(s) to pick up the new server.
724
- If you ever leak the token, log in to app.leadbay.ai again to invalidate the prior session.
1007
+ If you ever leak the token, run \`leadbay-mcp login --email <you> --region <us|fr>\` to mint a fresh one (which invalidates the prior session).
725
1008
  `
726
1009
  );
727
1010
  return anyOk ? 0 : 1;
@@ -794,11 +1077,17 @@ async function main() {
794
1077
  const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
795
1078
  const client = await resolveClientFromEnv(logger);
796
1079
  const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
797
- const includeWrite = process.env.LEADBAY_MCP_WRITE === "1";
798
- const server = buildServer(client, { includeAdvanced, includeWrite, logger });
1080
+ const includeWrite = parseWriteEnv();
1081
+ const bulkTracker = await createDefaultBulkStore({ logger });
1082
+ const server = buildServer(client, {
1083
+ includeAdvanced,
1084
+ includeWrite,
1085
+ logger,
1086
+ bulkTracker
1087
+ });
799
1088
  const transport = new StdioServerTransport();
800
1089
  logger.info?.(
801
- `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl})`
1090
+ `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability})`
802
1091
  );
803
1092
  await server.connect(transport);
804
1093
  }
@@ -806,8 +1095,8 @@ var isEntrypoint = (() => {
806
1095
  try {
807
1096
  const entry = process.argv[1];
808
1097
  if (!entry) return false;
809
- const url = new URL(import.meta.url);
810
- return url.pathname === entry || url.pathname.endsWith(entry);
1098
+ const self = fileURLToPath(import.meta.url);
1099
+ return realpathSync(self) === realpathSync(entry);
811
1100
  } catch {
812
1101
  return false;
813
1102
  }
@@ -820,5 +1109,11 @@ if (isEntrypoint) {
820
1109
  });
821
1110
  }
822
1111
  export {
823
- resolveClientFromEnv
1112
+ buildClaudeCodeAddArgs,
1113
+ checkLoginCollision,
1114
+ computeFreshDefaultPath,
1115
+ detectClaudeDesktopMode,
1116
+ parseWriteEnv,
1117
+ resolveClientFromEnv,
1118
+ resolveDefaultCredentialsPath
824
1119
  };