@leadbay/mcp 0.32.4 → 0.32.6
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 +56 -1
- package/dist/bin.js +43 -21
- package/dist/http-server.js +42 -20
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.32.6 — 2026-09-01
|
|
4
|
+
|
|
5
|
+
The 401 auto-retry is GET-only — replaying a write could double-execute a
|
|
6
|
+
mutation that already committed — but `mapErrorResponse` wrote its hint as if
|
|
7
|
+
`request()` had always retried. On `leadbay_create_topup_link` (a POST) the
|
|
8
|
+
agent was told the call had already been retried when it had been attempted
|
|
9
|
+
exactly once. Covers acceptance criteria 4 and 5 of product#3998; the 401 itself
|
|
10
|
+
is a backend bug fixed in leadbay/backend#1989.
|
|
11
|
+
|
|
12
|
+
- The GET-only rule is extracted into `retriesOn401` so the retry path and the
|
|
13
|
+
error mapper read one source of truth, and the actual outcome is threaded into
|
|
14
|
+
`mapErrorResponse` rather than assumed.
|
|
15
|
+
- The not-retried hint states only the fact and never the reason. A GET with
|
|
16
|
+
`retryOn401:false` (the startup auth probe) is not a write, and the earlier
|
|
17
|
+
text told it that it was. Why the retry didn't run is not actionable for the
|
|
18
|
+
agent — only that this was attempt one — so the flag stays a boolean.
|
|
19
|
+
- `readOnlyHint: false` on both Stripe tools. Each mints a Stripe session, and
|
|
20
|
+
for an org with no customer yet the shared `getStripeCustomer` path creates
|
|
21
|
+
the customer and persists `organizations.stripe_customer_id`.
|
|
22
|
+
- The two Stripe tools stay in `granularReadTools`. `readOnlyHint` (a client
|
|
23
|
+
confirmation hint) and `includeWrite` (a capability gate over the user's
|
|
24
|
+
Leadbay data) are different axes, and this repo already treats them that way
|
|
25
|
+
— `leadbay_preview_bulk_enrichment` sits in `granularWriteTools` with
|
|
26
|
+
`readOnlyHint: true`. Moving them would leave a `--no-write` user who hits
|
|
27
|
+
quota with no top-up link at all, which is the exact failure product#3998 was
|
|
28
|
+
filed for.
|
|
29
|
+
|
|
30
|
+
## 0.32.5 — 2026-09-01
|
|
31
|
+
|
|
32
|
+
`leadbay_update_contact` returned `NOT_FOUND` / 404 on 100% of the calls ever
|
|
33
|
+
made to it (product#3997). Not a missing route and not a backend bug.
|
|
34
|
+
|
|
35
|
+
Leadbay holds contacts in two id namespaces — `org_contacts` (the org's own
|
|
36
|
+
directory) and `paid_contacts` (enrichment results) — with separate models,
|
|
37
|
+
separate DAOs and separate ids. `POST /contacts/{id}/update` resolves
|
|
38
|
+
`orgContacts.findById` only, so a paid id can only ever 404.
|
|
39
|
+
|
|
40
|
+
`research_lead_by_id` merges `/leads/{id}/enrich/contacts` and
|
|
41
|
+
`/leads/{id}/contacts` into `contacts.reachable` / `contacts.candidates`, split
|
|
42
|
+
by **whether the person is messagable right now, not by which endpoint they came
|
|
43
|
+
from**. That split is right for outreach and wrong for identity: an enriched
|
|
44
|
+
paid contact sits in the same list as an org contact, and the agent had nothing
|
|
45
|
+
to distinguish them by. Roughly half the ids we handed out were unusable by a
|
|
46
|
+
tool the server instructions tell the agent to call after every outreach.
|
|
47
|
+
|
|
48
|
+
- The `source` field (`"org"` / `"paid"`) was already on the wire but undeclared
|
|
49
|
+
and unexplained. It is now documented in `research_lead_by_id`'s output schema
|
|
50
|
+
as the thing that decides whether an id can be passed to
|
|
51
|
+
`leadbay_update_contact`.
|
|
52
|
+
- `leadbay_update_contact`'s description states which namespace it accepts,
|
|
53
|
+
what a 404 means, and routes a correction to a paid contact through
|
|
54
|
+
`leadbay_add_contact` instead — the enrichment row is a provider's answer and
|
|
55
|
+
is not ours to edit.
|
|
56
|
+
- New: `research-contact-source-provenance.test.ts`, covering the case the
|
|
57
|
+
reachability split hides — an enriched paid contact and an org contact in the
|
|
58
|
+
SAME `reachable` list, distinguished only by `source`.
|
|
3
59
|
## 0.32.4 — 2026-09-01
|
|
4
60
|
|
|
5
61
|
`leadbay_account_status.notifications` was permanently `[]` on the hosted server
|
|
@@ -27,7 +83,6 @@ answer, and the ledger is not called.
|
|
|
27
83
|
responses.** Reviving it would mean a `GET /notifications` per tool call to
|
|
28
84
|
decorate every response. The cost lands on the check-in entry point only,
|
|
29
85
|
which is where the daily-rhythm channel is actually read.
|
|
30
|
-
|
|
31
86
|
## 0.32.0 — 2026-09-01
|
|
32
87
|
|
|
33
88
|
A poll-budget timeout stops being an error (product#4007). The import wizard's
|
package/dist/bin.js
CHANGED
|
@@ -213,12 +213,13 @@ function parseRetryAfter(value) {
|
|
|
213
213
|
}
|
|
214
214
|
return null;
|
|
215
215
|
}
|
|
216
|
-
var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, DEFAULT_REQUEST_TIMEOUT_MS, requestSignalStore, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
|
|
216
|
+
var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, retriesOn401, ME_CACHE_TTL_MS, MAX_CONCURRENT, DEFAULT_REQUEST_TIMEOUT_MS, requestSignalStore, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
|
|
217
217
|
var init_client = __esm({
|
|
218
218
|
"../core/dist/client.js"() {
|
|
219
219
|
"use strict";
|
|
220
220
|
LENS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
221
221
|
TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
222
|
+
retriesOn401 = (method) => method.toUpperCase() === "GET";
|
|
222
223
|
ME_CACHE_TTL_MS = 60 * 1e3;
|
|
223
224
|
MAX_CONCURRENT = 5;
|
|
224
225
|
DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
|
|
@@ -396,15 +397,16 @@ var init_client = __esm({
|
|
|
396
397
|
// error envelope says so.
|
|
397
398
|
//
|
|
398
399
|
// Arrow-function field so `this` stays bound even when the method is passed
|
|
399
|
-
// as a bare reference (see request()'s ternary). Retries are GET-ONLY
|
|
400
|
-
// on a write (POST/PUT/DELETE) may arrive AFTER the
|
|
401
|
-
// server-side, so blindly re-sending it would
|
|
402
|
-
// are idempotent, so retrying them is safe.
|
|
403
|
-
// concurrency slot first (release → sleep →
|
|
404
|
-
// doesn't pin all MAX_CONCURRENT slots in
|
|
400
|
+
// as a bare reference (see request()'s ternary). Retries are GET-ONLY (see
|
|
401
|
+
// retriesOn401): a 401 on a write (POST/PUT/DELETE) may arrive AFTER the
|
|
402
|
+
// mutation already committed server-side, so blindly re-sending it would
|
|
403
|
+
// double-execute the write. Reads are idempotent, so retrying them is safe.
|
|
404
|
+
// The 250ms backoff releases the concurrency slot first (release → sleep →
|
|
405
|
+
// re-acquire) so a wave of 401s doesn't pin all MAX_CONCURRENT slots in
|
|
406
|
+
// setTimeout and stall the queue.
|
|
405
407
|
httpsRequestWithRetry = async (method, url, headers, body, timeoutMs) => {
|
|
406
408
|
const res = await httpsRequest(method, url, headers, body, timeoutMs);
|
|
407
|
-
if (res.status === 401 && method
|
|
409
|
+
if (res.status === 401 && retriesOn401(method)) {
|
|
408
410
|
this.releaseSemaphore();
|
|
409
411
|
try {
|
|
410
412
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -423,6 +425,7 @@ var init_client = __esm({
|
|
|
423
425
|
throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y -p @leadbay/mcp@latest installer", path);
|
|
424
426
|
}
|
|
425
427
|
const retryOn401 = opts?.retryOn401 !== false;
|
|
428
|
+
const retriedOn401 = retryOn401 && retriesOn401(method);
|
|
426
429
|
await this.acquireSemaphore();
|
|
427
430
|
try {
|
|
428
431
|
const url = `${this._baseUrl}${API_PREFIX}${path}`;
|
|
@@ -443,7 +446,7 @@ var init_client = __esm({
|
|
|
443
446
|
return null;
|
|
444
447
|
}
|
|
445
448
|
if (res.status < 200 || res.status >= 300) {
|
|
446
|
-
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
449
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
|
|
447
450
|
}
|
|
448
451
|
return JSON.parse(res.body);
|
|
449
452
|
} catch (e) {
|
|
@@ -453,6 +456,7 @@ var init_client = __esm({
|
|
|
453
456
|
}
|
|
454
457
|
}
|
|
455
458
|
async requestVoid(method, path, body) {
|
|
459
|
+
const retriedOn401 = retriesOn401(method);
|
|
456
460
|
if (process.env.LEADBAY_MOCK === "1") {
|
|
457
461
|
await this.mockRequest(method, path, body);
|
|
458
462
|
return;
|
|
@@ -477,7 +481,7 @@ var init_client = __esm({
|
|
|
477
481
|
retry_after: parseRetryAfter(res.headers["retry-after"])
|
|
478
482
|
};
|
|
479
483
|
if (res.status < 200 || res.status >= 300) {
|
|
480
|
-
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
484
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
|
|
481
485
|
}
|
|
482
486
|
} catch (e) {
|
|
483
487
|
throw this.mapTransportError(e, `${method} ${path}`);
|
|
@@ -491,6 +495,7 @@ var init_client = __esm({
|
|
|
491
495
|
// mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
|
|
492
496
|
// the wizard at POST /1.6/imports.
|
|
493
497
|
async requestRawBinary(method, path, contentType, body) {
|
|
498
|
+
const retriedOn401 = retriesOn401(method);
|
|
494
499
|
if (process.env.LEADBAY_MOCK === "1") {
|
|
495
500
|
return this.mockRequestBinary(method, path, contentType, body);
|
|
496
501
|
}
|
|
@@ -515,7 +520,7 @@ var init_client = __esm({
|
|
|
515
520
|
return null;
|
|
516
521
|
}
|
|
517
522
|
if (res.status < 200 || res.status >= 300) {
|
|
518
|
-
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
523
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
|
|
519
524
|
}
|
|
520
525
|
return JSON.parse(res.body);
|
|
521
526
|
} catch (e) {
|
|
@@ -603,7 +608,7 @@ var init_client = __esm({
|
|
|
603
608
|
}
|
|
604
609
|
return envelope;
|
|
605
610
|
}
|
|
606
|
-
mapErrorResponse(status, rawBody, endpoint, headers) {
|
|
611
|
+
mapErrorResponse(status, rawBody, endpoint, headers, retried) {
|
|
607
612
|
let parsed;
|
|
608
613
|
try {
|
|
609
614
|
parsed = JSON.parse(rawBody);
|
|
@@ -612,7 +617,7 @@ var init_client = __esm({
|
|
|
612
617
|
}
|
|
613
618
|
const retryAfter = parseRetryAfter(headers["retry-after"]);
|
|
614
619
|
if (status === 401) {
|
|
615
|
-
return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", "
|
|
620
|
+
return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", retried ? "Tokens don't expire on a timer, so this isn't stale. Already auto-retried once and it 401'd again \u2014 usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly, else report it." : "Tokens don't expire on a timer, so this isn't stale. This call wasn't auto-retried, so it's the first attempt \u2014 a Leadbay-side hiccup, or the user logged out. Try again once, else report it.", endpoint, null, status);
|
|
616
621
|
}
|
|
617
622
|
if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
|
|
618
623
|
const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
|
|
@@ -715,7 +720,7 @@ var init_client = __esm({
|
|
|
715
720
|
try {
|
|
716
721
|
const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0, opts?.timeoutMs);
|
|
717
722
|
if (res.status < 200 || res.status >= 300) {
|
|
718
|
-
throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers);
|
|
723
|
+
throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers, retriesOn401("GET"));
|
|
719
724
|
}
|
|
720
725
|
const me = JSON.parse(res.body);
|
|
721
726
|
const observed = me.telemetry_enabled;
|
|
@@ -10289,9 +10294,9 @@ Trigger phrases: "update this contact", "fix this contact's title", "change thei
|
|
|
10289
10294
|
|
|
10290
10295
|
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
10291
10296
|
|
|
10292
|
-
Do NOT use for: "add a new contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`; "get email/phone for a contact (enrichment)" \u2192 \`leadbay_enrich_titles\`.
|
|
10297
|
+
Do NOT use for: "add a new contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`; "get email/phone for a contact (enrichment)" \u2192 \`leadbay_enrich_titles\`; "fix an enriched contact's details" \u2192 \`leadbay_add_contact\`.
|
|
10293
10298
|
|
|
10294
|
-
Prefer when: user wants to change details on
|
|
10299
|
+
Prefer when: user wants to change details on a contact that is in their own directory (\`source: "org"\`) \u2014 pass that contact's own \`contact_id\` plus first_name + last_name (required) and the fields to change
|
|
10295
10300
|
|
|
10296
10301
|
Examples that SHOULD invoke this tool:
|
|
10297
10302
|
- "Update Jane's title to SVP Engineering."
|
|
@@ -10311,7 +10316,18 @@ One-line confirmation naming the contact and what changed. No table.
|
|
|
10311
10316
|
|
|
10312
10317
|
Edit an existing contact in place \u2014 change their \`job_title\`, \`linkedin_page\`, \`email\`, \`phone_number\`, or name.
|
|
10313
10318
|
|
|
10314
|
-
Pass the contact's **own** \`contact_id\`
|
|
10319
|
+
Pass the contact's **own** \`contact_id\` \u2014 **not** the parent lead id.
|
|
10320
|
+
|
|
10321
|
+
**Only your organization's own directory contacts can be edited.** Leadbay holds contacts in two separate id namespaces and this endpoint resolves one of them:
|
|
10322
|
+
|
|
10323
|
+
| \`source\` on the contact | What it is | Editable here |
|
|
10324
|
+
|---|---|---|
|
|
10325
|
+
| \`"org"\` | A row in your organization's contact directory \u2014 added by you or your team, or promoted from an import | **yes** |
|
|
10326
|
+
| \`"paid"\` | An enrichment result bought from a data provider | **no** \u2014 returns \`NOT_FOUND\` / 404 |
|
|
10327
|
+
|
|
10328
|
+
\`leadbay_research_lead_by_id\` returns both, merged into \`contacts.reachable\` / \`contacts.candidates\` and split by whether the person is messagable right now \u2014 **not** by which namespace they came from. So read \`source\` on the contact before calling this. If it is \`"paid"\` and the user wants different details on record, add the corrected person with \`leadbay_add_contact\` instead; the enrichment row is a provider's answer and is not ours to edit.
|
|
10329
|
+
|
|
10330
|
+
A 404 from this tool almost always means a \`"paid"\` id was passed. Re-read the contact, check \`source\`, and do not retry the same id.
|
|
10315
10331
|
|
|
10316
10332
|
**\`first_name\` + \`last_name\` are required even on an edit.** The backend validates the full contact identity and rejects a partial body (\`invalid contact\`). So pass the contact's *current* first/last name even when you're only changing the title \u2014 read the current values via \`leadbay_research_lead_by_id\` first if you don't have them.
|
|
10317
10333
|
|
|
@@ -14650,8 +14666,10 @@ var init_create_topup_link = __esm({
|
|
|
14650
14666
|
createTopupLink = {
|
|
14651
14667
|
name: "leadbay_create_topup_link",
|
|
14652
14668
|
annotations: {
|
|
14669
|
+
// Not read-only: this POSTs a new Stripe Checkout Session into existence.
|
|
14670
|
+
// Clients read readOnlyHint to decide whether to ask the user to confirm.
|
|
14653
14671
|
title: "Generate Stripe checkout URL for AI-credits top-up",
|
|
14654
|
-
readOnlyHint:
|
|
14672
|
+
readOnlyHint: false,
|
|
14655
14673
|
destructiveHint: false,
|
|
14656
14674
|
idempotentHint: false,
|
|
14657
14675
|
openWorldHint: true
|
|
@@ -14684,8 +14702,12 @@ var init_open_billing_portal = __esm({
|
|
|
14684
14702
|
openBillingPortal = {
|
|
14685
14703
|
name: "leadbay_open_billing_portal",
|
|
14686
14704
|
annotations: {
|
|
14705
|
+
// Not read-only, despite being a GET: the backend mints a Stripe portal
|
|
14706
|
+
// session, and for an org with no customer yet it also creates the Stripe
|
|
14707
|
+
// customer and persists organizations.stripe_customer_id. Same
|
|
14708
|
+
// getStripeCustomer path as leadbay_create_topup_link.
|
|
14687
14709
|
title: "Generate Stripe customer-portal URL for subscription management",
|
|
14688
|
-
readOnlyHint:
|
|
14710
|
+
readOnlyHint: false,
|
|
14689
14711
|
destructiveHint: false,
|
|
14690
14712
|
idempotentHint: false,
|
|
14691
14713
|
openWorldHint: true
|
|
@@ -19141,7 +19163,7 @@ var init_research_lead_by_id = __esm({
|
|
|
19141
19163
|
},
|
|
19142
19164
|
contacts: {
|
|
19143
19165
|
type: "object",
|
|
19144
|
-
description:
|
|
19166
|
+
description: 'Two-tier contact set, partitioned by reachability \u2014 agent-friendly framing of the backend\'s paid-vs-org split. `reachable`: contacts with an email or phone right now (org-directory entries that ship with channels, PLUS paid contacts whose enrichment has completed). The agent can message these without buying enrichment. `candidates`: paid-contact entries WITHOUT resolved channels yet \u2014 typically LinkedIn URL only, `enrichment_done: false`. The agent must call leadbay_enrich_titles (or leadbay_prepare_outreach with enrich:true) before these become messagable. Every contact in both lists carries `source`: `"org"` means it is a row in your organization\'s own contact directory, `"paid"` means it came from enrichment. The two are separate id namespaces on the backend, so only a `source:"org"` id can be passed to leadbay_update_contact / leadbay_remove_contact \u2014 a `"paid"` id returns NOT_FOUND there.',
|
|
19145
19167
|
properties: {
|
|
19146
19168
|
reachable: { type: "array", items: { type: "object" } },
|
|
19147
19169
|
candidates: { type: "array", items: { type: "object" } }
|
|
@@ -32961,7 +32983,7 @@ var OAUTH_BASE_URLS = {
|
|
|
32961
32983
|
fr: "https://staging.api.leadbay.app"
|
|
32962
32984
|
}
|
|
32963
32985
|
};
|
|
32964
|
-
var VERSION = "0.32.
|
|
32986
|
+
var VERSION = "0.32.6";
|
|
32965
32987
|
var HELP = `
|
|
32966
32988
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
32967
32989
|
|
package/dist/http-server.js
CHANGED
|
@@ -2797,6 +2797,7 @@ import { readdirSync, readFileSync, existsSync } from "fs";
|
|
|
2797
2797
|
import { join } from "path";
|
|
2798
2798
|
var LENS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2799
2799
|
var TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
2800
|
+
var retriesOn401 = (method) => method.toUpperCase() === "GET";
|
|
2800
2801
|
var ME_CACHE_TTL_MS = 60 * 1e3;
|
|
2801
2802
|
var MAX_CONCURRENT = 5;
|
|
2802
2803
|
var DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
|
|
@@ -3154,15 +3155,16 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3154
3155
|
// error envelope says so.
|
|
3155
3156
|
//
|
|
3156
3157
|
// Arrow-function field so `this` stays bound even when the method is passed
|
|
3157
|
-
// as a bare reference (see request()'s ternary). Retries are GET-ONLY
|
|
3158
|
-
// on a write (POST/PUT/DELETE) may arrive AFTER the
|
|
3159
|
-
// server-side, so blindly re-sending it would
|
|
3160
|
-
// are idempotent, so retrying them is safe.
|
|
3161
|
-
// concurrency slot first (release → sleep →
|
|
3162
|
-
// doesn't pin all MAX_CONCURRENT slots in
|
|
3158
|
+
// as a bare reference (see request()'s ternary). Retries are GET-ONLY (see
|
|
3159
|
+
// retriesOn401): a 401 on a write (POST/PUT/DELETE) may arrive AFTER the
|
|
3160
|
+
// mutation already committed server-side, so blindly re-sending it would
|
|
3161
|
+
// double-execute the write. Reads are idempotent, so retrying them is safe.
|
|
3162
|
+
// The 250ms backoff releases the concurrency slot first (release → sleep →
|
|
3163
|
+
// re-acquire) so a wave of 401s doesn't pin all MAX_CONCURRENT slots in
|
|
3164
|
+
// setTimeout and stall the queue.
|
|
3163
3165
|
httpsRequestWithRetry = async (method, url, headers, body, timeoutMs) => {
|
|
3164
3166
|
const res = await httpsRequest(method, url, headers, body, timeoutMs);
|
|
3165
|
-
if (res.status === 401 && method
|
|
3167
|
+
if (res.status === 401 && retriesOn401(method)) {
|
|
3166
3168
|
this.releaseSemaphore();
|
|
3167
3169
|
try {
|
|
3168
3170
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -3181,6 +3183,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3181
3183
|
throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y -p @leadbay/mcp@latest installer", path);
|
|
3182
3184
|
}
|
|
3183
3185
|
const retryOn401 = opts?.retryOn401 !== false;
|
|
3186
|
+
const retriedOn401 = retryOn401 && retriesOn401(method);
|
|
3184
3187
|
await this.acquireSemaphore();
|
|
3185
3188
|
try {
|
|
3186
3189
|
const url = `${this._baseUrl}${API_PREFIX}${path}`;
|
|
@@ -3201,7 +3204,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3201
3204
|
return null;
|
|
3202
3205
|
}
|
|
3203
3206
|
if (res.status < 200 || res.status >= 300) {
|
|
3204
|
-
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
3207
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
|
|
3205
3208
|
}
|
|
3206
3209
|
return JSON.parse(res.body);
|
|
3207
3210
|
} catch (e) {
|
|
@@ -3211,6 +3214,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3211
3214
|
}
|
|
3212
3215
|
}
|
|
3213
3216
|
async requestVoid(method, path, body) {
|
|
3217
|
+
const retriedOn401 = retriesOn401(method);
|
|
3214
3218
|
if (process.env.LEADBAY_MOCK === "1") {
|
|
3215
3219
|
await this.mockRequest(method, path, body);
|
|
3216
3220
|
return;
|
|
@@ -3235,7 +3239,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3235
3239
|
retry_after: parseRetryAfter(res.headers["retry-after"])
|
|
3236
3240
|
};
|
|
3237
3241
|
if (res.status < 200 || res.status >= 300) {
|
|
3238
|
-
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
3242
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
|
|
3239
3243
|
}
|
|
3240
3244
|
} catch (e) {
|
|
3241
3245
|
throw this.mapTransportError(e, `${method} ${path}`);
|
|
@@ -3249,6 +3253,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3249
3253
|
// mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
|
|
3250
3254
|
// the wizard at POST /1.6/imports.
|
|
3251
3255
|
async requestRawBinary(method, path, contentType, body) {
|
|
3256
|
+
const retriedOn401 = retriesOn401(method);
|
|
3252
3257
|
if (process.env.LEADBAY_MOCK === "1") {
|
|
3253
3258
|
return this.mockRequestBinary(method, path, contentType, body);
|
|
3254
3259
|
}
|
|
@@ -3273,7 +3278,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3273
3278
|
return null;
|
|
3274
3279
|
}
|
|
3275
3280
|
if (res.status < 200 || res.status >= 300) {
|
|
3276
|
-
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
3281
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers, retriedOn401);
|
|
3277
3282
|
}
|
|
3278
3283
|
return JSON.parse(res.body);
|
|
3279
3284
|
} catch (e) {
|
|
@@ -3361,7 +3366,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3361
3366
|
}
|
|
3362
3367
|
return envelope;
|
|
3363
3368
|
}
|
|
3364
|
-
mapErrorResponse(status, rawBody, endpoint, headers) {
|
|
3369
|
+
mapErrorResponse(status, rawBody, endpoint, headers, retried) {
|
|
3365
3370
|
let parsed;
|
|
3366
3371
|
try {
|
|
3367
3372
|
parsed = JSON.parse(rawBody);
|
|
@@ -3370,7 +3375,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3370
3375
|
}
|
|
3371
3376
|
const retryAfter = parseRetryAfter(headers["retry-after"]);
|
|
3372
3377
|
if (status === 401) {
|
|
3373
|
-
return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", "
|
|
3378
|
+
return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", retried ? "Tokens don't expire on a timer, so this isn't stale. Already auto-retried once and it 401'd again \u2014 usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly, else report it." : "Tokens don't expire on a timer, so this isn't stale. This call wasn't auto-retried, so it's the first attempt \u2014 a Leadbay-side hiccup, or the user logged out. Try again once, else report it.", endpoint, null, status);
|
|
3374
3379
|
}
|
|
3375
3380
|
if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
|
|
3376
3381
|
const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
|
|
@@ -3473,7 +3478,7 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3473
3478
|
try {
|
|
3474
3479
|
const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0, opts?.timeoutMs);
|
|
3475
3480
|
if (res.status < 200 || res.status >= 300) {
|
|
3476
|
-
throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers);
|
|
3481
|
+
throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers, retriesOn401("GET"));
|
|
3477
3482
|
}
|
|
3478
3483
|
const me = JSON.parse(res.body);
|
|
3479
3484
|
const observed = me.telemetry_enabled;
|
|
@@ -12682,9 +12687,9 @@ Trigger phrases: "update this contact", "fix this contact's title", "change thei
|
|
|
12682
12687
|
|
|
12683
12688
|
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
12684
12689
|
|
|
12685
|
-
Do NOT use for: "add a new contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`; "get email/phone for a contact (enrichment)" \u2192 \`leadbay_enrich_titles\`.
|
|
12690
|
+
Do NOT use for: "add a new contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`; "get email/phone for a contact (enrichment)" \u2192 \`leadbay_enrich_titles\`; "fix an enriched contact's details" \u2192 \`leadbay_add_contact\`.
|
|
12686
12691
|
|
|
12687
|
-
Prefer when: user wants to change details on
|
|
12692
|
+
Prefer when: user wants to change details on a contact that is in their own directory (\`source: "org"\`) \u2014 pass that contact's own \`contact_id\` plus first_name + last_name (required) and the fields to change
|
|
12688
12693
|
|
|
12689
12694
|
Examples that SHOULD invoke this tool:
|
|
12690
12695
|
- "Update Jane's title to SVP Engineering."
|
|
@@ -12704,7 +12709,18 @@ One-line confirmation naming the contact and what changed. No table.
|
|
|
12704
12709
|
|
|
12705
12710
|
Edit an existing contact in place \u2014 change their \`job_title\`, \`linkedin_page\`, \`email\`, \`phone_number\`, or name.
|
|
12706
12711
|
|
|
12707
|
-
Pass the contact's **own** \`contact_id\`
|
|
12712
|
+
Pass the contact's **own** \`contact_id\` \u2014 **not** the parent lead id.
|
|
12713
|
+
|
|
12714
|
+
**Only your organization's own directory contacts can be edited.** Leadbay holds contacts in two separate id namespaces and this endpoint resolves one of them:
|
|
12715
|
+
|
|
12716
|
+
| \`source\` on the contact | What it is | Editable here |
|
|
12717
|
+
|---|---|---|
|
|
12718
|
+
| \`"org"\` | A row in your organization's contact directory \u2014 added by you or your team, or promoted from an import | **yes** |
|
|
12719
|
+
| \`"paid"\` | An enrichment result bought from a data provider | **no** \u2014 returns \`NOT_FOUND\` / 404 |
|
|
12720
|
+
|
|
12721
|
+
\`leadbay_research_lead_by_id\` returns both, merged into \`contacts.reachable\` / \`contacts.candidates\` and split by whether the person is messagable right now \u2014 **not** by which namespace they came from. So read \`source\` on the contact before calling this. If it is \`"paid"\` and the user wants different details on record, add the corrected person with \`leadbay_add_contact\` instead; the enrichment row is a provider's answer and is not ours to edit.
|
|
12722
|
+
|
|
12723
|
+
A 404 from this tool almost always means a \`"paid"\` id was passed. Re-read the contact, check \`source\`, and do not retry the same id.
|
|
12708
12724
|
|
|
12709
12725
|
**\`first_name\` + \`last_name\` are required even on an edit.** The backend validates the full contact identity and rejects a partial body (\`invalid contact\`). So pass the contact's *current* first/last name even when you're only changing the title \u2014 read the current values via \`leadbay_research_lead_by_id\` first if you don't have them.
|
|
12710
12726
|
|
|
@@ -16819,8 +16835,10 @@ function coerceCsvValue(v) {
|
|
|
16819
16835
|
var createTopupLink = {
|
|
16820
16836
|
name: "leadbay_create_topup_link",
|
|
16821
16837
|
annotations: {
|
|
16838
|
+
// Not read-only: this POSTs a new Stripe Checkout Session into existence.
|
|
16839
|
+
// Clients read readOnlyHint to decide whether to ask the user to confirm.
|
|
16822
16840
|
title: "Generate Stripe checkout URL for AI-credits top-up",
|
|
16823
|
-
readOnlyHint:
|
|
16841
|
+
readOnlyHint: false,
|
|
16824
16842
|
destructiveHint: false,
|
|
16825
16843
|
idempotentHint: false,
|
|
16826
16844
|
openWorldHint: true
|
|
@@ -16846,8 +16864,12 @@ var createTopupLink = {
|
|
|
16846
16864
|
var openBillingPortal = {
|
|
16847
16865
|
name: "leadbay_open_billing_portal",
|
|
16848
16866
|
annotations: {
|
|
16867
|
+
// Not read-only, despite being a GET: the backend mints a Stripe portal
|
|
16868
|
+
// session, and for an org with no customer yet it also creates the Stripe
|
|
16869
|
+
// customer and persists organizations.stripe_customer_id. Same
|
|
16870
|
+
// getStripeCustomer path as leadbay_create_topup_link.
|
|
16849
16871
|
title: "Generate Stripe customer-portal URL for subscription management",
|
|
16850
|
-
readOnlyHint:
|
|
16872
|
+
readOnlyHint: false,
|
|
16851
16873
|
destructiveHint: false,
|
|
16852
16874
|
idempotentHint: false,
|
|
16853
16875
|
openWorldHint: true
|
|
@@ -20921,7 +20943,7 @@ var researchLeadById = {
|
|
|
20921
20943
|
},
|
|
20922
20944
|
contacts: {
|
|
20923
20945
|
type: "object",
|
|
20924
|
-
description:
|
|
20946
|
+
description: 'Two-tier contact set, partitioned by reachability \u2014 agent-friendly framing of the backend\'s paid-vs-org split. `reachable`: contacts with an email or phone right now (org-directory entries that ship with channels, PLUS paid contacts whose enrichment has completed). The agent can message these without buying enrichment. `candidates`: paid-contact entries WITHOUT resolved channels yet \u2014 typically LinkedIn URL only, `enrichment_done: false`. The agent must call leadbay_enrich_titles (or leadbay_prepare_outreach with enrich:true) before these become messagable. Every contact in both lists carries `source`: `"org"` means it is a row in your organization\'s own contact directory, `"paid"` means it came from enrichment. The two are separate id namespaces on the backend, so only a `source:"org"` id can be passed to leadbay_update_contact / leadbay_remove_contact \u2014 a `"paid"` id returns NOT_FOUND there.',
|
|
20925
20947
|
properties: {
|
|
20926
20948
|
reachable: { type: "array", items: { type: "object" } },
|
|
20927
20949
|
candidates: { type: "array", items: { type: "object" } }
|
|
@@ -29547,7 +29569,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
29547
29569
|
}
|
|
29548
29570
|
|
|
29549
29571
|
// src/http-server.ts
|
|
29550
|
-
var VERSION = true ? "0.32.
|
|
29572
|
+
var VERSION = true ? "0.32.6" : "0.0.0-dev";
|
|
29551
29573
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
29552
29574
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
29553
29575
|
var logger = {
|
package/dist/installer-gui.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.6",
|
|
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",
|