@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 +18 -0
- package/README.md +59 -17
- package/dist/bin.js +583 -34
- package/package.json +1 -1
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
|
-
-
|
|
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
|
|
11
|
-
-
|
|
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).
|
|
15
|
-
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
|
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.
|
|
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
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3119
|
-
|
|
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
|
-
|
|
3122
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|