@leadbay/mcp 0.2.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 ADDED
@@ -0,0 +1,824 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ compositeReadTools,
4
+ compositeWriteTools,
5
+ createClient,
6
+ granularReadTools,
7
+ granularWriteTools,
8
+ resolveRegion
9
+ } from "./chunk-BGJ6JWIO.js";
10
+
11
+ // src/bin.ts
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+
14
+ // src/server.ts
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import {
17
+ CallToolRequestSchema,
18
+ ListToolsRequestSchema
19
+ } 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.";
21
+ function formatErrorForLLM(err) {
22
+ if (err && typeof err === "object" && err.error === true) {
23
+ const parts = [`${err.message}.`, err.hint];
24
+ if (err._meta?.region) {
25
+ parts.push(`(region=${err._meta.region}, endpoint=${err._meta.endpoint || "?"})`);
26
+ }
27
+ if (err._meta?.retry_after) {
28
+ parts.push(`Retry after ${err._meta.retry_after}s.`);
29
+ }
30
+ return parts.filter(Boolean).join(" ").trim();
31
+ }
32
+ if (err instanceof Error) {
33
+ return err.message;
34
+ }
35
+ return String(err);
36
+ }
37
+ function toolsListPayload(tools) {
38
+ return tools.map((t) => ({
39
+ name: t.name,
40
+ description: t.description,
41
+ inputSchema: t.inputSchema
42
+ }));
43
+ }
44
+ 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
+ const exposedTools = [];
53
+ exposedTools.push(...compositeReadTools);
54
+ if (opts.includeWrite) {
55
+ exposedTools.push(...compositeWriteTools);
56
+ }
57
+ if (opts.includeAdvanced) {
58
+ exposedTools.push(...granularReadTools);
59
+ if (opts.includeWrite) {
60
+ exposedTools.push(...granularWriteTools);
61
+ }
62
+ }
63
+ const toolByName = /* @__PURE__ */ new Map();
64
+ for (const t of exposedTools) {
65
+ if (!toolByName.has(t.name) && t.name !== "leadbay_login") {
66
+ toolByName.set(t.name, t);
67
+ }
68
+ }
69
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
70
+ tools: toolsListPayload([...toolByName.values()])
71
+ }));
72
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
73
+ const name = req.params.name;
74
+ const tool = toolByName.get(name);
75
+ if (!tool) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: `Unknown Leadbay tool: ${name}. Available: ${[...toolByName.keys()].join(", ")}.`
81
+ }
82
+ ],
83
+ isError: true
84
+ };
85
+ }
86
+ const args = req.params.arguments ?? {};
87
+ try {
88
+ const result = await tool.execute(client, args, { logger: opts.logger });
89
+ if (result && typeof result === "object" && result.error === true) {
90
+ return {
91
+ content: [
92
+ { type: "text", text: formatErrorForLLM(result) }
93
+ ],
94
+ isError: true
95
+ };
96
+ }
97
+ return {
98
+ content: [
99
+ { type: "text", text: JSON.stringify(result, null, 2) }
100
+ ]
101
+ };
102
+ } catch (err) {
103
+ return {
104
+ content: [
105
+ { type: "text", text: formatErrorForLLM(err) }
106
+ ],
107
+ isError: true
108
+ };
109
+ }
110
+ });
111
+ return server;
112
+ }
113
+
114
+ // src/bin.ts
115
+ import { createRequire } from "module";
116
+ var VERSION = "0.2.0";
117
+ var HELP = `
118
+ leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
119
+
120
+ USAGE
121
+ leadbay-mcp Run the MCP stdio server (for Claude Desktop, Cursor, etc.)
122
+ leadbay-mcp install One-shot setup: mint a token AND register the MCP server with
123
+ your installed MCP clients (Claude Code / Claude Desktop /
124
+ Cursor). Auto-detects which clients are installed; you confirm
125
+ before each write. Token never lands in terminal scrollback.
126
+ Run this first if you're getting started.
127
+ leadbay-mcp login Lower-level: just mint a bearer token (no auto-install).
128
+ Use when you want to copy the token into a config file
129
+ yourself.
130
+ leadbay-mcp doctor Validate your token, probe your region, print account + quota.
131
+ leadbay-mcp --version Print version
132
+ leadbay-mcp --help Print this help
133
+
134
+ ENV VARS
135
+ LEADBAY_TOKEN (required) Bearer token from https://app.leadbay.ai/settings/api-tokens
136
+ LEADBAY_REGION (optional) "us" or "fr". Auto-detected from /users/me if unset.
137
+ LEADBAY_BASE_URL (optional) Override API base URL (for staging/dev).
138
+ LEADBAY_MCP_ADVANCED (optional) Set to "1" to expose granular API tools alongside
139
+ 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.
144
+ LEADBAY_MOCK (optional) Set to "1" to serve all responses from on-disk fixtures
145
+ (no network, no real auth). Useful for agent-author dry-running.
146
+ GETs are matched against fixture JSON files; POSTs/DELETEs are
147
+ journaled in-process and return {mocked: true, would_call: {...}}.
148
+ LEADBAY_MOCK_DIR (optional) Fixture directory. Default: ./.context/leadbay-live-shapes/
149
+ LEADBAY_LOG_LEVEL (optional) "debug" | "info" | "error" (default "error"). Logs to stderr.
150
+
151
+ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json)
152
+ {
153
+ "mcpServers": {
154
+ "leadbay": {
155
+ "command": "npx",
156
+ "args": ["-y", "@leadbay/mcp@0.1"],
157
+ "env": {
158
+ "LEADBAY_TOKEN": "lb_...",
159
+ "LEADBAY_REGION": "us"
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ DOCS
166
+ https://github.com/leadbay/leadclaw#readme
167
+ `.trim();
168
+ function makeStderrLogger(level) {
169
+ const rank = { debug: 0, info: 1, error: 2 };
170
+ const threshold = rank[level] ?? rank.error;
171
+ return {
172
+ info: (m) => {
173
+ if (rank.info >= threshold) process.stderr.write(`[leadbay-mcp info] ${m}
174
+ `);
175
+ },
176
+ warn: (m) => {
177
+ if (rank.info >= threshold) process.stderr.write(`[leadbay-mcp warn] ${m}
178
+ `);
179
+ },
180
+ error: (m) => {
181
+ process.stderr.write(`[leadbay-mcp error] ${m}
182
+ `);
183
+ }
184
+ };
185
+ }
186
+ function parseLogLevel(raw) {
187
+ if (raw === "debug" || raw === "info") return raw;
188
+ return "error";
189
+ }
190
+ function exitWithTokenError() {
191
+ 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"
193
+ );
194
+ process.exit(1);
195
+ }
196
+ async function resolveClientFromEnv(logger) {
197
+ const token = process.env.LEADBAY_TOKEN;
198
+ if (!token) exitWithTokenError();
199
+ const regionEnv = process.env.LEADBAY_REGION;
200
+ const explicitRegion = regionEnv === "us" || regionEnv === "fr" ? regionEnv : void 0;
201
+ const baseUrl = process.env.LEADBAY_BASE_URL;
202
+ if (baseUrl || explicitRegion) {
203
+ const config = { token };
204
+ if (baseUrl) config.baseUrl = baseUrl;
205
+ if (explicitRegion) config.region = explicitRegion;
206
+ return createClient(config);
207
+ }
208
+ process.stderr.write(
209
+ "[leadbay-mcp warn] LEADBAY_REGION is unset; probing api-us and api-fr in parallel.\n Your bearer token will be sent to BOTH backends. Set LEADBAY_REGION=us|fr in your\n MCP client config to avoid this.\n"
210
+ );
211
+ logger.info?.("Auto-detecting region via /users/me on us and fr...");
212
+ const probe = async (region) => {
213
+ const c = createClient({ token, region });
214
+ await c.request("GET", "/users/me");
215
+ return c;
216
+ };
217
+ try {
218
+ return await Promise.any([probe("us"), probe("fr")]);
219
+ } catch (err) {
220
+ const errors = err?.errors ?? [];
221
+ const firstAuth = errors.find(
222
+ (e) => e?.code === "AUTH_EXPIRED" || e?.code === "NOT_AUTHENTICATED"
223
+ );
224
+ if (firstAuth) {
225
+ process.stderr.write(
226
+ `leadbay-mcp: ${firstAuth.message}. ${firstAuth.hint}
227
+ Tip: verify your LEADBAY_TOKEN is valid and, if you know your region, set LEADBAY_REGION=us or LEADBAY_REGION=fr.
228
+ `
229
+ );
230
+ process.exit(1);
231
+ }
232
+ const firstMsg = errors[0]?.message ?? String(err);
233
+ process.stderr.write(
234
+ `leadbay-mcp: region auto-detection failed (${firstMsg}). Defaulting to us; set LEADBAY_REGION to skip probing.
235
+ `
236
+ );
237
+ return createClient({ token, region: "us" });
238
+ }
239
+ }
240
+ async function readPassword() {
241
+ const envPwd = process.env.LEADBAY_PASSWORD;
242
+ if (envPwd) return envPwd;
243
+ const isTTY = process.stdin.isTTY === true;
244
+ if (!isTTY) {
245
+ return await new Promise((resolve) => {
246
+ const chunks = [];
247
+ process.stdin.on("data", (c) => {
248
+ chunks.push(typeof c === "string" ? Buffer.from(c, "utf8") : c);
249
+ });
250
+ process.stdin.on(
251
+ "end",
252
+ () => resolve(Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, ""))
253
+ );
254
+ });
255
+ }
256
+ process.stderr.write("Password: ");
257
+ process.stdin.setRawMode(true);
258
+ process.stdin.resume();
259
+ process.stdin.setEncoding("utf8");
260
+ let buf = "";
261
+ return await new Promise((resolve) => {
262
+ const onData = (key) => {
263
+ if (key === "" || key === "") {
264
+ process.stdin.setRawMode(false);
265
+ process.stdin.pause();
266
+ process.stderr.write("\n");
267
+ process.exit(130);
268
+ }
269
+ if (key === "\r" || key === "\n") {
270
+ process.stdin.setRawMode(false);
271
+ process.stdin.pause();
272
+ process.stdin.removeListener("data", onData);
273
+ process.stderr.write("\n");
274
+ resolve(buf);
275
+ return;
276
+ }
277
+ if (key === "\x7F" || key === "\b") {
278
+ if (buf.length > 0) buf = buf.slice(0, -1);
279
+ return;
280
+ }
281
+ buf += key;
282
+ };
283
+ process.stdin.on("data", onData);
284
+ });
285
+ }
286
+ function parseFlag(args, name) {
287
+ for (let i = 0; i < args.length; i++) {
288
+ if (args[i] === `--${name}` && i + 1 < args.length) return args[i + 1];
289
+ if (args[i].startsWith(`--${name}=`)) return args[i].slice(name.length + 3);
290
+ }
291
+ return void 0;
292
+ }
293
+ function hasFlag(args, name) {
294
+ return args.some((a) => a === `--${name}`);
295
+ }
296
+ async function runLogin(args) {
297
+ const email = parseFlag(args, "email");
298
+ if (!email) {
299
+ 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"
301
+ );
302
+ return 2;
303
+ }
304
+ const regionArg = parseFlag(args, "region");
305
+ const regionEnv = process.env.LEADBAY_REGION;
306
+ const allowFallback = hasFlag(args, "allow-region-fallback");
307
+ let pinnedRegion = null;
308
+ if (regionArg === "us" || regionArg === "fr") pinnedRegion = regionArg;
309
+ else if (!regionArg && (regionEnv === "us" || regionEnv === "fr")) pinnedRegion = regionEnv;
310
+ else if (regionArg) {
311
+ process.stderr.write(`leadbay-mcp login: invalid --region '${regionArg}' (use us or fr)
312
+ `);
313
+ return 2;
314
+ }
315
+ if (!pinnedRegion && !allowFallback) {
316
+ process.stderr.write(
317
+ "leadbay-mcp login: refusing to auto-detect region without consent.\n Avoiding silent credential cross-leak: by default, --region (or $LEADBAY_REGION) must be set\n so your password only ever hits the backend that owns your account.\n Either:\n --region us (or --region fr)\n or, if you don't know your region and accept the trade-off:\n --allow-region-fallback (your password will hit BOTH backends if the first 401s)\n"
318
+ );
319
+ return 2;
320
+ }
321
+ const password = await readPassword();
322
+ if (!password) {
323
+ process.stderr.write("leadbay-mcp login: empty password\n");
324
+ return 2;
325
+ }
326
+ let result;
327
+ try {
328
+ if (pinnedRegion && !allowFallback) {
329
+ const { REGIONS } = await import("./dist-PIXZN6N4.js");
330
+ const baseUrl = REGIONS[pinnedRegion];
331
+ const c = createClient({ region: pinnedRegion });
332
+ const token = await loginAt(baseUrl, email, password);
333
+ result = { region: pinnedRegion, baseUrl, token, verified: true };
334
+ void c;
335
+ } else {
336
+ result = await resolveRegion(email, password, pinnedRegion ?? void 0);
337
+ }
338
+ } catch (err) {
339
+ process.stderr.write(`leadbay-mcp login: ${err?.message ?? String(err)}
340
+ `);
341
+ return 1;
342
+ }
343
+ const config = {
344
+ mcpServers: {
345
+ leadbay: {
346
+ command: "npx",
347
+ args: ["-y", "@leadbay/mcp@0.2"],
348
+ env: {
349
+ LEADBAY_TOKEN: result.token,
350
+ LEADBAY_REGION: result.region
351
+ }
352
+ }
353
+ }
354
+ };
355
+ const writeConfigPath = parseFlag(args, "write-config");
356
+ const quiet = hasFlag(args, "quiet");
357
+ if (writeConfigPath) {
358
+ const { writeFileSync, chmodSync } = await import("fs");
359
+ writeFileSync(writeConfigPath, JSON.stringify(config, null, 2) + "\n", {
360
+ encoding: "utf8",
361
+ mode: 384
362
+ });
363
+ try {
364
+ chmodSync(writeConfigPath, 384);
365
+ } catch {
366
+ }
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) {
374
+ 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
378
+ `
379
+ );
380
+ }
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;
388
+ }
389
+ process.stderr.write(
390
+ `
391
+ 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
+
399
+ `
400
+ );
401
+ process.stdout.write(JSON.stringify(config, null, 2) + "\n");
402
+ process.stderr.write(
403
+ `
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.
412
+ `
413
+ );
414
+ return 0;
415
+ }
416
+ async function loginAt(baseUrl, email, password) {
417
+ const https = await import("https");
418
+ return await new Promise((resolve, reject) => {
419
+ const body = JSON.stringify({ email, password });
420
+ const u = new URL(baseUrl + "/1.5/auth/login");
421
+ const r = https.request(
422
+ {
423
+ hostname: u.hostname,
424
+ port: 443,
425
+ path: u.pathname,
426
+ method: "POST",
427
+ headers: {
428
+ "Content-Type": "application/json",
429
+ "Content-Length": Buffer.byteLength(body)
430
+ }
431
+ },
432
+ (res) => {
433
+ const chunks = [];
434
+ res.on("data", (c) => chunks.push(c));
435
+ res.on("end", () => {
436
+ const raw = Buffer.concat(chunks).toString("utf8");
437
+ if (res.statusCode === 200) {
438
+ try {
439
+ const parsed = JSON.parse(raw);
440
+ if (parsed?.token) return resolve(parsed.token);
441
+ return reject(new Error("login response had no token"));
442
+ } catch {
443
+ return reject(new Error("login response was not JSON"));
444
+ }
445
+ }
446
+ reject(
447
+ new Error(
448
+ `login failed (${res.statusCode}) at ${baseUrl}: ${raw.slice(0, 200)}`
449
+ )
450
+ );
451
+ });
452
+ }
453
+ );
454
+ r.on("error", reject);
455
+ r.write(body);
456
+ r.end();
457
+ });
458
+ }
459
+ async function detectClients() {
460
+ const out = [];
461
+ const { existsSync } = await import("fs");
462
+ const os = await import("os");
463
+ const claudeBin = await new Promise((resolve) => {
464
+ const cmd = process.platform === "win32" ? "where" : "which";
465
+ const child = require_("node:child_process").spawn(cmd, ["claude"], {
466
+ stdio: ["ignore", "pipe", "ignore"]
467
+ });
468
+ let buf = "";
469
+ child.stdout.on("data", (c) => buf += c.toString());
470
+ child.on(
471
+ "close",
472
+ (code) => resolve(code === 0 ? buf.split(/\r?\n/)[0] : null)
473
+ );
474
+ });
475
+ if (claudeBin) {
476
+ out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
477
+ }
478
+ 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 });
482
+ }
483
+ const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
484
+ if (existsSync(cursorPath)) {
485
+ out.push({ id: "cursor", label: "Cursor", detail: cursorPath });
486
+ } else {
487
+ const cursorDir = process.platform === "win32" ? `${home}\\.cursor` : `${home}/.cursor`;
488
+ if (existsSync(cursorDir)) {
489
+ out.push({
490
+ id: "cursor",
491
+ label: "Cursor",
492
+ detail: cursorPath + " (will be created)"
493
+ });
494
+ }
495
+ }
496
+ return out;
497
+ }
498
+ function require_(mod) {
499
+ return createRequire(import.meta.url)(mod);
500
+ }
501
+ async function readChoice(prompt, def = true) {
502
+ const isTTY = process.stdin.isTTY === true && process.stdout.isTTY === true;
503
+ if (!isTTY) return def;
504
+ process.stderr.write(`${prompt} ${def ? "[Y/n]" : "[y/N]"} `);
505
+ process.stdin.setRawMode(true);
506
+ process.stdin.resume();
507
+ process.stdin.setEncoding("utf8");
508
+ return await new Promise((resolve) => {
509
+ const onData = (k) => {
510
+ if (k === "\r" || k === "\n") {
511
+ process.stdin.setRawMode(false);
512
+ process.stdin.pause();
513
+ process.stdin.removeListener("data", onData);
514
+ process.stderr.write(def ? "y\n" : "n\n");
515
+ return resolve(def);
516
+ }
517
+ if (k === "" || k === "") {
518
+ process.stdin.setRawMode(false);
519
+ process.stdin.pause();
520
+ process.stderr.write("\n");
521
+ process.exit(130);
522
+ }
523
+ const lower = k.toLowerCase();
524
+ if (lower === "y" || lower === "n") {
525
+ process.stdin.setRawMode(false);
526
+ process.stdin.pause();
527
+ process.stdin.removeListener("data", onData);
528
+ process.stderr.write(`${lower}
529
+ `);
530
+ return resolve(lower === "y");
531
+ }
532
+ };
533
+ process.stdin.on("data", onData);
534
+ });
535
+ }
536
+ async function installInClaudeCode(token, region, includeWrite) {
537
+ const cp = await import("child_process");
538
+ const args = [
539
+ "mcp",
540
+ "add",
541
+ "leadbay",
542
+ "--env",
543
+ `LEADBAY_TOKEN=${token}`,
544
+ "--env",
545
+ `LEADBAY_REGION=${region}`
546
+ ];
547
+ if (includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=1`);
548
+ args.push("--", "npx", "-y", "@leadbay/mcp@0.2");
549
+ return await new Promise((resolve) => {
550
+ const child = cp.spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
551
+ let stderr = "";
552
+ child.stderr.on("data", (c) => stderr += c.toString());
553
+ child.on(
554
+ "close",
555
+ (code) => resolve({
556
+ ok: code === 0,
557
+ message: code === 0 ? "registered" : `claude mcp add exited ${code}: ${stderr.trim().slice(0, 200)}`
558
+ })
559
+ );
560
+ child.on(
561
+ "error",
562
+ (err) => resolve({ ok: false, message: `failed to spawn claude: ${err.message}` })
563
+ );
564
+ });
565
+ }
566
+ async function installInJsonConfig(configPath, token, region, includeWrite) {
567
+ try {
568
+ const { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } = await import("fs");
569
+ const { dirname } = await import("path");
570
+ let parsed = {};
571
+ let preserved = {};
572
+ if (existsSync(configPath)) {
573
+ const raw = readFileSync(configPath, "utf8");
574
+ try {
575
+ preserved = JSON.parse(raw);
576
+ parsed = preserved;
577
+ } catch {
578
+ return { ok: false, message: `existing ${configPath} is not valid JSON; refusing to overwrite` };
579
+ }
580
+ } else {
581
+ mkdirSync(dirname(configPath), { recursive: true });
582
+ }
583
+ parsed.mcpServers = parsed.mcpServers ?? {};
584
+ const env = {
585
+ LEADBAY_TOKEN: token,
586
+ LEADBAY_REGION: region
587
+ };
588
+ if (includeWrite) env.LEADBAY_MCP_WRITE = "1";
589
+ parsed.mcpServers.leadbay = {
590
+ command: "npx",
591
+ args: ["-y", "@leadbay/mcp@0.2"],
592
+ env
593
+ };
594
+ const tmp = configPath + ".tmp";
595
+ writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", "utf8");
596
+ const { renameSync, chmodSync } = await import("fs");
597
+ renameSync(tmp, configPath);
598
+ try {
599
+ const st = statSync(configPath);
600
+ if ((st.mode & 511) > 384 && Object.keys(preserved).length === 0) {
601
+ chmodSync(configPath, 384);
602
+ }
603
+ } catch {
604
+ }
605
+ return { ok: true, message: "registered" };
606
+ } catch (err) {
607
+ return { ok: false, message: err?.message ?? String(err) };
608
+ }
609
+ }
610
+ async function runInstall(args) {
611
+ const email = parseFlag(args, "email");
612
+ if (!email) {
613
+ 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"
615
+ );
616
+ return 2;
617
+ }
618
+ const regionArg = parseFlag(args, "region");
619
+ const regionEnv = process.env.LEADBAY_REGION;
620
+ const allowFallback = hasFlag(args, "allow-region-fallback");
621
+ let pinnedRegion = null;
622
+ if (regionArg === "us" || regionArg === "fr") pinnedRegion = regionArg;
623
+ else if (!regionArg && (regionEnv === "us" || regionEnv === "fr")) pinnedRegion = regionEnv;
624
+ else if (regionArg) {
625
+ process.stderr.write(`leadbay-mcp install: invalid --region '${regionArg}' (use us or fr)
626
+ `);
627
+ return 2;
628
+ }
629
+ if (!pinnedRegion && !allowFallback) {
630
+ process.stderr.write(
631
+ "leadbay-mcp install: --region us|fr (or $LEADBAY_REGION) is required by default.\n This avoids sending your password to a Leadbay backend you don't use.\n Pass --allow-region-fallback to opt in to auto-detect (your password will hit BOTH backends if the first 401s).\n"
632
+ );
633
+ return 2;
634
+ }
635
+ const detected = await detectClients();
636
+ const targetArg = parseFlag(args, "target");
637
+ let chosen = detected;
638
+ if (targetArg) {
639
+ const want = new Set(targetArg.split(",").map((s) => s.trim()));
640
+ chosen = detected.filter((c) => want.has(c.id));
641
+ const missing = [...want].filter((id) => !detected.some((c) => c.id === id));
642
+ if (missing.length) {
643
+ process.stderr.write(
644
+ `leadbay-mcp install: --target requested [${[...want].join(", ")}] but these were not detected on this machine: ${missing.join(", ")}
645
+ `
646
+ );
647
+ }
648
+ }
649
+ if (chosen.length === 0) {
650
+ process.stderr.write(
651
+ "leadbay-mcp install: no MCP clients detected on this machine.\n Install Claude Code (https://docs.claude.com/claude-code), Claude Desktop, or Cursor first,\n or use `leadbay-mcp login --write-config /path/to/config.json` to mint a token without auto-install.\n"
652
+ );
653
+ return 1;
654
+ }
655
+ process.stderr.write(
656
+ `
657
+ leadbay-mcp install \u2014 detected MCP clients on this machine:
658
+ `
659
+ );
660
+ for (const c of chosen) process.stderr.write(` \u2022 ${c.label.padEnd(16)} ${c.detail}
661
+ `);
662
+ process.stderr.write("\n");
663
+ const password = await readPassword();
664
+ if (!password) {
665
+ process.stderr.write("leadbay-mcp install: empty password\n");
666
+ return 2;
667
+ }
668
+ let token;
669
+ let region;
670
+ try {
671
+ if (pinnedRegion && !allowFallback) {
672
+ const { REGIONS } = await import("./dist-PIXZN6N4.js");
673
+ const baseUrl = REGIONS[pinnedRegion];
674
+ token = await loginAt(baseUrl, email, password);
675
+ region = pinnedRegion;
676
+ } else {
677
+ const result = await resolveRegion(email, password, pinnedRegion ?? void 0);
678
+ token = result.token;
679
+ region = result.region;
680
+ }
681
+ } catch (err) {
682
+ process.stderr.write(`leadbay-mcp install: ${err?.message ?? String(err)}
683
+ `);
684
+ return 1;
685
+ }
686
+ process.stderr.write(`Logged in to ${region.toUpperCase()} backend.
687
+
688
+ `);
689
+ const includeWrite = hasFlag(args, "include-write");
690
+ if (!includeWrite) {
691
+ 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"
693
+ );
694
+ }
695
+ const skipPrompts = hasFlag(args, "yes");
696
+ const results = [];
697
+ for (const c of chosen) {
698
+ const ok = skipPrompts || await readChoice(`Install into ${c.label} (${c.detail})?`, true);
699
+ if (!ok) {
700
+ results.push({ id: c.id, label: c.label, ok: false, message: "skipped by user" });
701
+ continue;
702
+ }
703
+ let res;
704
+ if (c.id === "claude-code") {
705
+ res = await installInClaudeCode(token, region, includeWrite);
706
+ } else {
707
+ const path = c.detail.split(" ")[0];
708
+ res = await installInJsonConfig(path, token, region, includeWrite);
709
+ }
710
+ results.push({ id: c.id, label: c.label, ...res });
711
+ }
712
+ process.stderr.write("\n=== install summary ===\n");
713
+ let anyOk = false;
714
+ for (const r of results) {
715
+ process.stderr.write(` ${r.ok ? "\u2713" : "\u2717"} ${r.label.padEnd(16)} ${r.message}
716
+ `);
717
+ if (r.ok) anyOk = true;
718
+ }
719
+ process.stderr.write(
720
+ `
721
+ 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
723
+ 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.
725
+ `
726
+ );
727
+ return anyOk ? 0 : 1;
728
+ }
729
+ async function runDoctor() {
730
+ const token = process.env.LEADBAY_TOKEN;
731
+ if (!token) {
732
+ exitWithTokenError();
733
+ }
734
+ const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
735
+ const regionEnv = process.env.LEADBAY_REGION;
736
+ const baseUrl = process.env.LEADBAY_BASE_URL;
737
+ const regions = regionEnv === "fr" ? ["fr", "us"] : regionEnv === "us" ? ["us", "fr"] : ["us", "fr"];
738
+ for (const region of regions) {
739
+ const config = { token };
740
+ if (baseUrl) config.baseUrl = baseUrl;
741
+ else config.region = region;
742
+ const client = createClient(config);
743
+ logger.info?.(`Trying region="${region}" baseUrl="${client.baseUrl}"`);
744
+ try {
745
+ const me = await client.request("GET", "/users/me");
746
+ process.stdout.write(
747
+ `Leadbay connection OK.
748
+ Region: ${baseUrl ? "(custom baseUrl)" : region}
749
+ Base URL: ${client.baseUrl}
750
+ Organization: ${me.organization.name} (${me.organization.id})
751
+ Billing: ${me.organization.billing?.status ?? "unknown"}
752
+ AI credits: ${me.organization.billing?.ai_credits ?? "?"} / ${me.organization.billing?.ai_credits_quota ?? "?"}
753
+ `
754
+ );
755
+ return 0;
756
+ } catch (err) {
757
+ logger.error?.(`${region}: ${err?.message ?? err}`);
758
+ if (err?.code === "AUTH_EXPIRED" || err?.code === "NOT_AUTHENTICATED") {
759
+ process.stderr.write(
760
+ `Leadbay: your LEADBAY_TOKEN is not valid for ${region}. ${err.hint}
761
+ `
762
+ );
763
+ return 1;
764
+ }
765
+ }
766
+ if (baseUrl) break;
767
+ }
768
+ process.stderr.write(
769
+ "Leadbay doctor: could not reach any Leadbay region with this token. Check the token and your network.\n"
770
+ );
771
+ return 1;
772
+ }
773
+ async function main() {
774
+ const arg = process.argv[2];
775
+ if (arg === "--version" || arg === "-v") {
776
+ process.stdout.write(`${VERSION}
777
+ `);
778
+ return;
779
+ }
780
+ if (arg === "--help" || arg === "-h" || arg === "help") {
781
+ process.stdout.write(`${HELP}
782
+ `);
783
+ return;
784
+ }
785
+ if (arg === "install") {
786
+ process.exit(await runInstall(process.argv.slice(3)));
787
+ }
788
+ if (arg === "login") {
789
+ process.exit(await runLogin(process.argv.slice(3)));
790
+ }
791
+ if (arg === "doctor") {
792
+ process.exit(await runDoctor());
793
+ }
794
+ const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
795
+ const client = await resolveClientFromEnv(logger);
796
+ 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 });
799
+ const transport = new StdioServerTransport();
800
+ logger.info?.(
801
+ `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl})`
802
+ );
803
+ await server.connect(transport);
804
+ }
805
+ var isEntrypoint = (() => {
806
+ try {
807
+ const entry = process.argv[1];
808
+ if (!entry) return false;
809
+ const url = new URL(import.meta.url);
810
+ return url.pathname === entry || url.pathname.endsWith(entry);
811
+ } catch {
812
+ return false;
813
+ }
814
+ })();
815
+ if (isEntrypoint) {
816
+ main().catch((err) => {
817
+ process.stderr.write(`leadbay-mcp: ${err?.message ?? err}
818
+ `);
819
+ process.exit(1);
820
+ });
821
+ }
822
+ export {
823
+ resolveClientFromEnv
824
+ };