@leadbay/mcp 0.15.1 → 0.16.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.16.0 — 2026-05-28
4
+
5
+ - **OAuth login** (`leadbay-mcp login --oauth`): browser-based Authorization
6
+ Code + PKCE flow with Dynamic Client Registration (RFC 7591). No password
7
+ ever touches the CLI. The resulting `o.<token>` is interchangeable with the
8
+ legacy bearer token.
9
+ - Region is auto-detected via stargate GeoIP (`stargate.leadbay.app/1.0/user_info`)
10
+ for OAuth. Pass `--region us|fr` to override if you're on a VPN or travelling.
11
+ - Pass `--staging` together with `--oauth` to point at `staging.leadbay.app` for
12
+ testing.
13
+ - The Claude Desktop `.dxt` / `.mcpb` bundle now opts into OAuth bootstrap on
14
+ first launch. The install dialog no longer asks for a bearer token, region, or
15
+ backend URL; it only exposes the write-tools toggle.
16
+ - Existing email-password `login` flow is unchanged and remains available for
17
+ manual installs and CI.
18
+ - **Pin bumps**: active `@leadbay/mcp@0.13` install/runtime references in docs,
19
+ generated client config, and MCP Registry metadata now point at `@0.16`.
20
+
3
21
  ## 0.15.0 — 2026-05-27
4
22
 
5
23
  - **Sentry observability**: every non-2xx Leadbay API outcome now lands in
package/README.md CHANGED
@@ -5,14 +5,17 @@
5
5
  <!--
6
6
  LLM HINT — if you're an AI agent reading this README to install the server:
7
7
  - Use ONLY the commands shown verbatim in §1. Do not invent variants.
8
- - DO NOT tell the user "LEADBAY_REGION is auto-detected, drop it" the
8
+ - For the legacy email/password install, DO NOT tell the user
9
+ "LEADBAY_REGION is auto-detected, drop it" — the
9
10
  server's region auto-probe sends the bearer token to BOTH regions, which
10
- is a credential cross-leak. Always pass --region (us or fr) on install.
11
- - To mint a token, the user runs `install` (auto-registers with MCP clients)
11
+ is a credential cross-leak. Always pass --region (us or fr) on that path.
12
+ - Claude Desktop's `.dxt` / `.mcpb` bundle uses OAuth on first launch and
13
+ does not ask the user to paste a token or choose a region.
14
+ - To mint a token manually, the user runs `install` (auto-registers with MCP clients)
12
15
  or `login` (mints a token to a 0600-mode credentials file by default since
13
16
  0.3.0; pass `--write-config /path` to override the path or
14
- `--unsafe-print-token` for legacy CI flows). There is no web UI for this —
15
- do not invent one.
17
+ `--unsafe-print-token` for legacy CI flows). Prefer `login --oauth` when
18
+ the production OAuth backend/frontend are deployed.
16
19
  -->
17
20
 
18
21
  A Model Context Protocol server that lets Claude Desktop, Cursor, Claude Code, and any other MCP-compatible agent find, research, and prepare outreach on B2B prospects using your Leadbay account.
@@ -45,7 +48,7 @@ to suppress this ambient metadata.
45
48
  ## 1. Install (one command)
46
49
 
47
50
  ```bash
48
- npx -y @leadbay/mcp@0.13 install --email you@yourcompany.com --region us
51
+ npx -y @leadbay/mcp@0.16 install --email you@yourcompany.com --region us
49
52
  # (you'll be prompted for your password — it's not echoed)
50
53
  ```
51
54
 
@@ -82,20 +85,22 @@ You can verify the skills installed by running `/skill list` after install. To u
82
85
 
83
86
  ### Claude Desktop 2026 (DXT)
84
87
 
85
- Claude Desktop 2026 ships the DXT (Desktop Extension) system — the legacy `claude_desktop_config.json` is UI-prefs-only there and gets overwritten by the app. If you're on 2026, **install the `.dxt` bundle** from [Releases](https://github.com/leadbay/leadclaw/releases/latest) (drag-drop into Settings → Extensions). `leadbay-mcp install` detects this and skips the legacy write automatically.
88
+ Claude Desktop 2026 ships the DXT (Desktop Extension) system — the legacy `claude_desktop_config.json` is UI-prefs-only there and gets overwritten by the app. If you're on 2026, **install the `.dxt` / `.mcpb` bundle** from [Releases](https://github.com/leadbay/leadclaw/releases/latest) (drag-drop into Settings → Extensions). On first launch, the extension opens Leadbay in your browser for OAuth consent, auto-detects your region through stargate, then persists the resulting token locally. The extension settings only ask whether write tools should be enabled.
89
+
90
+ `leadbay-mcp install` detects Claude Desktop 2026 and skips the legacy config write automatically.
86
91
 
87
92
  ### `npm install -g` says "EACCES" / "permission denied"
88
93
 
89
94
  If you installed Node from the official [nodejs.org](https://nodejs.org) `.pkg`, `/usr/local/lib/node_modules` is root-owned. Any of these works:
90
95
 
91
- - **Use `npx` (recommended, no global install):** all examples above use `npx -y @leadbay/mcp@0.13 ...` — no global install needed.
96
+ - **Use `npx` (recommended, no global install):** all examples above use `npx -y @leadbay/mcp@0.16 ...` — no global install needed.
92
97
  - **`sudo npm install -g @leadbay/mcp`** (enter your macOS password).
93
98
  - **Use a Node version manager** — [nvm](https://github.com/nvm-sh/nvm), [volta](https://volta.sh), [fnm](https://github.com/Schniz/fnm). They install Node under your home directory, so `npm install -g` works without sudo.
94
99
 
95
100
  ### If you'd rather mint a token without auto-install
96
101
 
97
102
  ```bash
98
- npx -y @leadbay/mcp@0.13 login \
103
+ npx -y @leadbay/mcp@0.16 login \
99
104
  --email you@yourcompany.com \
100
105
  --region us
101
106
  ```
@@ -113,7 +118,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
113
118
  "mcpServers": {
114
119
  "leadbay": {
115
120
  "command": "npx",
116
- "args": ["-y", "@leadbay/mcp@0.13"],
121
+ "args": ["-y", "@leadbay/mcp@0.16"],
117
122
  "env": {
118
123
  "LEADBAY_TOKEN": "<paste-token-from-step-1>",
119
124
  "LEADBAY_REGION": "us"
@@ -134,7 +139,7 @@ In Cursor settings, add the MCP server:
134
139
  "mcp.servers": {
135
140
  "leadbay": {
136
141
  "command": "npx",
137
- "args": ["-y", "@leadbay/mcp@0.13"],
142
+ "args": ["-y", "@leadbay/mcp@0.16"],
138
143
  "env": { "LEADBAY_TOKEN": "<paste-token>", "LEADBAY_REGION": "us" }
139
144
  }
140
145
  }
@@ -147,7 +152,7 @@ In Cursor settings, add the MCP server:
147
152
  claude mcp add leadbay --scope user \
148
153
  --env LEADBAY_TOKEN=<paste-token> \
149
154
  --env LEADBAY_REGION=us \
150
- -- npx -y @leadbay/mcp@0.13
155
+ -- npx -y @leadbay/mcp@0.16
151
156
  ```
152
157
 
153
158
  > **`--scope user`** registers Leadbay globally for your account (visible from any project). Without it, `claude mcp add` defaults to project-local scope and the server only appears in conversations opened from the directory where you ran the command.
@@ -159,7 +164,7 @@ claude mcp add leadbay --scope user \
159
164
  Before starting Claude, run:
160
165
 
161
166
  ```bash
162
- LEADBAY_TOKEN=<paste-token> npx -y @leadbay/mcp@0.13 doctor
167
+ LEADBAY_TOKEN=<paste-token> npx -y @leadbay/mcp@0.16 doctor
163
168
  ```
164
169
 
165
170
  Expected output:
@@ -387,17 +392,54 @@ The user's literal text replaces `verification.ref` in the outreach record, and
387
392
  | `No enrichment credits remaining` | Out of quota | Contact Leadbay support to extend quota |
388
393
  | Claude Desktop "loading forever" on first use | `npx` cold-start fetching the package | First run takes ~10s. Prefer `npm install -g @leadbay/mcp` for faster startup. |
389
394
  | Claude Desktop doesn't show Leadbay tools | Server crashed at startup | Check `~/Library/Logs/Claude/mcp*.log` (macOS) or `%APPDATA%\Claude\logs\mcp*.log` (Windows). |
390
- | Claude Code can't find Leadbay in a new conversation | MCP server installed at project scope (default before 0.3.0) | Re-run with `--scope user`: `claude mcp remove leadbay && claude mcp add leadbay --scope user --env LEADBAY_TOKEN=… --env LEADBAY_REGION=us -- npx -y @leadbay/mcp@0.13` |
395
+ | Claude Code can't find Leadbay in a new conversation | MCP server installed at project scope (default before 0.3.0) | Re-run with `--scope user`: `claude mcp remove leadbay && claude mcp add leadbay --scope user --env LEADBAY_TOKEN=… --env LEADBAY_REGION=us -- npx -y @leadbay/mcp@0.16` |
391
396
  | Agent reports "tool not found" for `refine_prompt` / `adjust_audience` etc. | Pre-0.3.0 install with `LEADBAY_MCP_WRITE` unset (writes were off) | Either re-run `npx @leadbay/mcp install` or remove `LEADBAY_MCP_WRITE=0` from your client config (writes are on by default in 0.3.0+) |
392
397
 
393
398
  ## 5. Upgrade & rotation
394
399
 
395
- **Upgrade**: change the pinned minor in your config, e.g. `"@leadbay/mcp@0.2"` → `"@leadbay/mcp@0.13"`, then restart the client. **0.3.0 enables composite write tools by default** — see [MIGRATION.md](./MIGRATION.md). See also the [changelog](https://github.com/leadbay/leadclaw/releases).
400
+ **Upgrade**: change the pinned minor in your config, e.g. `"@leadbay/mcp@0.2"` → `"@leadbay/mcp@0.16"`, then restart the client. **0.3.0 enables composite write tools by default** — see [MIGRATION.md](./MIGRATION.md). See also the [changelog](https://github.com/leadbay/leadclaw/releases).
396
401
 
397
- **Rotate token**: re-run `npx -y @leadbay/mcp@0.13 install --email you@yourcompany.com --region us` (or `login`) — the new session token replaces the old one in your MCP client config, and logging in again invalidates the prior session on most session backends.
402
+ **Rotate token**: re-run `npx -y @leadbay/mcp@0.16 install --email you@yourcompany.com --region us` (or `login`) — the new session token replaces the old one in your MCP client config, and logging in again invalidates the prior session on most session backends.
398
403
 
399
404
  ## 6. Advanced
400
405
 
406
+ ### OAuth login (preview)
407
+
408
+ Browser-based login is available as an alternative to email/password:
409
+
410
+ ```bash
411
+ npx -y @leadbay/mcp login --oauth
412
+ ```
413
+
414
+ The CLI:
415
+
416
+ 1. Probes `stargate.leadbay.app/1.0/user_info` to auto-detect your region
417
+ from GeoIP (override with `--region us|fr` if you're behind a VPN or
418
+ travelling outside your home region).
419
+ 2. Opens your browser to `leadbay.app/oauth/authorize`.
420
+ 3. After you click **Allow**, redirects to a one-shot loopback URL on
421
+ `http://127.0.0.1:<random-port>/callback`.
422
+ 4. Exchanges the authorization code for a token using PKCE (S256), then writes
423
+ the same credentials file that the email/password flow produces.
424
+
425
+ The CLI registers a fresh OAuth client per machine (RFC 7591 Dynamic Client
426
+ Registration), so no shared secret lives in the binary. The resulting token is
427
+ long-lived and interchangeable with the legacy bearer token. Manual MCP config
428
+ can still pass it as `LEADBAY_TOKEN`; the Claude Desktop bundle performs the
429
+ OAuth flow itself and persists the token without asking you to paste it.
430
+
431
+ For testing against staging before the production backend deploy lands:
432
+
433
+ ```bash
434
+ npx -y @leadbay/mcp login --oauth --staging
435
+ ```
436
+
437
+ `--staging` switches the backend (`staging.api.leadbay.app` /
438
+ `api-{us,fr}-staging.leadbay.app`), the consent UI (`staging.leadbay.app`),
439
+ and the stargate region probe (`staging.stargate.leadbay.app`). It also
440
+ persists `LEADBAY_BASE_URL` in the credentials file so subsequent runs don't
441
+ snap back to prod.
442
+
401
443
  ### Exposing the granular tools and disabling write tools
402
444
 
403
445
  By default the server exposes the **composite workflow tools** — both reads (`leadbay_pull_leads`, `leadbay_research_lead_by_id`, `leadbay_account_status`, `leadbay_recall_ordered_titles`, `leadbay_research_lead_by_name_fuzzy`, `leadbay_prepare_outreach`, `leadbay_qualify_status`, `leadbay_list_mappable_fields`) and writes (`leadbay_bulk_qualify_leads`, `leadbay_enrich_titles`, `leadbay_refine_prompt`, `leadbay_report_outreach`, `leadbay_adjust_audience`, `leadbay_answer_clarification`, `leadbay_import_leads`, `leadbay_import_and_qualify`). These work well with most prompts.
@@ -507,7 +549,7 @@ After your first authenticated call, your PostHog `distinctId` is set to your Le
507
549
  "mcpServers": {
508
550
  "leadbay": {
509
551
  "command": "npx",
510
- "args": ["-y", "@leadbay/mcp@0.13"],
552
+ "args": ["-y", "@leadbay/mcp@0.16"],
511
553
  "env": {
512
554
  "LEADBAY_TOKEN": "u.…",
513
555
  "LEADBAY_REGION": "us",
package/dist/bin.js CHANGED
@@ -2754,9 +2754,373 @@ async function createDefaultUpdateStateStore(opts = {}) {
2754
2754
  }
2755
2755
  }
2756
2756
 
2757
+ // src/oauth.ts
2758
+ import { createHash, randomBytes } from "crypto";
2759
+ import { createServer } from "http";
2760
+ import { request as httpsRequestRaw } from "https";
2761
+ import { spawn } from "child_process";
2762
+ var STARGATE_URLS = {
2763
+ prod: "https://stargate.leadbay.app/1.0/user_info",
2764
+ staging: "https://staging.stargate.leadbay.app/1.0/user_info"
2765
+ };
2766
+ var FR_COUNTRY_CODES = /* @__PURE__ */ new Set([
2767
+ "FR",
2768
+ // France
2769
+ // French overseas territories — same regional partition as France in the
2770
+ // backend's stargate /login route (see backend/specs/stargate/1.0).
2771
+ "GP",
2772
+ "MQ",
2773
+ "GF",
2774
+ "RE",
2775
+ "YT",
2776
+ "MF",
2777
+ "BL",
2778
+ "PM",
2779
+ "WF",
2780
+ "PF",
2781
+ "NC",
2782
+ "TF"
2783
+ ]);
2784
+ async function inferRegionViaStargate(opts) {
2785
+ const url = STARGATE_URLS[opts.staging ? "staging" : "prod"];
2786
+ const res = await httpsCall("GET", url, { Accept: "application/json" });
2787
+ if (res.status !== 200) {
2788
+ throw new Error(
2789
+ `Stargate region probe failed: GET ${url} returned ${res.status}. Pass --region us|fr to skip auto-detection.`
2790
+ );
2791
+ }
2792
+ let parsed;
2793
+ try {
2794
+ parsed = JSON.parse(res.body);
2795
+ } catch {
2796
+ throw new Error(`Stargate region probe returned non-JSON body`);
2797
+ }
2798
+ const country = parsed.userCountry;
2799
+ if (!country || typeof country !== "string") {
2800
+ throw new Error(`Stargate response missing userCountry: ${res.body.slice(0, 200)}`);
2801
+ }
2802
+ if (country === "US") return "us";
2803
+ if (FR_COUNTRY_CODES.has(country)) return "fr";
2804
+ throw new Error(
2805
+ `Stargate detected your country as ${country}, which isn't mapped to a Leadbay region. Pass --region us|fr explicitly.`
2806
+ );
2807
+ }
2808
+ function generatePkce() {
2809
+ const verifier = base64UrlEncode(randomBytes(32));
2810
+ const challenge = base64UrlEncode(
2811
+ createHash("sha256").update(verifier, "ascii").digest()
2812
+ );
2813
+ return { verifier, challenge, method: "S256" };
2814
+ }
2815
+ function base64UrlEncode(buf) {
2816
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2817
+ }
2818
+ function httpsCall(method, url, headers, body) {
2819
+ return new Promise((resolve, reject) => {
2820
+ const u = new URL(url);
2821
+ const reqHeaders = { ...headers };
2822
+ if (body !== void 0) reqHeaders["Content-Length"] = Buffer.byteLength(body);
2823
+ const req = httpsRequestRaw(
2824
+ {
2825
+ hostname: u.hostname,
2826
+ port: u.port ? Number(u.port) : 443,
2827
+ path: u.pathname + u.search,
2828
+ method,
2829
+ headers: reqHeaders
2830
+ },
2831
+ (res) => {
2832
+ const chunks = [];
2833
+ res.on("data", (c) => chunks.push(c));
2834
+ res.on(
2835
+ "end",
2836
+ () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") })
2837
+ );
2838
+ }
2839
+ );
2840
+ req.on("error", reject);
2841
+ if (body !== void 0) req.write(body);
2842
+ req.end();
2843
+ });
2844
+ }
2845
+ async function fetchDiscoveryDoc(authServerBaseUrl) {
2846
+ const url = trimSlash(authServerBaseUrl) + "/.well-known/oauth-authorization-server";
2847
+ const res = await httpsCall("GET", url, { Accept: "application/json" });
2848
+ if (res.status !== 200) {
2849
+ throw new Error(
2850
+ `OAuth discovery failed: GET ${url} returned ${res.status}. Either OAuth isn't deployed to this backend yet, or the URL is wrong.`
2851
+ );
2852
+ }
2853
+ let doc;
2854
+ try {
2855
+ doc = JSON.parse(res.body);
2856
+ } catch {
2857
+ throw new Error(`OAuth discovery returned non-JSON body from ${url}`);
2858
+ }
2859
+ for (const field of ["authorization_endpoint", "token_endpoint", "registration_endpoint"]) {
2860
+ if (typeof doc[field] !== "string" || !doc[field]) {
2861
+ throw new Error(`OAuth discovery doc missing required field: ${field}`);
2862
+ }
2863
+ }
2864
+ if (doc.code_challenge_methods_supported && !doc.code_challenge_methods_supported.includes("S256")) {
2865
+ throw new Error(
2866
+ `OAuth server doesn't support S256 PKCE (only ${doc.code_challenge_methods_supported.join(", ")}). Aborting \u2014 plain PKCE is too weak for a public client.`
2867
+ );
2868
+ }
2869
+ return doc;
2870
+ }
2871
+ function trimSlash(s) {
2872
+ return s.endsWith("/") ? s.slice(0, -1) : s;
2873
+ }
2874
+ async function registerClient(registrationEndpoint, params) {
2875
+ const body = JSON.stringify({
2876
+ client_name: params.clientName,
2877
+ redirect_uris: [params.redirectUri],
2878
+ logo_uri: params.logoUri,
2879
+ token_endpoint_auth_method: "none"
2880
+ // public client
2881
+ });
2882
+ const res = await httpsCall(
2883
+ "POST",
2884
+ registrationEndpoint,
2885
+ { "Content-Type": "application/json", Accept: "application/json" },
2886
+ body
2887
+ );
2888
+ if (res.status === 429) {
2889
+ throw new Error(
2890
+ `OAuth client registration rate-limited (429). The backend allows ~10 registrations per IP per hour. Wait and retry, or use the password flow (drop the --oauth flag).`
2891
+ );
2892
+ }
2893
+ if (res.status !== 201 && res.status !== 200) {
2894
+ throw new Error(
2895
+ `OAuth client registration failed: POST ${registrationEndpoint} \u2192 ${res.status} ${res.body.slice(0, 300)}`
2896
+ );
2897
+ }
2898
+ let parsed;
2899
+ try {
2900
+ parsed = JSON.parse(res.body);
2901
+ } catch {
2902
+ throw new Error(`OAuth client registration returned non-JSON body`);
2903
+ }
2904
+ if (!parsed.client_id) {
2905
+ throw new Error(`OAuth client registration response missing client_id`);
2906
+ }
2907
+ return parsed;
2908
+ }
2909
+ async function startLoopbackListener(opts) {
2910
+ let resolveCallback;
2911
+ let rejectCallback;
2912
+ const callbackPromise = new Promise((res, rej) => {
2913
+ resolveCallback = res;
2914
+ rejectCallback = rej;
2915
+ });
2916
+ const server = createServer((req, res) => {
2917
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2918
+ if (req.method !== "GET" || url.pathname !== "/callback") {
2919
+ res.statusCode = 404;
2920
+ res.end("Not Found");
2921
+ return;
2922
+ }
2923
+ const params = url.searchParams;
2924
+ const errParam = params.get("error");
2925
+ if (errParam) {
2926
+ const desc = params.get("error_description") ?? "";
2927
+ res.statusCode = 400;
2928
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2929
+ res.end(renderHtml("Authorization failed", `${errParam}${desc ? `: ${desc}` : ""}`));
2930
+ rejectCallback(new Error(`OAuth authorization denied: ${errParam}${desc ? ` (${desc})` : ""}`));
2931
+ return;
2932
+ }
2933
+ const code = params.get("code");
2934
+ const state = params.get("state");
2935
+ if (!code || !state) {
2936
+ res.statusCode = 400;
2937
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2938
+ res.end(renderHtml("Authorization failed", "Missing code or state parameter."));
2939
+ rejectCallback(new Error("OAuth callback missing code or state"));
2940
+ return;
2941
+ }
2942
+ if (state !== opts.expectedState) {
2943
+ res.statusCode = 400;
2944
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2945
+ res.end(renderHtml("Authorization failed", "Invalid state parameter (possible CSRF)."));
2946
+ rejectCallback(new Error("OAuth callback state mismatch (possible CSRF)"));
2947
+ return;
2948
+ }
2949
+ res.statusCode = 200;
2950
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2951
+ res.end(renderHtml("You're signed in", "You can close this tab and return to the terminal."));
2952
+ resolveCallback({ code, state });
2953
+ });
2954
+ await new Promise((resolve, reject) => {
2955
+ server.once("error", reject);
2956
+ server.listen(0, "127.0.0.1", () => {
2957
+ server.off("error", reject);
2958
+ resolve();
2959
+ });
2960
+ });
2961
+ const addr = server.address();
2962
+ const redirectUri = `http://127.0.0.1:${addr.port}/callback`;
2963
+ const timer = setTimeout(() => {
2964
+ rejectCallback(new Error(`OAuth login timed out after ${Math.round(opts.timeoutMs / 1e3)}s`));
2965
+ }, opts.timeoutMs);
2966
+ return {
2967
+ redirectUri,
2968
+ waitForCallback: () => callbackPromise.finally(() => {
2969
+ clearTimeout(timer);
2970
+ }),
2971
+ close: () => {
2972
+ clearTimeout(timer);
2973
+ server.close();
2974
+ }
2975
+ };
2976
+ }
2977
+ function renderHtml(title, message) {
2978
+ const safeTitle = escapeHtml(title);
2979
+ const safeMsg = escapeHtml(message);
2980
+ return `<!doctype html>
2981
+ <html lang="en"><head>
2982
+ <meta charset="utf-8"><title>${safeTitle} \u2014 Leadbay MCP</title>
2983
+ <style>
2984
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
2985
+ display:flex;align-items:center;justify-content:center;height:100vh;
2986
+ margin:0;background:#fafafa;color:#111}
2987
+ .card{padding:32px 40px;border:1px solid #eee;border-radius:12px;
2988
+ background:#fff;max-width:420px;text-align:center}
2989
+ h1{font-size:18px;margin:0 0 12px;font-weight:600}
2990
+ p{margin:0;color:#555;font-size:14px;line-height:1.5}
2991
+ </style></head>
2992
+ <body><div class="card"><h1>${safeTitle}</h1><p>${safeMsg}</p></div></body></html>`;
2993
+ }
2994
+ function escapeHtml(s) {
2995
+ return s.replace(
2996
+ /[&<>"']/g,
2997
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
2998
+ );
2999
+ }
3000
+ async function exchangeCodeForToken(opts) {
3001
+ const form = new URLSearchParams({
3002
+ grant_type: "authorization_code",
3003
+ code: opts.code,
3004
+ redirect_uri: opts.redirectUri,
3005
+ client_id: opts.clientId,
3006
+ code_verifier: opts.codeVerifier
3007
+ }).toString();
3008
+ const res = await httpsCall(
3009
+ "POST",
3010
+ opts.tokenEndpoint,
3011
+ {
3012
+ "Content-Type": "application/x-www-form-urlencoded",
3013
+ Accept: "application/json"
3014
+ },
3015
+ form
3016
+ );
3017
+ if (res.status !== 200) {
3018
+ throw new Error(
3019
+ `OAuth token exchange failed: POST ${opts.tokenEndpoint} \u2192 ${res.status} ${res.body.slice(0, 300)}`
3020
+ );
3021
+ }
3022
+ let parsed;
3023
+ try {
3024
+ parsed = JSON.parse(res.body);
3025
+ } catch {
3026
+ throw new Error("OAuth token endpoint returned non-JSON body");
3027
+ }
3028
+ if (!parsed.access_token) {
3029
+ throw new Error(`OAuth token response missing access_token: ${res.body.slice(0, 200)}`);
3030
+ }
3031
+ return { accessToken: parsed.access_token };
3032
+ }
3033
+ async function openInBrowser(url) {
3034
+ const platform = process.platform;
3035
+ let cmd;
3036
+ let args;
3037
+ if (platform === "darwin") {
3038
+ cmd = "open";
3039
+ args = [url];
3040
+ } else if (platform === "win32") {
3041
+ cmd = "cmd";
3042
+ args = ["/c", "start", '""', url];
3043
+ } else {
3044
+ cmd = "xdg-open";
3045
+ args = [url];
3046
+ }
3047
+ await new Promise((resolve, reject) => {
3048
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
3049
+ child.on("error", reject);
3050
+ child.on("spawn", () => {
3051
+ child.unref();
3052
+ resolve();
3053
+ });
3054
+ });
3055
+ }
3056
+ async function oauthLogin(opts) {
3057
+ const log = opts.log ?? (() => {
3058
+ });
3059
+ const open = opts.openBrowser ?? openInBrowser;
3060
+ const timeoutMs = opts.timeoutMs ?? 5 * 60 * 1e3;
3061
+ log(`Discovering OAuth endpoints at ${opts.authServerBaseUrl}\u2026
3062
+ `);
3063
+ const doc = await fetchDiscoveryDoc(opts.authServerBaseUrl);
3064
+ const state = base64UrlEncode(randomBytes(16));
3065
+ const pkce = generatePkce();
3066
+ log("Starting loopback listener on 127.0.0.1\u2026\n");
3067
+ const listener = await startLoopbackListener({ expectedState: state, timeoutMs });
3068
+ try {
3069
+ log(`Registering client at ${doc.registration_endpoint}\u2026
3070
+ `);
3071
+ const client = await registerClient(doc.registration_endpoint, {
3072
+ clientName: opts.clientName,
3073
+ redirectUri: listener.redirectUri,
3074
+ logoUri: opts.logoUri
3075
+ });
3076
+ const authorizeUrl = new URL(doc.authorization_endpoint);
3077
+ authorizeUrl.searchParams.set("response_type", "code");
3078
+ authorizeUrl.searchParams.set("client_id", client.client_id);
3079
+ authorizeUrl.searchParams.set("redirect_uri", listener.redirectUri);
3080
+ authorizeUrl.searchParams.set("state", state);
3081
+ authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
3082
+ authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
3083
+ log(`Opening browser to authorize\u2026
3084
+ ${authorizeUrl.toString()}
3085
+ `);
3086
+ try {
3087
+ await open(authorizeUrl.toString());
3088
+ } catch (err) {
3089
+ log(
3090
+ `Could not open browser automatically (${err?.message ?? err}). Open this URL manually:
3091
+ ${authorizeUrl.toString()}
3092
+ `
3093
+ );
3094
+ }
3095
+ log("Waiting for authorization (5 min timeout)\u2026\n");
3096
+ const { code } = await listener.waitForCallback();
3097
+ log("Exchanging authorization code for access token\u2026\n");
3098
+ const { accessToken } = await exchangeCodeForToken({
3099
+ tokenEndpoint: doc.token_endpoint,
3100
+ code,
3101
+ codeVerifier: pkce.verifier,
3102
+ clientId: client.client_id,
3103
+ redirectUri: listener.redirectUri
3104
+ });
3105
+ return { accessToken };
3106
+ } finally {
3107
+ listener.close();
3108
+ }
3109
+ }
3110
+
2757
3111
  // src/bin.ts
2758
3112
  import { createRequire } from "module";
2759
- var VERSION = "0.15.1";
3113
+ var OAUTH_BASE_URLS = {
3114
+ prod: {
3115
+ us: "https://api-us.leadbay.app",
3116
+ fr: "https://api-fr.leadbay.app"
3117
+ },
3118
+ staging: {
3119
+ us: "https://api-us-staging.leadbay.app",
3120
+ fr: "https://staging.api.leadbay.app"
3121
+ }
3122
+ };
3123
+ var VERSION = "0.16.0";
2760
3124
  var HELP = `
2761
3125
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
2762
3126
 
@@ -2805,7 +3169,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
2805
3169
  "mcpServers": {
2806
3170
  "leadbay": {
2807
3171
  "command": "npx",
2808
- "args": ["-y", "@leadbay/mcp@0.13"],
3172
+ "args": ["-y", "@leadbay/mcp@0.16"],
2809
3173
  "env": {
2810
3174
  "LEADBAY_TOKEN": "lb_...",
2811
3175
  "LEADBAY_REGION": "us",
@@ -2878,9 +3242,151 @@ function makeBrokenClient(stubError, region) {
2878
3242
  const baseUrl = region === "fr" ? "https://api-fr.leadbay.app" : "https://api-us.leadbay.app";
2879
3243
  return new BrokenLeadbayClient(stubError, baseUrl, region);
2880
3244
  }
3245
+ function hydrateEnvFromCredentialsFile() {
3246
+ if (process.env.LEADBAY_TOKEN) return false;
3247
+ try {
3248
+ const { existsSync, readFileSync } = require_("node:fs");
3249
+ const { path } = resolveOAuthBootstrapCredentialsPath();
3250
+ if (!existsSync(path)) return false;
3251
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
3252
+ const env = parsed?.mcpServers?.leadbay?.env;
3253
+ if (!env || typeof env !== "object") return false;
3254
+ if (typeof env.LEADBAY_TOKEN === "string" && env.LEADBAY_TOKEN.length > 0) {
3255
+ process.env.LEADBAY_TOKEN = env.LEADBAY_TOKEN;
3256
+ }
3257
+ if (!process.env.LEADBAY_REGION && typeof env.LEADBAY_REGION === "string") {
3258
+ process.env.LEADBAY_REGION = env.LEADBAY_REGION;
3259
+ }
3260
+ if (!process.env.LEADBAY_BASE_URL && typeof env.LEADBAY_BASE_URL === "string") {
3261
+ process.env.LEADBAY_BASE_URL = env.LEADBAY_BASE_URL;
3262
+ }
3263
+ return !!process.env.LEADBAY_TOKEN;
3264
+ } catch {
3265
+ return false;
3266
+ }
3267
+ }
3268
+ function resolveOAuthBootstrapCredentialsPath() {
3269
+ const resolved = resolveDefaultCredentialsPath();
3270
+ if (process.env.LEADBAY_OAUTH_STAGING !== "1") return resolved;
3271
+ const { dirname: dirname2, join } = require_("node:path");
3272
+ return {
3273
+ path: join(dirname2(resolved.path), "credentials.staging.json"),
3274
+ legacy: resolved.legacy
3275
+ };
3276
+ }
3277
+ async function bootstrapOAuthIfMissing(logger) {
3278
+ if (process.env.LEADBAY_TOKEN) return false;
3279
+ const { hostname } = await import("os");
3280
+ process.stderr.write(
3281
+ `
3282
+ [leadbay-mcp@${VERSION}] No token found \u2014 starting OAuth login in your browser\u2026
3283
+ (This is a one-time setup. The resulting token will be persisted at
3284
+ ${(() => {
3285
+ try {
3286
+ return resolveOAuthBootstrapCredentialsPath().path;
3287
+ } catch {
3288
+ return "<credentials file>";
3289
+ }
3290
+ })()}
3291
+ so subsequent launches start instantly.)
3292
+
3293
+ `
3294
+ );
3295
+ const envBaseUrl = process.env.LEADBAY_BASE_URL;
3296
+ const envRegion = process.env.LEADBAY_REGION;
3297
+ const isStaging = process.env.LEADBAY_OAUTH_STAGING === "1" || !!envBaseUrl && /staging/.test(envBaseUrl);
3298
+ let region;
3299
+ let authServerBaseUrl;
3300
+ try {
3301
+ if (envBaseUrl) {
3302
+ authServerBaseUrl = envBaseUrl;
3303
+ region = /(-fr|staging\.api)/.test(envBaseUrl) ? "fr" : "us";
3304
+ } else if (envRegion === "us" || envRegion === "fr") {
3305
+ region = envRegion;
3306
+ authServerBaseUrl = OAUTH_BASE_URLS[isStaging ? "staging" : "prod"][region];
3307
+ } else {
3308
+ region = await inferRegionViaStargate({ staging: isStaging });
3309
+ authServerBaseUrl = OAUTH_BASE_URLS[isStaging ? "staging" : "prod"][region];
3310
+ }
3311
+ const { accessToken } = await oauthLogin({
3312
+ authServerBaseUrl,
3313
+ clientName: `Leadbay MCP @ ${hostname()}`,
3314
+ log: (m) => process.stderr.write(m)
3315
+ });
3316
+ try {
3317
+ const { writeFileSync, mkdirSync, chmodSync } = require_("node:fs");
3318
+ const { dirname: dirname2 } = require_("node:path");
3319
+ const { path } = resolveOAuthBootstrapCredentialsPath();
3320
+ const envBlock = {
3321
+ LEADBAY_TOKEN: accessToken,
3322
+ LEADBAY_REGION: region
3323
+ };
3324
+ if (isStaging || envBaseUrl) envBlock.LEADBAY_BASE_URL = authServerBaseUrl;
3325
+ const config = {
3326
+ mcpServers: {
3327
+ leadbay: {
3328
+ command: "npx",
3329
+ args: ["-y", `@leadbay/mcp@${VERSION.split(".").slice(0, 2).join(".")}`],
3330
+ env: envBlock
3331
+ }
3332
+ }
3333
+ };
3334
+ mkdirSync(dirname2(path), { recursive: true });
3335
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
3336
+ try {
3337
+ chmodSync(path, 384);
3338
+ } catch {
3339
+ }
3340
+ process.stderr.write(`[leadbay-mcp] Persisted credentials to ${path}
3341
+ `);
3342
+ } catch (err) {
3343
+ process.stderr.write(
3344
+ `[leadbay-mcp warn] OAuth succeeded but persisting the token failed (${err?.message ?? err}). You'll be prompted to re-authorize on next launch.
3345
+ `
3346
+ );
3347
+ }
3348
+ process.env.LEADBAY_TOKEN = accessToken;
3349
+ process.env.LEADBAY_REGION = region;
3350
+ if (isStaging || envBaseUrl) process.env.LEADBAY_BASE_URL = authServerBaseUrl;
3351
+ logger.info?.(`OAuth bootstrap complete \u2014 region=${region}`);
3352
+ return true;
3353
+ } catch (err) {
3354
+ process.stderr.write(
3355
+ `[leadbay-mcp] OAuth bootstrap failed: ${err?.message ?? err}
3356
+ The server will start but tools will return AUTH_MISSING until you authorize.
3357
+ `
3358
+ );
3359
+ return false;
3360
+ }
3361
+ }
2881
3362
  async function resolveClientFromEnv(logger) {
3363
+ if (process.env.LEADBAY_OAUTH_BOOTSTRAP === "1") {
3364
+ hydrateEnvFromCredentialsFile();
3365
+ if (!process.env.LEADBAY_TOKEN) {
3366
+ await bootstrapOAuthIfMissing(logger);
3367
+ }
3368
+ }
2882
3369
  const token = process.env.LEADBAY_TOKEN;
2883
3370
  if (!token) {
3371
+ if (process.env.LEADBAY_OAUTH_BOOTSTRAP === "1") {
3372
+ process.stderr.write(
3373
+ "leadbay-mcp: OAuth authorization is required but no token is available.\n Restart the Claude Desktop extension to authorize Leadbay in your browser.\n\nRun `leadbay-mcp --help` for the full config template.\n"
3374
+ );
3375
+ const regionEnv3 = process.env.LEADBAY_REGION;
3376
+ const region2 = regionEnv3 === "fr" ? "fr" : "us";
3377
+ return {
3378
+ client: makeBrokenClient(
3379
+ {
3380
+ error: true,
3381
+ code: "AUTH_MISSING",
3382
+ message: "Leadbay OAuth authorization has not completed.",
3383
+ hint: "Restart the Claude Desktop extension and complete the Leadbay OAuth browser authorization."
3384
+ },
3385
+ region2
3386
+ ),
3387
+ authState: "missing"
3388
+ };
3389
+ }
2884
3390
  process.stderr.write(
2885
3391
  "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"
2886
3392
  );
@@ -3027,7 +3533,7 @@ function checkLoginCollision(existingConfig, email, region) {
3027
3533
  const cfg = existingConfig;
3028
3534
  const existingEmail = typeof cfg.email === "string" && cfg.email.length > 0 ? cfg.email : void 0;
3029
3535
  const existingRegion = typeof cfg.mcpServers?.leadbay?.env?.LEADBAY_REGION === "string" ? cfg.mcpServers.leadbay.env.LEADBAY_REGION : void 0;
3030
- if (existingEmail !== void 0 && existingEmail !== email) {
3536
+ if (existingEmail !== void 0 && email !== void 0 && existingEmail !== email) {
3031
3537
  return `existing email=${existingEmail} (this login is email=${email})`;
3032
3538
  }
3033
3539
  if (existingRegion !== void 0 && existingRegion !== region) {
@@ -3053,6 +3559,8 @@ function computeFreshDefaultPath() {
3053
3559
  return path.join(home, ".config", "leadbay", "credentials.json");
3054
3560
  }
3055
3561
  async function runLogin(args) {
3562
+ const useOAuth = hasFlag(args, "oauth");
3563
+ const useStaging = hasFlag(args, "staging");
3056
3564
  const email = parseFlag(args, "email");
3057
3565
  const defaultPathPreview = (() => {
3058
3566
  try {
@@ -3061,11 +3569,15 @@ async function runLogin(args) {
3061
3569
  return "<HOME>/.config/leadbay/credentials.json";
3062
3570
  }
3063
3571
  })();
3064
- if (!email) {
3572
+ if (!email && !useOAuth) {
3065
3573
  process.stderr.write(
3066
3574
  `Usage: leadbay-mcp login --email you@example.com [--region us|fr] [--allow-region-fallback]
3067
3575
  [--write-config PATH] [--unsafe-print-token] [--force] [--quiet]
3576
+ leadbay-mcp login --oauth [--region us|fr] [--staging] [--write-config PATH] [--force] [--quiet]
3068
3577
  Then enter your password (hidden), or pipe it via stdin / set $LEADBAY_PASSWORD.
3578
+ --oauth Use OAuth Authorization Code + PKCE in your browser instead of email/password.
3579
+ Region is auto-detected via stargate GeoIP; pass --region to override.
3580
+ --staging Point at staging.leadbay.app endpoints. Use with --oauth for testing.
3069
3581
  --region Pin the backend (us|fr); avoids sending your password to a backend you don't use.
3070
3582
  Defaults to $LEADBAY_REGION if set; otherwise asks you to pass --allow-region-fallback.
3071
3583
  --allow-region-fallback Try us, then fr (or fr, then us). Your password hits BOTH backends if the
@@ -3092,45 +3604,82 @@ async function runLogin(args) {
3092
3604
  `);
3093
3605
  return 2;
3094
3606
  }
3095
- if (!pinnedRegion && !allowFallback) {
3607
+ if (!pinnedRegion && !allowFallback && !useOAuth) {
3096
3608
  process.stderr.write(
3097
3609
  "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"
3098
3610
  );
3099
3611
  return 2;
3100
3612
  }
3101
- const password = await readPassword();
3102
- if (!password) {
3103
- process.stderr.write("leadbay-mcp login: empty password\n");
3104
- return 2;
3105
- }
3106
3613
  let result;
3107
- try {
3108
- if (pinnedRegion && !allowFallback) {
3109
- const { REGIONS } = await import("./dist-2NAFYPXG.js");
3110
- const baseUrl = REGIONS[pinnedRegion];
3111
- const c = createClient({ region: pinnedRegion });
3112
- const token = await loginAt(baseUrl, email, password);
3113
- result = { region: pinnedRegion, baseUrl, token, verified: true };
3114
- void c;
3614
+ if (useOAuth) {
3615
+ let region;
3616
+ if (pinnedRegion) {
3617
+ region = pinnedRegion;
3115
3618
  } else {
3116
- result = await resolveRegion(email, password, pinnedRegion ?? void 0);
3619
+ try {
3620
+ process.stderr.write("Detecting your region from stargate\u2026\n");
3621
+ region = await inferRegionViaStargate({ staging: useStaging });
3622
+ process.stderr.write(`Detected region: ${region.toUpperCase()}
3623
+ `);
3624
+ } catch (err) {
3625
+ process.stderr.write(`leadbay-mcp@${VERSION} login --oauth: ${err?.message ?? String(err)}
3626
+ `);
3627
+ await reportCliFailure("__oauth_login__", err);
3628
+ return 1;
3629
+ }
3117
3630
  }
3118
- } catch (err) {
3119
- process.stderr.write(`leadbay-mcp@${VERSION} login: ${err?.message ?? String(err)}
3631
+ const baseUrl = OAUTH_BASE_URLS[useStaging ? "staging" : "prod"][region];
3632
+ try {
3633
+ const { hostname } = await import("os");
3634
+ const clientName = `Leadbay MCP @ ${hostname()}`;
3635
+ const { accessToken } = await oauthLogin({
3636
+ authServerBaseUrl: baseUrl,
3637
+ clientName,
3638
+ log: (m) => process.stderr.write(m)
3639
+ });
3640
+ result = { region, baseUrl, token: accessToken, verified: true };
3641
+ } catch (err) {
3642
+ process.stderr.write(`leadbay-mcp@${VERSION} login --oauth: ${err?.message ?? String(err)}
3120
3643
  `);
3121
- await reportCliFailure("__login__", err);
3122
- return 1;
3644
+ await reportCliFailure("__oauth_login__", err);
3645
+ return 1;
3646
+ }
3647
+ } else {
3648
+ const password = await readPassword();
3649
+ if (!password) {
3650
+ process.stderr.write("leadbay-mcp login: empty password\n");
3651
+ return 2;
3652
+ }
3653
+ try {
3654
+ if (pinnedRegion && !allowFallback) {
3655
+ const { REGIONS } = await import("./dist-2NAFYPXG.js");
3656
+ const baseUrl = REGIONS[pinnedRegion];
3657
+ const c = createClient({ region: pinnedRegion });
3658
+ const token = await loginAt(baseUrl, email, password);
3659
+ result = { region: pinnedRegion, baseUrl, token, verified: true };
3660
+ void c;
3661
+ } else {
3662
+ result = await resolveRegion(email, password, pinnedRegion ?? void 0);
3663
+ }
3664
+ } catch (err) {
3665
+ process.stderr.write(`leadbay-mcp@${VERSION} login: ${err?.message ?? String(err)}
3666
+ `);
3667
+ await reportCliFailure("__login__", err);
3668
+ return 1;
3669
+ }
3123
3670
  }
3671
+ const envBlock = {
3672
+ LEADBAY_TOKEN: result.token,
3673
+ LEADBAY_REGION: result.region
3674
+ };
3675
+ if (useStaging) envBlock.LEADBAY_BASE_URL = result.baseUrl;
3124
3676
  const config = {
3125
- email,
3677
+ ...email ? { email } : {},
3126
3678
  mcpServers: {
3127
3679
  leadbay: {
3128
3680
  command: "npx",
3129
- args: ["-y", "@leadbay/mcp@0.13"],
3130
- env: {
3131
- LEADBAY_TOKEN: result.token,
3132
- LEADBAY_REGION: result.region
3133
- }
3681
+ args: ["-y", "@leadbay/mcp@0.16"],
3682
+ env: envBlock
3134
3683
  }
3135
3684
  }
3136
3685
  };
@@ -3166,7 +3715,7 @@ Or for Claude Code (token included \u2014 same warning applies):
3166
3715
  claude mcp add leadbay --scope user \\
3167
3716
  --env LEADBAY_TOKEN=${result.token} \\
3168
3717
  --env LEADBAY_REGION=${result.region} \\
3169
- -- npx -y @leadbay/mcp@0.13
3718
+ -- npx -y @leadbay/mcp@0.16
3170
3719
 
3171
3720
  Restart your MCP client to pick up the new server.
3172
3721
  `
@@ -3272,7 +3821,7 @@ For Claude Code, run:
3272
3821
  claude mcp add leadbay --scope user \\
3273
3822
  --env LEADBAY_TOKEN=$(jq -r .mcpServers.leadbay.env.LEADBAY_TOKEN ${quotedPath}) \\
3274
3823
  --env LEADBAY_REGION=${result.region} \\
3275
- -- npx -y @leadbay/mcp@0.13
3824
+ -- npx -y @leadbay/mcp@0.16
3276
3825
  `
3277
3826
  );
3278
3827
  }
@@ -3452,7 +4001,7 @@ function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled) {
3452
4001
  `LEADBAY_TELEMETRY_ENABLED=${telemetryEnabled ? "true" : "false"}`
3453
4002
  ];
3454
4003
  if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
3455
- args.push("--", "npx", "-y", "@leadbay/mcp@0.13");
4004
+ args.push("--", "npx", "-y", "@leadbay/mcp@0.16");
3456
4005
  return args;
3457
4006
  }
3458
4007
  async function installInClaudeCode(token, region, includeWrite, telemetryEnabled) {
@@ -3502,7 +4051,7 @@ async function installInJsonConfig(configPath, token, region, includeWrite, tele
3502
4051
  if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
3503
4052
  parsed.mcpServers.leadbay = {
3504
4053
  command: "npx",
3505
- args: ["-y", "@leadbay/mcp@0.13"],
4054
+ args: ["-y", "@leadbay/mcp@0.16"],
3506
4055
  env
3507
4056
  };
3508
4057
  const tmp = configPath + ".tmp";
@@ -3689,7 +4238,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
3689
4238
  process.stderr.write(
3690
4239
  `
3691
4240
  The token was written into client config files but never printed to your terminal.
3692
- Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.13 doctor
4241
+ Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.16 doctor
3693
4242
  Restart your MCP client(s) to pick up the new server.
3694
4243
  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).
3695
4244
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "mcpName": "io.github.leadbay/leadbay-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
6
6
  "type": "module",