@leadbay/mcp 0.2.2 → 0.4.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
@@ -4,10 +4,11 @@ import {
4
4
  compositeWriteTools,
5
5
  createClient,
6
6
  createDefaultBulkStore,
7
+ formatLoginError,
7
8
  granularReadTools,
8
9
  granularWriteTools,
9
10
  resolveRegion
10
- } from "./chunk-FJBO2MY2.js";
11
+ } from "./chunk-O2UOXRZO.js";
11
12
 
12
13
  // src/bin.ts
13
14
  import { realpathSync } from "fs";
@@ -20,7 +21,44 @@ import {
20
21
  CallToolRequestSchema,
21
22
  ListToolsRequestSchema
22
23
  } from "@modelcontextprotocol/sdk/types.js";
23
- 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\nHow 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.\n\nTwo 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. Call leadbay_bulk_qualify_leads for deeper qualification or leadbay_enrich_titles for contacts on any lead that looks worth it.\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.\n\nSuggested 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.";
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
+ }
24
62
  function formatErrorForLLM(err) {
25
63
  if (err && typeof err === "object" && err.error === true) {
26
64
  const parts = [`${err.message}.`, err.hint];
@@ -45,13 +83,6 @@ function toolsListPayload(tools) {
45
83
  }));
46
84
  }
47
85
  function buildServer(client, opts = {}) {
48
- const server = new Server(
49
- { name: "leadbay", version: "0.2.0" },
50
- {
51
- capabilities: { tools: {} },
52
- instructions: SERVER_INSTRUCTIONS
53
- }
54
- );
55
86
  const exposedTools = [];
56
87
  exposedTools.push(...compositeReadTools);
57
88
  if (opts.includeWrite) {
@@ -69,6 +100,14 @@ function buildServer(client, opts = {}) {
69
100
  toolByName.set(t.name, t);
70
101
  }
71
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
+ );
72
111
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
73
112
  tools: toolsListPayload([...toolByName.values()])
74
113
  }));
@@ -119,7 +158,7 @@ function buildServer(client, opts = {}) {
119
158
 
120
159
  // src/bin.ts
121
160
  import { createRequire } from "module";
122
- var VERSION = "0.2.2";
161
+ var VERSION = "0.4.0";
123
162
  var HELP = `
124
163
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
125
164
 
@@ -143,10 +182,12 @@ ENV VARS
143
182
  LEADBAY_BASE_URL (optional) Override API base URL (for staging/dev).
144
183
  LEADBAY_MCP_ADVANCED (optional) Set to "1" to expose granular API tools alongside
145
184
  the composite workflow tools. Most users don't need this.
146
- LEADBAY_MCP_WRITE (optional) Set to "1" to expose write composites (refine_prompt,
147
- report_outreach, adjust_audience, etc.) and write granulars.
148
- Defaults off \u2014 read composites are exposed by default; mutations
149
- 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.
150
191
  LEADBAY_MOCK (optional) Set to "1" to serve all responses from on-disk fixtures
151
192
  (no network, no real auth). Useful for agent-author dry-running.
152
193
  GETs are matched against fixture JSON files; POSTs/DELETEs are
@@ -159,7 +200,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
159
200
  "mcpServers": {
160
201
  "leadbay": {
161
202
  "command": "npx",
162
- "args": ["-y", "@leadbay/mcp@0.1"],
203
+ "args": ["-y", "@leadbay/mcp@0.3"],
163
204
  "env": {
164
205
  "LEADBAY_TOKEN": "lb_...",
165
206
  "LEADBAY_REGION": "us"
@@ -193,6 +234,18 @@ function parseLogLevel(raw) {
193
234
  if (raw === "debug" || raw === "info") return raw;
194
235
  return "error";
195
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
+ }
196
249
  function exitWithTokenError() {
197
250
  process.stderr.write(
198
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"
@@ -299,11 +352,73 @@ function parseFlag(args, name) {
299
352
  function hasFlag(args, name) {
300
353
  return args.some((a) => a === `--${name}`);
301
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
+ }
302
396
  async function runLogin(args) {
303
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
+ })();
304
405
  if (!email) {
305
406
  process.stderr.write(
306
- "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
+ `
307
422
  );
308
423
  return 2;
309
424
  }
@@ -332,7 +447,7 @@ async function runLogin(args) {
332
447
  let result;
333
448
  try {
334
449
  if (pinnedRegion && !allowFallback) {
335
- const { REGIONS } = await import("./dist-FENQ2I7R.js");
450
+ const { REGIONS } = await import("./dist-RONMQBYU.js");
336
451
  const baseUrl = REGIONS[pinnedRegion];
337
452
  const c = createClient({ region: pinnedRegion });
338
453
  const token = await loginAt(baseUrl, email, password);
@@ -347,10 +462,11 @@ async function runLogin(args) {
347
462
  return 1;
348
463
  }
349
464
  const config = {
465
+ email,
350
466
  mcpServers: {
351
467
  leadbay: {
352
468
  command: "npx",
353
- args: ["-y", "@leadbay/mcp@0.2"],
469
+ args: ["-y", "@leadbay/mcp@0.3"],
354
470
  env: {
355
471
  LEADBAY_TOKEN: result.token,
356
472
  LEADBAY_REGION: result.region
@@ -360,61 +476,150 @@ async function runLogin(args) {
360
476
  };
361
477
  const writeConfigPath = parseFlag(args, "write-config");
362
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;
363
518
  if (writeConfigPath) {
364
- const { writeFileSync, chmodSync } = await import("fs");
365
- 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", {
366
560
  encoding: "utf8",
367
561
  mode: 384
368
562
  });
369
563
  try {
370
- chmodSync(writeConfigPath, 384);
564
+ chmodSync(tmp, 384);
371
565
  } catch {
372
566
  }
373
- process.stderr.write(
374
- `
375
- Logged in to ${result.region.toUpperCase()} backend (${result.verified ? "verified" : "UNVERIFIED \u2014 check your email"}).
376
- Wrote MCP config to ${writeConfigPath} (mode 0600). Token NOT printed to terminal.
377
- `
378
- );
379
- 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") {
380
579
  process.stderr.write(
381
- `
382
- For Claude Code, run:
383
- 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).
384
583
  `
385
584
  );
585
+ return 1;
386
586
  }
387
- process.stderr.write(
388
- `
389
- TREAT THE TOKEN AS A SECRET. It grants full access to your Leadbay account.
390
- Delete the config file once your MCP client has it loaded, or keep it 0600.
391
- `
392
- );
393
- return 0;
587
+ process.stderr.write(`leadbay-mcp login: ${err?.message ?? String(err)}
588
+ `);
589
+ return 1;
394
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)";
395
592
  process.stderr.write(
396
593
  `
397
594
  Logged in to ${result.region.toUpperCase()} backend (${result.verified ? "verified" : "UNVERIFIED \u2014 check your email"}).
398
-
399
- \u26A0\uFE0F About to print your bearer token to STDOUT.
400
- Treat it like a password. Do NOT paste this into chat, screen-share, or commit it.
401
- For safer handling, re-run with --write-config /path/to/config.json (writes 0600).
402
-
403
- Add this to your MCP client config:
404
-
595
+ Wrote MCP config to ${targetPath} ${modeNote}. Token NOT printed to terminal.
405
596
  `
406
597
  );
407
- 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
+ }
408
619
  process.stderr.write(
409
620
  `
410
- Or for Claude Code (token included \u2014 same warning applies):
411
-
412
- claude mcp add leadbay \\
413
- --env LEADBAY_TOKEN=${result.token} \\
414
- --env LEADBAY_REGION=${result.region} \\
415
- -- npx -y @leadbay/mcp@0.2
416
-
417
- 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.
418
623
  `
419
624
  );
420
625
  return 0;
@@ -450,9 +655,7 @@ async function loginAt(baseUrl, email, password) {
450
655
  }
451
656
  }
452
657
  reject(
453
- new Error(
454
- `login failed (${res.statusCode}) at ${baseUrl}: ${raw.slice(0, 200)}`
455
- )
658
+ new Error(formatLoginError(res.statusCode ?? 0, raw, baseUrl))
456
659
  );
457
660
  });
458
661
  }
@@ -462,6 +665,31 @@ async function loginAt(baseUrl, email, password) {
462
665
  r.end();
463
666
  });
464
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
+ }
465
693
  async function detectClients() {
466
694
  const out = [];
467
695
  const { existsSync } = await import("fs");
@@ -482,9 +710,16 @@ async function detectClients() {
482
710
  out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
483
711
  }
484
712
  const home = os.homedir();
485
- 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`;
486
- if (existsSync(cdPath)) {
487
- 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
+ });
488
723
  }
489
724
  const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
490
725
  if (existsSync(cursorPath)) {
@@ -539,19 +774,25 @@ async function readChoice(prompt, def = true) {
539
774
  process.stdin.on("data", onData);
540
775
  });
541
776
  }
542
- async function installInClaudeCode(token, region, includeWrite) {
543
- const cp = await import("child_process");
777
+ function buildClaudeCodeAddArgs(token, region, includeWrite) {
544
778
  const args = [
545
779
  "mcp",
546
780
  "add",
547
781
  "leadbay",
782
+ "--scope",
783
+ "user",
548
784
  "--env",
549
785
  `LEADBAY_TOKEN=${token}`,
550
786
  "--env",
551
787
  `LEADBAY_REGION=${region}`
552
788
  ];
553
- if (includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=1`);
554
- 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);
555
796
  return await new Promise((resolve) => {
556
797
  const child = cp.spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
557
798
  let stderr = "";
@@ -591,10 +832,10 @@ async function installInJsonConfig(configPath, token, region, includeWrite) {
591
832
  LEADBAY_TOKEN: token,
592
833
  LEADBAY_REGION: region
593
834
  };
594
- if (includeWrite) env.LEADBAY_MCP_WRITE = "1";
835
+ if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
595
836
  parsed.mcpServers.leadbay = {
596
837
  command: "npx",
597
- args: ["-y", "@leadbay/mcp@0.2"],
838
+ args: ["-y", "@leadbay/mcp@0.3"],
598
839
  env
599
840
  };
600
841
  const tmp = configPath + ".tmp";
@@ -617,10 +858,15 @@ async function runInstall(args) {
617
858
  const email = parseFlag(args, "email");
618
859
  if (!email) {
619
860
  process.stderr.write(
620
- "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"
621
862
  );
622
863
  return 2;
623
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
+ }
624
870
  const regionArg = parseFlag(args, "region");
625
871
  const regionEnv = process.env.LEADBAY_REGION;
626
872
  const allowFallback = hasFlag(args, "allow-region-fallback");
@@ -663,9 +909,27 @@ async function runInstall(args) {
663
909
  leadbay-mcp install \u2014 detected MCP clients on this machine:
664
910
  `
665
911
  );
666
- 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}
667
915
  `);
916
+ }
668
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
+ }
669
933
  const password = await readPassword();
670
934
  if (!password) {
671
935
  process.stderr.write("leadbay-mcp install: empty password\n");
@@ -675,7 +939,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
675
939
  let region;
676
940
  try {
677
941
  if (pinnedRegion && !allowFallback) {
678
- const { REGIONS } = await import("./dist-FENQ2I7R.js");
942
+ const { REGIONS } = await import("./dist-RONMQBYU.js");
679
943
  const baseUrl = REGIONS[pinnedRegion];
680
944
  token = await loginAt(baseUrl, email, password);
681
945
  region = pinnedRegion;
@@ -692,15 +956,28 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
692
956
  process.stderr.write(`Logged in to ${region.toUpperCase()} backend.
693
957
 
694
958
  `);
695
- const includeWrite = hasFlag(args, "include-write");
696
- 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 {
697
965
  process.stderr.write(
698
- "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"
699
967
  );
700
968
  }
701
969
  const skipPrompts = hasFlag(args, "yes");
702
970
  const results = [];
703
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
+ }
704
981
  const ok = skipPrompts || await readChoice(`Install into ${c.label} (${c.detail})?`, true);
705
982
  if (!ok) {
706
983
  results.push({ id: c.id, label: c.label, ok: false, message: "skipped by user" });
@@ -725,7 +1002,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
725
1002
  process.stderr.write(
726
1003
  `
727
1004
  The token was written into client config files but never printed to your terminal.
728
- Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.2 doctor
1005
+ Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.3 doctor
729
1006
  Restart your MCP client(s) to pick up the new server.
730
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).
731
1008
  `
@@ -800,7 +1077,7 @@ async function main() {
800
1077
  const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
801
1078
  const client = await resolveClientFromEnv(logger);
802
1079
  const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
803
- const includeWrite = process.env.LEADBAY_MCP_WRITE === "1";
1080
+ const includeWrite = parseWriteEnv();
804
1081
  const bulkTracker = await createDefaultBulkStore({ logger });
805
1082
  const server = buildServer(client, {
806
1083
  includeAdvanced,
@@ -832,5 +1109,11 @@ if (isEntrypoint) {
832
1109
  });
833
1110
  }
834
1111
  export {
835
- resolveClientFromEnv
1112
+ buildClaudeCodeAddArgs,
1113
+ checkLoginCollision,
1114
+ computeFreshDefaultPath,
1115
+ detectClaudeDesktopMode,
1116
+ parseWriteEnv,
1117
+ resolveClientFromEnv,
1118
+ resolveDefaultCredentialsPath
836
1119
  };