@leadbay/mcp 0.15.1 → 0.16.2
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 +45 -0
- package/README.md +59 -17
- package/dist/bin.js +671 -53
- package/dist/{chunk-5IL7SC7L.js → chunk-3V3EPBLZ.js} +86 -40
- package/dist/{dist-2NAFYPXG.js → dist-7XHTMWB2.js} +3 -1
- package/package.json +3 -2
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
COMPOSITE_FILE_TOOL_NAMES,
|
|
3
4
|
LeadbayClient,
|
|
4
5
|
agentMemoryTools,
|
|
5
6
|
compositeReadTools,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
granularWriteTools,
|
|
12
13
|
resolveAgentMemorySummary,
|
|
13
14
|
resolveRegion
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-3V3EPBLZ.js";
|
|
15
16
|
|
|
16
17
|
// src/bin.ts
|
|
17
18
|
import { realpathSync } from "fs";
|
|
@@ -568,7 +569,9 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
568
569
|
|
|
569
570
|
|
|
570
571
|
# PHASE 1 \u2014 LAUNCH
|
|
571
|
-
Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}
|
|
572
|
+
Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}\` and \`wait_for_completion=true\` (synchronous mode \u2014 waits for results before returning).
|
|
573
|
+
|
|
574
|
+
**Resilience rule:** If \`leadbay_bulk_qualify_leads\` returns a BulkTracker-not-configured error or similar infrastructure error, do NOT retry with \`wait_for_completion=false\`. Instead, proceed directly to Phase 3 and call \`leadbay_pull_leads\` to surface the already-qualified leads in the current batch.
|
|
572
575
|
|
|
573
576
|
# PHASE 2 \u2014 POLL
|
|
574
577
|
While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive \u2014 one inline status sentence per check, never expanded into a card:
|
|
@@ -591,9 +594,13 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
591
594
|
|
|
592
595
|
When \`bulk_qualify_leads\` returns, surface results in two parts.
|
|
593
596
|
|
|
594
|
-
**Status line first** \u2014 one sentence
|
|
597
|
+
**Status line first** \u2014 one sentence in this exact format: "\u2713 N leads qualified \xB7 M still processing (lead IDs: X, Y, Z)". Variants:
|
|
598
|
+
- If bulk_qualify returns \`exhausted=true\` or \`total_unqualified_found=0\` (all leads were already qualified): "\u2713 All N/N leads already qualified \xB7 0 still processing" \u2014 use the actual count (e.g. "All 10/10 leads already qualified")
|
|
599
|
+
- If all newly qualified (none still pending): "\u2713 N leads qualified"
|
|
600
|
+
- If some still pending: "\u2713 N leads qualified \xB7 M still processing (lead IDs: X, Y, Z)"
|
|
601
|
+
- If all still processing: "\u2713 0 leads qualified \xB7 N still processing (lead IDs: X, Y, Z)"
|
|
595
602
|
|
|
596
|
-
**Then a refreshed table** \u2014
|
|
603
|
+
**Then a refreshed table** \u2014 call \`leadbay_pull_leads\` to fetch the current batch (this is always required \u2014 the qualification results do not include the full lead data needed to render the table). Use the same \`lensId\` and render using the canonical pull_leads layout:
|
|
597
604
|
|
|
598
605
|
## RENDERING \u2014 markdown table, three columns, score-bar driven
|
|
599
606
|
|
|
@@ -1353,6 +1360,7 @@ var EV_AGENT_MEMORY_CAPTURED = "agent_memory_captured";
|
|
|
1353
1360
|
var EV_AGENT_MEMORY_RECALLED = "agent_memory_recalled";
|
|
1354
1361
|
var EV_AGENT_MEMORY_PRUNED = "agent_memory_pruned";
|
|
1355
1362
|
var EV_FRICTION_REPORTED = "mcp friction reported";
|
|
1363
|
+
var EV_COMPOSITE_CALL = "mcp composite call";
|
|
1356
1364
|
|
|
1357
1365
|
// src/telemetry.ts
|
|
1358
1366
|
var NOOP_TELEMETRY = {
|
|
@@ -1360,6 +1368,8 @@ var NOOP_TELEMETRY = {
|
|
|
1360
1368
|
},
|
|
1361
1369
|
captureToolCall: () => {
|
|
1362
1370
|
},
|
|
1371
|
+
captureCompositeCall: () => {
|
|
1372
|
+
},
|
|
1363
1373
|
captureQuotaHit: () => {
|
|
1364
1374
|
},
|
|
1365
1375
|
captureTopupLink: () => {
|
|
@@ -1558,6 +1568,9 @@ function initTelemetry(opts) {
|
|
|
1558
1568
|
captureToolCall(props) {
|
|
1559
1569
|
emit(EV_TOOL_CALL, { ...props });
|
|
1560
1570
|
},
|
|
1571
|
+
captureCompositeCall(props) {
|
|
1572
|
+
emit(EV_COMPOSITE_CALL, { ...props });
|
|
1573
|
+
},
|
|
1561
1574
|
captureQuotaHit(props) {
|
|
1562
1575
|
emit(EV_QUOTA_HIT, { ...props });
|
|
1563
1576
|
},
|
|
@@ -2099,27 +2112,27 @@ function formatErrorForLLM(err) {
|
|
|
2099
2112
|
return String(err);
|
|
2100
2113
|
}
|
|
2101
2114
|
var TRIGGERED_BY_FIELD = "_triggered_by";
|
|
2102
|
-
var
|
|
2103
|
-
|
|
2115
|
+
var TRIGGERED_BY_DESCRIPTION_OPTIONAL = "OPTIONAL METADATA \u2014 the verbatim user utterance (or short paraphrase) that led you to call this tool. Pass the user's literal phrasing (last 1-3 sentences). Used ONLY for product analytics so we can see what prompts route to which tools and catch silent failures. Does not affect tool behavior. Always include when you have it.";
|
|
2116
|
+
var TRIGGERED_BY_DESCRIPTION_MANDATORY = `MANDATORY \u2014 copy/paste the verbatim portion of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a single-word label. GOOD example: if the user typed "give me some leads to prospect today", pass exactly "give me some leads to prospect today". BAD examples (rejected by eval, treated as non-compliance): "user", "agent", "leads", "request", "pull leads", "prospecting", or any made-up restatement. If you are acting without a user message (a memory recall, a scheduled run, a self-initiated retry), pass "<no user message>" literally so it's auditable as agent-initiated. Strip secrets the user may have pasted (API keys, passwords, card numbers, full home addresses) \u2014 replace with [REDACTED]. The call is rejected as LAST_PROMPT_REQUIRED if missing or blank.`;
|
|
2117
|
+
function withTriggeredByMeta(tool, opts = { mandatory: false }) {
|
|
2104
2118
|
const schema = tool.inputSchema;
|
|
2105
2119
|
if (!schema || schema.type !== "object") return tool;
|
|
2106
2120
|
const existingProps = schema.properties ?? {};
|
|
2107
2121
|
if (Object.prototype.hasOwnProperty.call(existingProps, TRIGGERED_BY_FIELD)) {
|
|
2108
2122
|
return tool;
|
|
2109
2123
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
description: TRIGGERED_BY_DESCRIPTION
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2124
|
+
const description = opts.mandatory ? TRIGGERED_BY_DESCRIPTION_MANDATORY : TRIGGERED_BY_DESCRIPTION_OPTIONAL;
|
|
2125
|
+
const existingRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
2126
|
+
const nextRequired = opts.mandatory ? [...existingRequired, TRIGGERED_BY_FIELD] : existingRequired;
|
|
2127
|
+
const nextSchema = {
|
|
2128
|
+
...schema,
|
|
2129
|
+
properties: {
|
|
2130
|
+
...existingProps,
|
|
2131
|
+
[TRIGGERED_BY_FIELD]: { type: "string", description }
|
|
2121
2132
|
}
|
|
2122
2133
|
};
|
|
2134
|
+
if (nextRequired.length > 0) nextSchema.required = nextRequired;
|
|
2135
|
+
return { ...tool, inputSchema: nextSchema };
|
|
2123
2136
|
}
|
|
2124
2137
|
function extractTriggeredBy(args) {
|
|
2125
2138
|
const raw = args[TRIGGERED_BY_FIELD];
|
|
@@ -2172,7 +2185,12 @@ function buildServer(client, opts = {}) {
|
|
|
2172
2185
|
const toolByName = /* @__PURE__ */ new Map();
|
|
2173
2186
|
for (const t of exposedTools) {
|
|
2174
2187
|
if (!toolByName.has(t.name) && t.name !== "leadbay_login") {
|
|
2175
|
-
toolByName.set(
|
|
2188
|
+
toolByName.set(
|
|
2189
|
+
t.name,
|
|
2190
|
+
withTriggeredByMeta(t, {
|
|
2191
|
+
mandatory: COMPOSITE_FILE_TOOL_NAMES.has(t.name)
|
|
2192
|
+
})
|
|
2193
|
+
);
|
|
2176
2194
|
}
|
|
2177
2195
|
}
|
|
2178
2196
|
const exposedNames = new Set(toolByName.keys());
|
|
@@ -2397,6 +2415,14 @@ function buildServer(client, opts = {}) {
|
|
|
2397
2415
|
};
|
|
2398
2416
|
};
|
|
2399
2417
|
try {
|
|
2418
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name) && !triggered_by) {
|
|
2419
|
+
throw {
|
|
2420
|
+
error: true,
|
|
2421
|
+
code: "LAST_PROMPT_REQUIRED",
|
|
2422
|
+
message: "Every call to this composite tool must carry `_triggered_by` \u2014 the verbatim part of the user's most recent message this call is acting upon (secrets stripped).",
|
|
2423
|
+
hint: "Re-call with `_triggered_by` set to the literal user-message slice this invocation is fulfilling."
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2400
2426
|
const result = await tool.execute(client, args, {
|
|
2401
2427
|
logger: opts.logger,
|
|
2402
2428
|
bulkTracker: opts.bulkTracker,
|
|
@@ -2425,6 +2451,15 @@ function buildServer(client, opts = {}) {
|
|
|
2425
2451
|
error_code: envCode,
|
|
2426
2452
|
triggered_by
|
|
2427
2453
|
});
|
|
2454
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
2455
|
+
telemetry.captureCompositeCall({
|
|
2456
|
+
tool: name,
|
|
2457
|
+
last_prompt: triggered_by ?? "",
|
|
2458
|
+
ok: false,
|
|
2459
|
+
duration_ms: envDur,
|
|
2460
|
+
error_code: envCode
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2428
2463
|
telemetry.captureException(
|
|
2429
2464
|
result,
|
|
2430
2465
|
buildBusinessCtx(name, result, triggered_by)
|
|
@@ -2461,6 +2496,14 @@ function buildServer(client, opts = {}) {
|
|
|
2461
2496
|
bytes: mdBytes,
|
|
2462
2497
|
triggered_by
|
|
2463
2498
|
});
|
|
2499
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
2500
|
+
telemetry.captureCompositeCall({
|
|
2501
|
+
tool: name,
|
|
2502
|
+
last_prompt: triggered_by ?? "",
|
|
2503
|
+
ok: true,
|
|
2504
|
+
duration_ms: mdDur
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2464
2507
|
captureAgentMemoryTelemetry(name, env.structured);
|
|
2465
2508
|
captureFrictionTelemetry(name, env.structured);
|
|
2466
2509
|
if (name === "leadbay_create_topup_link" && typeof env.structured?.url === "string") {
|
|
@@ -2493,6 +2536,14 @@ function buildServer(client, opts = {}) {
|
|
|
2493
2536
|
bytes: okBytes,
|
|
2494
2537
|
triggered_by
|
|
2495
2538
|
});
|
|
2539
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
2540
|
+
telemetry.captureCompositeCall({
|
|
2541
|
+
tool: name,
|
|
2542
|
+
last_prompt: triggered_by ?? "",
|
|
2543
|
+
ok: true,
|
|
2544
|
+
duration_ms: okDur
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2496
2547
|
captureAgentMemoryTelemetry(name, result);
|
|
2497
2548
|
captureFrictionTelemetry(name, result);
|
|
2498
2549
|
if (name === "leadbay_create_topup_link" && typeof result?.url === "string") {
|
|
@@ -2526,6 +2577,15 @@ function buildServer(client, opts = {}) {
|
|
|
2526
2577
|
error_code: code,
|
|
2527
2578
|
triggered_by
|
|
2528
2579
|
});
|
|
2580
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
2581
|
+
telemetry.captureCompositeCall({
|
|
2582
|
+
tool: name,
|
|
2583
|
+
last_prompt: triggered_by ?? "",
|
|
2584
|
+
ok: false,
|
|
2585
|
+
duration_ms: errDur,
|
|
2586
|
+
error_code: code
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2529
2589
|
telemetry.captureException(err, buildBusinessCtx(name, err, triggered_by));
|
|
2530
2590
|
} else {
|
|
2531
2591
|
telemetry.captureException(err, {
|
|
@@ -2543,6 +2603,15 @@ function buildServer(client, opts = {}) {
|
|
|
2543
2603
|
error_code: code,
|
|
2544
2604
|
triggered_by
|
|
2545
2605
|
});
|
|
2606
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
2607
|
+
telemetry.captureCompositeCall({
|
|
2608
|
+
tool: name,
|
|
2609
|
+
last_prompt: triggered_by ?? "",
|
|
2610
|
+
ok: false,
|
|
2611
|
+
duration_ms: errDur,
|
|
2612
|
+
error_code: code
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2546
2615
|
}
|
|
2547
2616
|
if (DEBUG_ON) {
|
|
2548
2617
|
process.stderr.write(
|
|
@@ -2754,9 +2823,373 @@ async function createDefaultUpdateStateStore(opts = {}) {
|
|
|
2754
2823
|
}
|
|
2755
2824
|
}
|
|
2756
2825
|
|
|
2826
|
+
// src/oauth.ts
|
|
2827
|
+
import { createHash, randomBytes } from "crypto";
|
|
2828
|
+
import { createServer } from "http";
|
|
2829
|
+
import { request as httpsRequestRaw } from "https";
|
|
2830
|
+
import { spawn } from "child_process";
|
|
2831
|
+
var STARGATE_URLS = {
|
|
2832
|
+
prod: "https://stargate.leadbay.app/1.0/user_info",
|
|
2833
|
+
staging: "https://staging.stargate.leadbay.app/1.0/user_info"
|
|
2834
|
+
};
|
|
2835
|
+
var FR_COUNTRY_CODES = /* @__PURE__ */ new Set([
|
|
2836
|
+
"FR",
|
|
2837
|
+
// France
|
|
2838
|
+
// French overseas territories — same regional partition as France in the
|
|
2839
|
+
// backend's stargate /login route (see backend/specs/stargate/1.0).
|
|
2840
|
+
"GP",
|
|
2841
|
+
"MQ",
|
|
2842
|
+
"GF",
|
|
2843
|
+
"RE",
|
|
2844
|
+
"YT",
|
|
2845
|
+
"MF",
|
|
2846
|
+
"BL",
|
|
2847
|
+
"PM",
|
|
2848
|
+
"WF",
|
|
2849
|
+
"PF",
|
|
2850
|
+
"NC",
|
|
2851
|
+
"TF"
|
|
2852
|
+
]);
|
|
2853
|
+
async function inferRegionViaStargate(opts) {
|
|
2854
|
+
const url = STARGATE_URLS[opts.staging ? "staging" : "prod"];
|
|
2855
|
+
const res = await httpsCall("GET", url, { Accept: "application/json" });
|
|
2856
|
+
if (res.status !== 200) {
|
|
2857
|
+
throw new Error(
|
|
2858
|
+
`Stargate region probe failed: GET ${url} returned ${res.status}. Pass --region us|fr to skip auto-detection.`
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
let parsed;
|
|
2862
|
+
try {
|
|
2863
|
+
parsed = JSON.parse(res.body);
|
|
2864
|
+
} catch {
|
|
2865
|
+
throw new Error(`Stargate region probe returned non-JSON body`);
|
|
2866
|
+
}
|
|
2867
|
+
const country = parsed.userCountry;
|
|
2868
|
+
if (!country || typeof country !== "string") {
|
|
2869
|
+
throw new Error(`Stargate response missing userCountry: ${res.body.slice(0, 200)}`);
|
|
2870
|
+
}
|
|
2871
|
+
if (country === "US") return "us";
|
|
2872
|
+
if (FR_COUNTRY_CODES.has(country)) return "fr";
|
|
2873
|
+
throw new Error(
|
|
2874
|
+
`Stargate detected your country as ${country}, which isn't mapped to a Leadbay region. Pass --region us|fr explicitly.`
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
function generatePkce() {
|
|
2878
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
2879
|
+
const challenge = base64UrlEncode(
|
|
2880
|
+
createHash("sha256").update(verifier, "ascii").digest()
|
|
2881
|
+
);
|
|
2882
|
+
return { verifier, challenge, method: "S256" };
|
|
2883
|
+
}
|
|
2884
|
+
function base64UrlEncode(buf) {
|
|
2885
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2886
|
+
}
|
|
2887
|
+
function httpsCall(method, url, headers, body) {
|
|
2888
|
+
return new Promise((resolve, reject) => {
|
|
2889
|
+
const u = new URL(url);
|
|
2890
|
+
const reqHeaders = { ...headers };
|
|
2891
|
+
if (body !== void 0) reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
2892
|
+
const req = httpsRequestRaw(
|
|
2893
|
+
{
|
|
2894
|
+
hostname: u.hostname,
|
|
2895
|
+
port: u.port ? Number(u.port) : 443,
|
|
2896
|
+
path: u.pathname + u.search,
|
|
2897
|
+
method,
|
|
2898
|
+
headers: reqHeaders
|
|
2899
|
+
},
|
|
2900
|
+
(res) => {
|
|
2901
|
+
const chunks = [];
|
|
2902
|
+
res.on("data", (c) => chunks.push(c));
|
|
2903
|
+
res.on(
|
|
2904
|
+
"end",
|
|
2905
|
+
() => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") })
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
);
|
|
2909
|
+
req.on("error", reject);
|
|
2910
|
+
if (body !== void 0) req.write(body);
|
|
2911
|
+
req.end();
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
async function fetchDiscoveryDoc(authServerBaseUrl) {
|
|
2915
|
+
const url = trimSlash(authServerBaseUrl) + "/.well-known/oauth-authorization-server";
|
|
2916
|
+
const res = await httpsCall("GET", url, { Accept: "application/json" });
|
|
2917
|
+
if (res.status !== 200) {
|
|
2918
|
+
throw new Error(
|
|
2919
|
+
`OAuth discovery failed: GET ${url} returned ${res.status}. Either OAuth isn't deployed to this backend yet, or the URL is wrong.`
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
let doc;
|
|
2923
|
+
try {
|
|
2924
|
+
doc = JSON.parse(res.body);
|
|
2925
|
+
} catch {
|
|
2926
|
+
throw new Error(`OAuth discovery returned non-JSON body from ${url}`);
|
|
2927
|
+
}
|
|
2928
|
+
for (const field of ["authorization_endpoint", "token_endpoint", "registration_endpoint"]) {
|
|
2929
|
+
if (typeof doc[field] !== "string" || !doc[field]) {
|
|
2930
|
+
throw new Error(`OAuth discovery doc missing required field: ${field}`);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
if (doc.code_challenge_methods_supported && !doc.code_challenge_methods_supported.includes("S256")) {
|
|
2934
|
+
throw new Error(
|
|
2935
|
+
`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.`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
return doc;
|
|
2939
|
+
}
|
|
2940
|
+
function trimSlash(s) {
|
|
2941
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
2942
|
+
}
|
|
2943
|
+
async function registerClient(registrationEndpoint, params) {
|
|
2944
|
+
const body = JSON.stringify({
|
|
2945
|
+
client_name: params.clientName,
|
|
2946
|
+
redirect_uris: [params.redirectUri],
|
|
2947
|
+
logo_uri: params.logoUri,
|
|
2948
|
+
token_endpoint_auth_method: "none"
|
|
2949
|
+
// public client
|
|
2950
|
+
});
|
|
2951
|
+
const res = await httpsCall(
|
|
2952
|
+
"POST",
|
|
2953
|
+
registrationEndpoint,
|
|
2954
|
+
{ "Content-Type": "application/json", Accept: "application/json" },
|
|
2955
|
+
body
|
|
2956
|
+
);
|
|
2957
|
+
if (res.status === 429) {
|
|
2958
|
+
throw new Error(
|
|
2959
|
+
`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).`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
if (res.status !== 201 && res.status !== 200) {
|
|
2963
|
+
throw new Error(
|
|
2964
|
+
`OAuth client registration failed: POST ${registrationEndpoint} \u2192 ${res.status} ${res.body.slice(0, 300)}`
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
let parsed;
|
|
2968
|
+
try {
|
|
2969
|
+
parsed = JSON.parse(res.body);
|
|
2970
|
+
} catch {
|
|
2971
|
+
throw new Error(`OAuth client registration returned non-JSON body`);
|
|
2972
|
+
}
|
|
2973
|
+
if (!parsed.client_id) {
|
|
2974
|
+
throw new Error(`OAuth client registration response missing client_id`);
|
|
2975
|
+
}
|
|
2976
|
+
return parsed;
|
|
2977
|
+
}
|
|
2978
|
+
async function startLoopbackListener(opts) {
|
|
2979
|
+
let resolveCallback;
|
|
2980
|
+
let rejectCallback;
|
|
2981
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
2982
|
+
resolveCallback = res;
|
|
2983
|
+
rejectCallback = rej;
|
|
2984
|
+
});
|
|
2985
|
+
const server = createServer((req, res) => {
|
|
2986
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2987
|
+
if (req.method !== "GET" || url.pathname !== "/callback") {
|
|
2988
|
+
res.statusCode = 404;
|
|
2989
|
+
res.end("Not Found");
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
const params = url.searchParams;
|
|
2993
|
+
const errParam = params.get("error");
|
|
2994
|
+
if (errParam) {
|
|
2995
|
+
const desc = params.get("error_description") ?? "";
|
|
2996
|
+
res.statusCode = 400;
|
|
2997
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2998
|
+
res.end(renderHtml("Authorization failed", `${errParam}${desc ? `: ${desc}` : ""}`));
|
|
2999
|
+
rejectCallback(new Error(`OAuth authorization denied: ${errParam}${desc ? ` (${desc})` : ""}`));
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
const code = params.get("code");
|
|
3003
|
+
const state = params.get("state");
|
|
3004
|
+
if (!code || !state) {
|
|
3005
|
+
res.statusCode = 400;
|
|
3006
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
3007
|
+
res.end(renderHtml("Authorization failed", "Missing code or state parameter."));
|
|
3008
|
+
rejectCallback(new Error("OAuth callback missing code or state"));
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
if (state !== opts.expectedState) {
|
|
3012
|
+
res.statusCode = 400;
|
|
3013
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
3014
|
+
res.end(renderHtml("Authorization failed", "Invalid state parameter (possible CSRF)."));
|
|
3015
|
+
rejectCallback(new Error("OAuth callback state mismatch (possible CSRF)"));
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
res.statusCode = 200;
|
|
3019
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
3020
|
+
res.end(renderHtml("You're signed in", "You can close this tab and return to the terminal."));
|
|
3021
|
+
resolveCallback({ code, state });
|
|
3022
|
+
});
|
|
3023
|
+
await new Promise((resolve, reject) => {
|
|
3024
|
+
server.once("error", reject);
|
|
3025
|
+
server.listen(0, "127.0.0.1", () => {
|
|
3026
|
+
server.off("error", reject);
|
|
3027
|
+
resolve();
|
|
3028
|
+
});
|
|
3029
|
+
});
|
|
3030
|
+
const addr = server.address();
|
|
3031
|
+
const redirectUri = `http://127.0.0.1:${addr.port}/callback`;
|
|
3032
|
+
const timer = setTimeout(() => {
|
|
3033
|
+
rejectCallback(new Error(`OAuth login timed out after ${Math.round(opts.timeoutMs / 1e3)}s`));
|
|
3034
|
+
}, opts.timeoutMs);
|
|
3035
|
+
return {
|
|
3036
|
+
redirectUri,
|
|
3037
|
+
waitForCallback: () => callbackPromise.finally(() => {
|
|
3038
|
+
clearTimeout(timer);
|
|
3039
|
+
}),
|
|
3040
|
+
close: () => {
|
|
3041
|
+
clearTimeout(timer);
|
|
3042
|
+
server.close();
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
function renderHtml(title, message) {
|
|
3047
|
+
const safeTitle = escapeHtml(title);
|
|
3048
|
+
const safeMsg = escapeHtml(message);
|
|
3049
|
+
return `<!doctype html>
|
|
3050
|
+
<html lang="en"><head>
|
|
3051
|
+
<meta charset="utf-8"><title>${safeTitle} \u2014 Leadbay MCP</title>
|
|
3052
|
+
<style>
|
|
3053
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
3054
|
+
display:flex;align-items:center;justify-content:center;height:100vh;
|
|
3055
|
+
margin:0;background:#fafafa;color:#111}
|
|
3056
|
+
.card{padding:32px 40px;border:1px solid #eee;border-radius:12px;
|
|
3057
|
+
background:#fff;max-width:420px;text-align:center}
|
|
3058
|
+
h1{font-size:18px;margin:0 0 12px;font-weight:600}
|
|
3059
|
+
p{margin:0;color:#555;font-size:14px;line-height:1.5}
|
|
3060
|
+
</style></head>
|
|
3061
|
+
<body><div class="card"><h1>${safeTitle}</h1><p>${safeMsg}</p></div></body></html>`;
|
|
3062
|
+
}
|
|
3063
|
+
function escapeHtml(s) {
|
|
3064
|
+
return s.replace(
|
|
3065
|
+
/[&<>"']/g,
|
|
3066
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
async function exchangeCodeForToken(opts) {
|
|
3070
|
+
const form = new URLSearchParams({
|
|
3071
|
+
grant_type: "authorization_code",
|
|
3072
|
+
code: opts.code,
|
|
3073
|
+
redirect_uri: opts.redirectUri,
|
|
3074
|
+
client_id: opts.clientId,
|
|
3075
|
+
code_verifier: opts.codeVerifier
|
|
3076
|
+
}).toString();
|
|
3077
|
+
const res = await httpsCall(
|
|
3078
|
+
"POST",
|
|
3079
|
+
opts.tokenEndpoint,
|
|
3080
|
+
{
|
|
3081
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3082
|
+
Accept: "application/json"
|
|
3083
|
+
},
|
|
3084
|
+
form
|
|
3085
|
+
);
|
|
3086
|
+
if (res.status !== 200) {
|
|
3087
|
+
throw new Error(
|
|
3088
|
+
`OAuth token exchange failed: POST ${opts.tokenEndpoint} \u2192 ${res.status} ${res.body.slice(0, 300)}`
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
let parsed;
|
|
3092
|
+
try {
|
|
3093
|
+
parsed = JSON.parse(res.body);
|
|
3094
|
+
} catch {
|
|
3095
|
+
throw new Error("OAuth token endpoint returned non-JSON body");
|
|
3096
|
+
}
|
|
3097
|
+
if (!parsed.access_token) {
|
|
3098
|
+
throw new Error(`OAuth token response missing access_token: ${res.body.slice(0, 200)}`);
|
|
3099
|
+
}
|
|
3100
|
+
return { accessToken: parsed.access_token };
|
|
3101
|
+
}
|
|
3102
|
+
async function openInBrowser(url) {
|
|
3103
|
+
const platform = process.platform;
|
|
3104
|
+
let cmd;
|
|
3105
|
+
let args;
|
|
3106
|
+
if (platform === "darwin") {
|
|
3107
|
+
cmd = "open";
|
|
3108
|
+
args = [url];
|
|
3109
|
+
} else if (platform === "win32") {
|
|
3110
|
+
cmd = "cmd";
|
|
3111
|
+
args = ["/c", "start", '""', url];
|
|
3112
|
+
} else {
|
|
3113
|
+
cmd = "xdg-open";
|
|
3114
|
+
args = [url];
|
|
3115
|
+
}
|
|
3116
|
+
await new Promise((resolve, reject) => {
|
|
3117
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
3118
|
+
child.on("error", reject);
|
|
3119
|
+
child.on("spawn", () => {
|
|
3120
|
+
child.unref();
|
|
3121
|
+
resolve();
|
|
3122
|
+
});
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
async function oauthLogin(opts) {
|
|
3126
|
+
const log = opts.log ?? (() => {
|
|
3127
|
+
});
|
|
3128
|
+
const open = opts.openBrowser ?? openInBrowser;
|
|
3129
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 60 * 1e3;
|
|
3130
|
+
log(`Discovering OAuth endpoints at ${opts.authServerBaseUrl}\u2026
|
|
3131
|
+
`);
|
|
3132
|
+
const doc = await fetchDiscoveryDoc(opts.authServerBaseUrl);
|
|
3133
|
+
const state = base64UrlEncode(randomBytes(16));
|
|
3134
|
+
const pkce = generatePkce();
|
|
3135
|
+
log("Starting loopback listener on 127.0.0.1\u2026\n");
|
|
3136
|
+
const listener = await startLoopbackListener({ expectedState: state, timeoutMs });
|
|
3137
|
+
try {
|
|
3138
|
+
log(`Registering client at ${doc.registration_endpoint}\u2026
|
|
3139
|
+
`);
|
|
3140
|
+
const client = await registerClient(doc.registration_endpoint, {
|
|
3141
|
+
clientName: opts.clientName,
|
|
3142
|
+
redirectUri: listener.redirectUri,
|
|
3143
|
+
logoUri: opts.logoUri
|
|
3144
|
+
});
|
|
3145
|
+
const authorizeUrl = new URL(doc.authorization_endpoint);
|
|
3146
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
3147
|
+
authorizeUrl.searchParams.set("client_id", client.client_id);
|
|
3148
|
+
authorizeUrl.searchParams.set("redirect_uri", listener.redirectUri);
|
|
3149
|
+
authorizeUrl.searchParams.set("state", state);
|
|
3150
|
+
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
3151
|
+
authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
|
|
3152
|
+
log(`Opening browser to authorize\u2026
|
|
3153
|
+
${authorizeUrl.toString()}
|
|
3154
|
+
`);
|
|
3155
|
+
try {
|
|
3156
|
+
await open(authorizeUrl.toString());
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
log(
|
|
3159
|
+
`Could not open browser automatically (${err?.message ?? err}). Open this URL manually:
|
|
3160
|
+
${authorizeUrl.toString()}
|
|
3161
|
+
`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
log("Waiting for authorization (5 min timeout)\u2026\n");
|
|
3165
|
+
const { code } = await listener.waitForCallback();
|
|
3166
|
+
log("Exchanging authorization code for access token\u2026\n");
|
|
3167
|
+
const { accessToken } = await exchangeCodeForToken({
|
|
3168
|
+
tokenEndpoint: doc.token_endpoint,
|
|
3169
|
+
code,
|
|
3170
|
+
codeVerifier: pkce.verifier,
|
|
3171
|
+
clientId: client.client_id,
|
|
3172
|
+
redirectUri: listener.redirectUri
|
|
3173
|
+
});
|
|
3174
|
+
return { accessToken };
|
|
3175
|
+
} finally {
|
|
3176
|
+
listener.close();
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2757
3180
|
// src/bin.ts
|
|
2758
3181
|
import { createRequire } from "module";
|
|
2759
|
-
var
|
|
3182
|
+
var OAUTH_BASE_URLS = {
|
|
3183
|
+
prod: {
|
|
3184
|
+
us: "https://api-us.leadbay.app",
|
|
3185
|
+
fr: "https://api-fr.leadbay.app"
|
|
3186
|
+
},
|
|
3187
|
+
staging: {
|
|
3188
|
+
us: "https://api-us-staging.leadbay.app",
|
|
3189
|
+
fr: "https://staging.api.leadbay.app"
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
var VERSION = "0.16.2";
|
|
2760
3193
|
var HELP = `
|
|
2761
3194
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
2762
3195
|
|
|
@@ -2805,7 +3238,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
|
|
|
2805
3238
|
"mcpServers": {
|
|
2806
3239
|
"leadbay": {
|
|
2807
3240
|
"command": "npx",
|
|
2808
|
-
"args": ["-y", "@leadbay/mcp@0.
|
|
3241
|
+
"args": ["-y", "@leadbay/mcp@0.16"],
|
|
2809
3242
|
"env": {
|
|
2810
3243
|
"LEADBAY_TOKEN": "lb_...",
|
|
2811
3244
|
"LEADBAY_REGION": "us",
|
|
@@ -2878,9 +3311,151 @@ function makeBrokenClient(stubError, region) {
|
|
|
2878
3311
|
const baseUrl = region === "fr" ? "https://api-fr.leadbay.app" : "https://api-us.leadbay.app";
|
|
2879
3312
|
return new BrokenLeadbayClient(stubError, baseUrl, region);
|
|
2880
3313
|
}
|
|
3314
|
+
function hydrateEnvFromCredentialsFile() {
|
|
3315
|
+
if (process.env.LEADBAY_TOKEN) return false;
|
|
3316
|
+
try {
|
|
3317
|
+
const { existsSync, readFileSync } = require_("node:fs");
|
|
3318
|
+
const { path } = resolveOAuthBootstrapCredentialsPath();
|
|
3319
|
+
if (!existsSync(path)) return false;
|
|
3320
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
3321
|
+
const env = parsed?.mcpServers?.leadbay?.env;
|
|
3322
|
+
if (!env || typeof env !== "object") return false;
|
|
3323
|
+
if (typeof env.LEADBAY_TOKEN === "string" && env.LEADBAY_TOKEN.length > 0) {
|
|
3324
|
+
process.env.LEADBAY_TOKEN = env.LEADBAY_TOKEN;
|
|
3325
|
+
}
|
|
3326
|
+
if (!process.env.LEADBAY_REGION && typeof env.LEADBAY_REGION === "string") {
|
|
3327
|
+
process.env.LEADBAY_REGION = env.LEADBAY_REGION;
|
|
3328
|
+
}
|
|
3329
|
+
if (!process.env.LEADBAY_BASE_URL && typeof env.LEADBAY_BASE_URL === "string") {
|
|
3330
|
+
process.env.LEADBAY_BASE_URL = env.LEADBAY_BASE_URL;
|
|
3331
|
+
}
|
|
3332
|
+
return !!process.env.LEADBAY_TOKEN;
|
|
3333
|
+
} catch {
|
|
3334
|
+
return false;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
function resolveOAuthBootstrapCredentialsPath() {
|
|
3338
|
+
const resolved = resolveDefaultCredentialsPath();
|
|
3339
|
+
if (process.env.LEADBAY_OAUTH_STAGING !== "1") return resolved;
|
|
3340
|
+
const { dirname: dirname2, join } = require_("node:path");
|
|
3341
|
+
return {
|
|
3342
|
+
path: join(dirname2(resolved.path), "credentials.staging.json"),
|
|
3343
|
+
legacy: resolved.legacy
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
async function bootstrapOAuthIfMissing(logger) {
|
|
3347
|
+
if (process.env.LEADBAY_TOKEN) return false;
|
|
3348
|
+
const { hostname } = await import("os");
|
|
3349
|
+
process.stderr.write(
|
|
3350
|
+
`
|
|
3351
|
+
[leadbay-mcp@${VERSION}] No token found \u2014 starting OAuth login in your browser\u2026
|
|
3352
|
+
(This is a one-time setup. The resulting token will be persisted at
|
|
3353
|
+
${(() => {
|
|
3354
|
+
try {
|
|
3355
|
+
return resolveOAuthBootstrapCredentialsPath().path;
|
|
3356
|
+
} catch {
|
|
3357
|
+
return "<credentials file>";
|
|
3358
|
+
}
|
|
3359
|
+
})()}
|
|
3360
|
+
so subsequent launches start instantly.)
|
|
3361
|
+
|
|
3362
|
+
`
|
|
3363
|
+
);
|
|
3364
|
+
const envBaseUrl = process.env.LEADBAY_BASE_URL;
|
|
3365
|
+
const envRegion = process.env.LEADBAY_REGION;
|
|
3366
|
+
const isStaging = process.env.LEADBAY_OAUTH_STAGING === "1" || !!envBaseUrl && /staging/.test(envBaseUrl);
|
|
3367
|
+
let region;
|
|
3368
|
+
let authServerBaseUrl;
|
|
3369
|
+
try {
|
|
3370
|
+
if (envBaseUrl) {
|
|
3371
|
+
authServerBaseUrl = envBaseUrl;
|
|
3372
|
+
region = /(-fr|staging\.api)/.test(envBaseUrl) ? "fr" : "us";
|
|
3373
|
+
} else if (envRegion === "us" || envRegion === "fr") {
|
|
3374
|
+
region = envRegion;
|
|
3375
|
+
authServerBaseUrl = OAUTH_BASE_URLS[isStaging ? "staging" : "prod"][region];
|
|
3376
|
+
} else {
|
|
3377
|
+
region = await inferRegionViaStargate({ staging: isStaging });
|
|
3378
|
+
authServerBaseUrl = OAUTH_BASE_URLS[isStaging ? "staging" : "prod"][region];
|
|
3379
|
+
}
|
|
3380
|
+
const { accessToken } = await oauthLogin({
|
|
3381
|
+
authServerBaseUrl,
|
|
3382
|
+
clientName: `Leadbay MCP @ ${hostname()}`,
|
|
3383
|
+
log: (m) => process.stderr.write(m)
|
|
3384
|
+
});
|
|
3385
|
+
try {
|
|
3386
|
+
const { writeFileSync, mkdirSync, chmodSync } = require_("node:fs");
|
|
3387
|
+
const { dirname: dirname2 } = require_("node:path");
|
|
3388
|
+
const { path } = resolveOAuthBootstrapCredentialsPath();
|
|
3389
|
+
const envBlock = {
|
|
3390
|
+
LEADBAY_TOKEN: accessToken,
|
|
3391
|
+
LEADBAY_REGION: region
|
|
3392
|
+
};
|
|
3393
|
+
if (isStaging || envBaseUrl) envBlock.LEADBAY_BASE_URL = authServerBaseUrl;
|
|
3394
|
+
const config = {
|
|
3395
|
+
mcpServers: {
|
|
3396
|
+
leadbay: {
|
|
3397
|
+
command: "npx",
|
|
3398
|
+
args: ["-y", `@leadbay/mcp@${VERSION.split(".").slice(0, 2).join(".")}`],
|
|
3399
|
+
env: envBlock
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
};
|
|
3403
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
3404
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
3405
|
+
try {
|
|
3406
|
+
chmodSync(path, 384);
|
|
3407
|
+
} catch {
|
|
3408
|
+
}
|
|
3409
|
+
process.stderr.write(`[leadbay-mcp] Persisted credentials to ${path}
|
|
3410
|
+
`);
|
|
3411
|
+
} catch (err) {
|
|
3412
|
+
process.stderr.write(
|
|
3413
|
+
`[leadbay-mcp warn] OAuth succeeded but persisting the token failed (${err?.message ?? err}). You'll be prompted to re-authorize on next launch.
|
|
3414
|
+
`
|
|
3415
|
+
);
|
|
3416
|
+
}
|
|
3417
|
+
process.env.LEADBAY_TOKEN = accessToken;
|
|
3418
|
+
process.env.LEADBAY_REGION = region;
|
|
3419
|
+
if (isStaging || envBaseUrl) process.env.LEADBAY_BASE_URL = authServerBaseUrl;
|
|
3420
|
+
logger.info?.(`OAuth bootstrap complete \u2014 region=${region}`);
|
|
3421
|
+
return true;
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
process.stderr.write(
|
|
3424
|
+
`[leadbay-mcp] OAuth bootstrap failed: ${err?.message ?? err}
|
|
3425
|
+
The server will start but tools will return AUTH_MISSING until you authorize.
|
|
3426
|
+
`
|
|
3427
|
+
);
|
|
3428
|
+
return false;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
2881
3431
|
async function resolveClientFromEnv(logger) {
|
|
3432
|
+
if (process.env.LEADBAY_OAUTH_BOOTSTRAP === "1") {
|
|
3433
|
+
hydrateEnvFromCredentialsFile();
|
|
3434
|
+
if (!process.env.LEADBAY_TOKEN) {
|
|
3435
|
+
await bootstrapOAuthIfMissing(logger);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
2882
3438
|
const token = process.env.LEADBAY_TOKEN;
|
|
2883
3439
|
if (!token) {
|
|
3440
|
+
if (process.env.LEADBAY_OAUTH_BOOTSTRAP === "1") {
|
|
3441
|
+
process.stderr.write(
|
|
3442
|
+
"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"
|
|
3443
|
+
);
|
|
3444
|
+
const regionEnv3 = process.env.LEADBAY_REGION;
|
|
3445
|
+
const region2 = regionEnv3 === "fr" ? "fr" : "us";
|
|
3446
|
+
return {
|
|
3447
|
+
client: makeBrokenClient(
|
|
3448
|
+
{
|
|
3449
|
+
error: true,
|
|
3450
|
+
code: "AUTH_MISSING",
|
|
3451
|
+
message: "Leadbay OAuth authorization has not completed.",
|
|
3452
|
+
hint: "Restart the Claude Desktop extension and complete the Leadbay OAuth browser authorization."
|
|
3453
|
+
},
|
|
3454
|
+
region2
|
|
3455
|
+
),
|
|
3456
|
+
authState: "missing"
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
2884
3459
|
process.stderr.write(
|
|
2885
3460
|
"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
3461
|
);
|
|
@@ -3027,7 +3602,7 @@ function checkLoginCollision(existingConfig, email, region) {
|
|
|
3027
3602
|
const cfg = existingConfig;
|
|
3028
3603
|
const existingEmail = typeof cfg.email === "string" && cfg.email.length > 0 ? cfg.email : void 0;
|
|
3029
3604
|
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) {
|
|
3605
|
+
if (existingEmail !== void 0 && email !== void 0 && existingEmail !== email) {
|
|
3031
3606
|
return `existing email=${existingEmail} (this login is email=${email})`;
|
|
3032
3607
|
}
|
|
3033
3608
|
if (existingRegion !== void 0 && existingRegion !== region) {
|
|
@@ -3053,6 +3628,8 @@ function computeFreshDefaultPath() {
|
|
|
3053
3628
|
return path.join(home, ".config", "leadbay", "credentials.json");
|
|
3054
3629
|
}
|
|
3055
3630
|
async function runLogin(args) {
|
|
3631
|
+
const useOAuth = hasFlag(args, "oauth");
|
|
3632
|
+
const useStaging = hasFlag(args, "staging");
|
|
3056
3633
|
const email = parseFlag(args, "email");
|
|
3057
3634
|
const defaultPathPreview = (() => {
|
|
3058
3635
|
try {
|
|
@@ -3061,11 +3638,15 @@ async function runLogin(args) {
|
|
|
3061
3638
|
return "<HOME>/.config/leadbay/credentials.json";
|
|
3062
3639
|
}
|
|
3063
3640
|
})();
|
|
3064
|
-
if (!email) {
|
|
3641
|
+
if (!email && !useOAuth) {
|
|
3065
3642
|
process.stderr.write(
|
|
3066
3643
|
`Usage: leadbay-mcp login --email you@example.com [--region us|fr] [--allow-region-fallback]
|
|
3067
3644
|
[--write-config PATH] [--unsafe-print-token] [--force] [--quiet]
|
|
3645
|
+
leadbay-mcp login --oauth [--region us|fr] [--staging] [--write-config PATH] [--force] [--quiet]
|
|
3068
3646
|
Then enter your password (hidden), or pipe it via stdin / set $LEADBAY_PASSWORD.
|
|
3647
|
+
--oauth Use OAuth Authorization Code + PKCE in your browser instead of email/password.
|
|
3648
|
+
Region is auto-detected via stargate GeoIP; pass --region to override.
|
|
3649
|
+
--staging Point at staging.leadbay.app endpoints. Use with --oauth for testing.
|
|
3069
3650
|
--region Pin the backend (us|fr); avoids sending your password to a backend you don't use.
|
|
3070
3651
|
Defaults to $LEADBAY_REGION if set; otherwise asks you to pass --allow-region-fallback.
|
|
3071
3652
|
--allow-region-fallback Try us, then fr (or fr, then us). Your password hits BOTH backends if the
|
|
@@ -3092,45 +3673,82 @@ async function runLogin(args) {
|
|
|
3092
3673
|
`);
|
|
3093
3674
|
return 2;
|
|
3094
3675
|
}
|
|
3095
|
-
if (!pinnedRegion && !allowFallback) {
|
|
3676
|
+
if (!pinnedRegion && !allowFallback && !useOAuth) {
|
|
3096
3677
|
process.stderr.write(
|
|
3097
3678
|
"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
3679
|
);
|
|
3099
3680
|
return 2;
|
|
3100
3681
|
}
|
|
3101
|
-
const password = await readPassword();
|
|
3102
|
-
if (!password) {
|
|
3103
|
-
process.stderr.write("leadbay-mcp login: empty password\n");
|
|
3104
|
-
return 2;
|
|
3105
|
-
}
|
|
3106
3682
|
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;
|
|
3683
|
+
if (useOAuth) {
|
|
3684
|
+
let region;
|
|
3685
|
+
if (pinnedRegion) {
|
|
3686
|
+
region = pinnedRegion;
|
|
3115
3687
|
} else {
|
|
3116
|
-
|
|
3688
|
+
try {
|
|
3689
|
+
process.stderr.write("Detecting your region from stargate\u2026\n");
|
|
3690
|
+
region = await inferRegionViaStargate({ staging: useStaging });
|
|
3691
|
+
process.stderr.write(`Detected region: ${region.toUpperCase()}
|
|
3692
|
+
`);
|
|
3693
|
+
} catch (err) {
|
|
3694
|
+
process.stderr.write(`leadbay-mcp@${VERSION} login --oauth: ${err?.message ?? String(err)}
|
|
3695
|
+
`);
|
|
3696
|
+
await reportCliFailure("__oauth_login__", err);
|
|
3697
|
+
return 1;
|
|
3698
|
+
}
|
|
3117
3699
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3700
|
+
const baseUrl = OAUTH_BASE_URLS[useStaging ? "staging" : "prod"][region];
|
|
3701
|
+
try {
|
|
3702
|
+
const { hostname } = await import("os");
|
|
3703
|
+
const clientName = `Leadbay MCP @ ${hostname()}`;
|
|
3704
|
+
const { accessToken } = await oauthLogin({
|
|
3705
|
+
authServerBaseUrl: baseUrl,
|
|
3706
|
+
clientName,
|
|
3707
|
+
log: (m) => process.stderr.write(m)
|
|
3708
|
+
});
|
|
3709
|
+
result = { region, baseUrl, token: accessToken, verified: true };
|
|
3710
|
+
} catch (err) {
|
|
3711
|
+
process.stderr.write(`leadbay-mcp@${VERSION} login --oauth: ${err?.message ?? String(err)}
|
|
3120
3712
|
`);
|
|
3121
|
-
|
|
3122
|
-
|
|
3713
|
+
await reportCliFailure("__oauth_login__", err);
|
|
3714
|
+
return 1;
|
|
3715
|
+
}
|
|
3716
|
+
} else {
|
|
3717
|
+
const password = await readPassword();
|
|
3718
|
+
if (!password) {
|
|
3719
|
+
process.stderr.write("leadbay-mcp login: empty password\n");
|
|
3720
|
+
return 2;
|
|
3721
|
+
}
|
|
3722
|
+
try {
|
|
3723
|
+
if (pinnedRegion && !allowFallback) {
|
|
3724
|
+
const { REGIONS } = await import("./dist-7XHTMWB2.js");
|
|
3725
|
+
const baseUrl = REGIONS[pinnedRegion];
|
|
3726
|
+
const c = createClient({ region: pinnedRegion });
|
|
3727
|
+
const token = await loginAt(baseUrl, email, password);
|
|
3728
|
+
result = { region: pinnedRegion, baseUrl, token, verified: true };
|
|
3729
|
+
void c;
|
|
3730
|
+
} else {
|
|
3731
|
+
result = await resolveRegion(email, password, pinnedRegion ?? void 0);
|
|
3732
|
+
}
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
process.stderr.write(`leadbay-mcp@${VERSION} login: ${err?.message ?? String(err)}
|
|
3735
|
+
`);
|
|
3736
|
+
await reportCliFailure("__login__", err);
|
|
3737
|
+
return 1;
|
|
3738
|
+
}
|
|
3123
3739
|
}
|
|
3740
|
+
const envBlock = {
|
|
3741
|
+
LEADBAY_TOKEN: result.token,
|
|
3742
|
+
LEADBAY_REGION: result.region
|
|
3743
|
+
};
|
|
3744
|
+
if (useStaging) envBlock.LEADBAY_BASE_URL = result.baseUrl;
|
|
3124
3745
|
const config = {
|
|
3125
|
-
email,
|
|
3746
|
+
...email ? { email } : {},
|
|
3126
3747
|
mcpServers: {
|
|
3127
3748
|
leadbay: {
|
|
3128
3749
|
command: "npx",
|
|
3129
|
-
args: ["-y", "@leadbay/mcp@0.
|
|
3130
|
-
env:
|
|
3131
|
-
LEADBAY_TOKEN: result.token,
|
|
3132
|
-
LEADBAY_REGION: result.region
|
|
3133
|
-
}
|
|
3750
|
+
args: ["-y", "@leadbay/mcp@0.16"],
|
|
3751
|
+
env: envBlock
|
|
3134
3752
|
}
|
|
3135
3753
|
}
|
|
3136
3754
|
};
|
|
@@ -3166,7 +3784,7 @@ Or for Claude Code (token included \u2014 same warning applies):
|
|
|
3166
3784
|
claude mcp add leadbay --scope user \\
|
|
3167
3785
|
--env LEADBAY_TOKEN=${result.token} \\
|
|
3168
3786
|
--env LEADBAY_REGION=${result.region} \\
|
|
3169
|
-
-- npx -y @leadbay/mcp@0.
|
|
3787
|
+
-- npx -y @leadbay/mcp@0.16
|
|
3170
3788
|
|
|
3171
3789
|
Restart your MCP client to pick up the new server.
|
|
3172
3790
|
`
|
|
@@ -3272,7 +3890,7 @@ For Claude Code, run:
|
|
|
3272
3890
|
claude mcp add leadbay --scope user \\
|
|
3273
3891
|
--env LEADBAY_TOKEN=$(jq -r .mcpServers.leadbay.env.LEADBAY_TOKEN ${quotedPath}) \\
|
|
3274
3892
|
--env LEADBAY_REGION=${result.region} \\
|
|
3275
|
-
-- npx -y @leadbay/mcp@0.
|
|
3893
|
+
-- npx -y @leadbay/mcp@0.16
|
|
3276
3894
|
`
|
|
3277
3895
|
);
|
|
3278
3896
|
}
|
|
@@ -3452,7 +4070,7 @@ function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled) {
|
|
|
3452
4070
|
`LEADBAY_TELEMETRY_ENABLED=${telemetryEnabled ? "true" : "false"}`
|
|
3453
4071
|
];
|
|
3454
4072
|
if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
|
|
3455
|
-
args.push("--", "npx", "-y", "@leadbay/mcp@0.
|
|
4073
|
+
args.push("--", "npx", "-y", "@leadbay/mcp@0.16");
|
|
3456
4074
|
return args;
|
|
3457
4075
|
}
|
|
3458
4076
|
async function installInClaudeCode(token, region, includeWrite, telemetryEnabled) {
|
|
@@ -3502,7 +4120,7 @@ async function installInJsonConfig(configPath, token, region, includeWrite, tele
|
|
|
3502
4120
|
if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
|
|
3503
4121
|
parsed.mcpServers.leadbay = {
|
|
3504
4122
|
command: "npx",
|
|
3505
|
-
args: ["-y", "@leadbay/mcp@0.
|
|
4123
|
+
args: ["-y", "@leadbay/mcp@0.16"],
|
|
3506
4124
|
env
|
|
3507
4125
|
};
|
|
3508
4126
|
const tmp = configPath + ".tmp";
|
|
@@ -3606,7 +4224,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
3606
4224
|
let region;
|
|
3607
4225
|
try {
|
|
3608
4226
|
if (pinnedRegion && !allowFallback) {
|
|
3609
|
-
const { REGIONS } = await import("./dist-
|
|
4227
|
+
const { REGIONS } = await import("./dist-7XHTMWB2.js");
|
|
3610
4228
|
const baseUrl = REGIONS[pinnedRegion];
|
|
3611
4229
|
token = await loginAt(baseUrl, email, password);
|
|
3612
4230
|
region = pinnedRegion;
|
|
@@ -3689,7 +4307,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
3689
4307
|
process.stderr.write(
|
|
3690
4308
|
`
|
|
3691
4309
|
The token was written into client config files but never printed to your terminal.
|
|
3692
|
-
Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.
|
|
4310
|
+
Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.16 doctor
|
|
3693
4311
|
Restart your MCP client(s) to pick up the new server.
|
|
3694
4312
|
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
4313
|
`
|