@leadbay/mcp 0.33.2 → 0.33.3
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 +54 -0
- package/dist/bin.js +82 -12
- package/dist/http-server.js +80 -11
- 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,59 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.33.3 — 2026-09-02
|
|
4
|
+
|
|
5
|
+
`leadbay_pin_contact` failed 43 of its 48 production calls.
|
|
6
|
+
|
|
7
|
+
Measured over the 180 days to 2026-09-02 (PostHog `mcp tool called`,
|
|
8
|
+
`properties.ok = false`). Sentry issue `MCP-3A` carries exactly 43 events, so
|
|
9
|
+
every `contact not found` in the `mcp` project is a pin call. 41 of the 43 are
|
|
10
|
+
one scheduled agent, `zoe+dogfood@leadbay.ai`, across 12 days and MCP 0.26.0
|
|
11
|
+
through 0.33.2. Its own `triggered_by` reads *"tâche planifiée : épingler le DG
|
|
12
|
+
Mickael Hamot (CROMOLOGY SERVICES) à la place du président"*. It picks the DG
|
|
13
|
+
out of `leadbay_research_lead_by_id` and pins that id, which on an unenriched
|
|
14
|
+
company is a `source: "paid"` candidate. It has never once succeeded, and pin
|
|
15
|
+
is its third most used tool.
|
|
16
|
+
|
|
17
|
+
`POST /contacts/{id}/pin` resolves through `org_contacts` only
|
|
18
|
+
(`OrgContactRoutes.kt:44`), so a paid candidate id can never resolve there. Two
|
|
19
|
+
changes:
|
|
20
|
+
|
|
21
|
+
- **The 404 now names its cause.** The client's shared 404 hint is "Verify the
|
|
22
|
+
ID is correct" (`client.ts:1048`), which on this endpoint is false: the id is
|
|
23
|
+
correct, it is the wrong namespace. An agent reads that as "look it up and
|
|
24
|
+
retry" and does, in bursts of up to 10 calls in 13 seconds. pin and unpin now
|
|
25
|
+
catch `NOT_FOUND` and replace the hint with one that names the org-vs-paid
|
|
26
|
+
split, says not to retry, and names the tools that make the person pinnable.
|
|
27
|
+
- **The descriptions carry the rule.** New shared snippet
|
|
28
|
+
`snippets/heuristics/pinnable-contacts.md`, included by both templates, plus
|
|
29
|
+
the rule in `prefer_when` so it lands in the first 600 chars every host reads.
|
|
30
|
+
It also states that pinning does not steer enrichment: enrichment selects by
|
|
31
|
+
job title (`resolveAutoIncludedTitles` →
|
|
32
|
+
`paidContacts.findSimilarJobTitlesWithScores`) and `pinnedBy` plays no part.
|
|
33
|
+
That was the second half of the agent's mistake.
|
|
34
|
+
|
|
35
|
+
**`pinned` is now readable.** The backend's `ContactPayload` has carried
|
|
36
|
+
`pinned` + `pinned_by_ai` all along; every MCP shaping site dropped them, so a
|
|
37
|
+
pin could be written but not read back except by watching `recommended`, which
|
|
38
|
+
moves for other reasons too. Now passed through in `research_lead_by_id`,
|
|
39
|
+
`get_contacts` and `get_lead_profile`, and marked `📌` in both markdown contact
|
|
40
|
+
lists. Deliberately asymmetric: `PaidContactPayload` has no pin state, so paid
|
|
41
|
+
contacts get no `pinned` key rather than a synthetic `false`.
|
|
42
|
+
|
|
43
|
+
Verified live against FR staging, running the built branch:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
BEFORE PIN: org contact → pinned: false paid contacts → no pinned key
|
|
47
|
+
AFTER PIN: org contact → pinned: true (pin http 204)
|
|
48
|
+
AFTER UNPIN: org contact → pinned: false (unpin http 204)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Remaining, not fixed here: on hosted an agent still has no way to enrich one
|
|
52
|
+
named person. `leadbay_enrich_contacts` takes a `contact_id` and does exactly
|
|
53
|
+
that, but sits in `granularWriteTools` behind `LEADBAY_MCP_ADVANCED=1`, and no
|
|
54
|
+
granular tool has been called from a hosted IP in 30 days. That is why the
|
|
55
|
+
agent reached for pin. Filed as leadbay/product#4050.
|
|
56
|
+
|
|
3
57
|
## 0.33.2 — 2026-09-02
|
|
4
58
|
|
|
5
59
|
Editing one field on a contact erased the others (product#4046).
|
package/dist/bin.js
CHANGED
|
@@ -3688,7 +3688,7 @@ Trigger phrases: "pin this contact", "mark this person as priority", "make this
|
|
|
3688
3688
|
|
|
3689
3689
|
Do NOT use for: "unpin / remove the pin" \u2192 \`leadbay_unpin_contact\`; "add a contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
3690
3690
|
|
|
3691
|
-
Prefer when: user wants ONE person flagged as the priority on a company \u2014 pass that contact's own \`contact_id\`
|
|
3691
|
+
Prefer when: user wants ONE person flagged as the priority on a company \u2014 pass that contact's own \`contact_id\`, and ONLY a \`source:"org"\` contact can be pinned (a \`source:"paid"\` candidate returns 'contact not found')
|
|
3692
3692
|
|
|
3693
3693
|
Examples that SHOULD invoke this tool:
|
|
3694
3694
|
- "Pin Jane Doe as the main contact on this company."
|
|
@@ -3710,9 +3710,24 @@ Pin a single contact on a company so it surfaces first as a priority / favourite
|
|
|
3710
3710
|
|
|
3711
3711
|
Pass the contact's **own** \`contact_id\` (the \`id\` field on a contact object from \`leadbay_research_lead_by_id\` or a contacts list) \u2014 **not** the parent lead id.
|
|
3712
3712
|
|
|
3713
|
+
**Only \`source: "org"\` contacts are pinnable.** Every contact returned by \`leadbay_research_lead_by_id\` carries a \`source\` field, and the two sources are separate id namespaces on the backend:
|
|
3714
|
+
|
|
3715
|
+
- \`source: "org"\` \u2014 a row in your organization's own contact directory. Pinnable. Also carries \`pinned\` (true when someone has pinned it) and \`pinned_by_ai\` (true when Leadbay's AI pinned it rather than a human).
|
|
3716
|
+
- \`source: "paid"\` \u2014 an enrichment *candidate* (the \`candidates\` bucket): a person Leadbay suggests but has not yet resolved into your directory. NOT pinnable, and carries no \`pinned\` field at all.
|
|
3717
|
+
|
|
3718
|
+
Passing a \`source: "paid"\` id here returns **\`contact not found\`**. That is the expected answer for a candidate, not an outage and not a transient error: nothing is broken, the person is simply not an org contact yet. Do not retry, do not re-fetch the lead hoping for a different result, and do not tell the user that pinning is failing or unavailable.
|
|
3719
|
+
|
|
3720
|
+
To pin someone who is currently only a candidate, first make them an org contact:
|
|
3721
|
+
|
|
3722
|
+
- \`leadbay_enrich_titles\` (or \`leadbay_prepare_outreach\` with \`enrich: true\`) resolves the candidate and writes a NEW org contact for that person. It has a **different \`id\`** from the paid candidate, so re-read the contacts list afterwards and pin the \`source: "org"\` row.
|
|
3723
|
+
- Or add them directly with \`leadbay_add_contact\`, which returns the new org contact's \`id\` \u2014 that id is pinnable immediately.
|
|
3724
|
+
|
|
3725
|
+
**Pinning does not steer enrichment.** It only marks who the priority is on a company the user already has. Enrichment picks people by JOB TITLE, so "enrich the Directeur G\xE9n\xE9ral rather than the Pr\xE9sident" is \`leadbay_enrich_titles\` with the wanted title \u2014 not a pin. Pinning first and enriching after changes nothing about who gets enriched.
|
|
3726
|
+
|
|
3727
|
+
|
|
3713
3728
|
Backend: \`POST /contacts/{contact_id}/pin\` \u2192 204. Idempotent. The inverse is \`leadbay_unpin_contact\`.
|
|
3714
3729
|
|
|
3715
|
-
Returns \`{ pinned: true, contact_id, action: "pinned" }\`.
|
|
3730
|
+
Returns \`{ pinned: true, contact_id, action: "pinned" }\`. To read the resulting state back, re-call \`leadbay_research_lead_by_id\` \u2014 the pinned contact's \`pinned\` flips to \`true\` and it becomes the lead's \`recommended\` contact.
|
|
3716
3731
|
|
|
3717
3732
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
3718
3733
|
`;
|
|
@@ -5657,7 +5672,7 @@ Trigger phrases: "unpin this contact", "remove the pin from this contact", "this
|
|
|
5657
5672
|
|
|
5658
5673
|
Do NOT use for: "pin / mark as priority" \u2192 \`leadbay_pin_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
5659
5674
|
|
|
5660
|
-
Prefer when: user wants to clear the pinned flag on a contact (but keep the contact) \u2014 pass that contact's own \`contact_id\`
|
|
5675
|
+
Prefer when: user wants to clear the pinned flag on a contact (but keep the contact) \u2014 pass that contact's own \`contact_id\`, and ONLY a \`source:"org"\` contact can be unpinned (a \`source:"paid"\` candidate returns 'contact not found')
|
|
5661
5676
|
|
|
5662
5677
|
Examples that SHOULD invoke this tool:
|
|
5663
5678
|
- "Unpin Jane Doe \u2014 she's not the priority anymore."
|
|
@@ -5680,9 +5695,26 @@ Unpin a single contact on a company \u2014 clears its priority / favourite flag.
|
|
|
5680
5695
|
|
|
5681
5696
|
Pass the contact's **own** \`contact_id\` \u2014 not the parent lead id.
|
|
5682
5697
|
|
|
5698
|
+
**Only \`source: "org"\` contacts are pinnable.** Every contact returned by \`leadbay_research_lead_by_id\` carries a \`source\` field, and the two sources are separate id namespaces on the backend:
|
|
5699
|
+
|
|
5700
|
+
- \`source: "org"\` \u2014 a row in your organization's own contact directory. Pinnable. Also carries \`pinned\` (true when someone has pinned it) and \`pinned_by_ai\` (true when Leadbay's AI pinned it rather than a human).
|
|
5701
|
+
- \`source: "paid"\` \u2014 an enrichment *candidate* (the \`candidates\` bucket): a person Leadbay suggests but has not yet resolved into your directory. NOT pinnable, and carries no \`pinned\` field at all.
|
|
5702
|
+
|
|
5703
|
+
Passing a \`source: "paid"\` id here returns **\`contact not found\`**. That is the expected answer for a candidate, not an outage and not a transient error: nothing is broken, the person is simply not an org contact yet. Do not retry, do not re-fetch the lead hoping for a different result, and do not tell the user that pinning is failing or unavailable.
|
|
5704
|
+
|
|
5705
|
+
To pin someone who is currently only a candidate, first make them an org contact:
|
|
5706
|
+
|
|
5707
|
+
- \`leadbay_enrich_titles\` (or \`leadbay_prepare_outreach\` with \`enrich: true\`) resolves the candidate and writes a NEW org contact for that person. It has a **different \`id\`** from the paid candidate, so re-read the contacts list afterwards and pin the \`source: "org"\` row.
|
|
5708
|
+
- Or add them directly with \`leadbay_add_contact\`, which returns the new org contact's \`id\` \u2014 that id is pinnable immediately.
|
|
5709
|
+
|
|
5710
|
+
**Pinning does not steer enrichment.** It only marks who the priority is on a company the user already has. Enrichment picks people by JOB TITLE, so "enrich the Directeur G\xE9n\xE9ral rather than the Pr\xE9sident" is \`leadbay_enrich_titles\` with the wanted title \u2014 not a pin. Pinning first and enriching after changes nothing about who gets enriched.
|
|
5711
|
+
|
|
5712
|
+
|
|
5713
|
+
A \`source: "org"\` contact that was never pinned is a no-op here, not an error \u2014 the backend answers 204 either way. Check \`pinned\` on the contact before calling if you need to tell the user whether anything actually changed.
|
|
5714
|
+
|
|
5683
5715
|
Backend: \`POST /contacts/{contact_id}/unpin\` \u2192 204. Idempotent. The inverse is \`leadbay_pin_contact\`.
|
|
5684
5716
|
|
|
5685
|
-
Returns \`{ pinned: false, contact_id, action: "unpinned" }\`.
|
|
5717
|
+
Returns \`{ pinned: false, contact_id, action: "unpinned" }\`. To read the resulting state back, re-call \`leadbay_research_lead_by_id\` \u2014 the contact's \`pinned\` flips to \`false\` and the lead's \`recommended\` contact reverts to the title-matched default.
|
|
5686
5718
|
|
|
5687
5719
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
5688
5720
|
`;
|
|
@@ -6086,6 +6118,10 @@ var init_get_lead_profile = __esm({
|
|
|
6086
6118
|
job_title: c.job_title,
|
|
6087
6119
|
recommended: c.recommended,
|
|
6088
6120
|
enrichment: c.enrichment,
|
|
6121
|
+
// Org contacts only — the backend's PaidContactPayload carries no pin
|
|
6122
|
+
// state, because a paid candidate cannot be pinned.
|
|
6123
|
+
pinned: c.pinned ?? false,
|
|
6124
|
+
pinned_by_ai: c.pinned_by_ai ?? false,
|
|
6089
6125
|
source: "org"
|
|
6090
6126
|
})),
|
|
6091
6127
|
...paidContacts.map((c) => ({
|
|
@@ -6177,7 +6213,7 @@ var init_get_contacts = __esm({
|
|
|
6177
6213
|
properties: {
|
|
6178
6214
|
contacts: {
|
|
6179
6215
|
type: "array",
|
|
6180
|
-
description: "Merged org+paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}.",
|
|
6216
|
+
description: "Merged org+paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}. `source:'org'` entries additionally carry {pinned, pinned_by_ai}; `source:'paid'` entries do not, because a paid candidate cannot be pinned \u2014 passing its id to leadbay_pin_contact / leadbay_unpin_contact returns NOT_FOUND.",
|
|
6181
6217
|
items: {
|
|
6182
6218
|
type: "object",
|
|
6183
6219
|
properties: {
|
|
@@ -6189,6 +6225,14 @@ var init_get_contacts = __esm({
|
|
|
6189
6225
|
linkedin_page: { type: ["string", "null"] },
|
|
6190
6226
|
job_title: { type: ["string", "null"] },
|
|
6191
6227
|
recommended: { type: "boolean" },
|
|
6228
|
+
pinned: {
|
|
6229
|
+
type: "boolean",
|
|
6230
|
+
description: "Someone flagged this person as the priority on the company. Present on source:'org' contacts only."
|
|
6231
|
+
},
|
|
6232
|
+
pinned_by_ai: {
|
|
6233
|
+
type: "boolean",
|
|
6234
|
+
description: "The pin came from Leadbay's AI rather than a human. Present on source:'org' contacts only."
|
|
6235
|
+
},
|
|
6192
6236
|
source: { type: "string", enum: ["org", "paid"] },
|
|
6193
6237
|
enrichment: {
|
|
6194
6238
|
type: ["object", "null"],
|
|
@@ -6247,6 +6291,10 @@ var init_get_contacts = __esm({
|
|
|
6247
6291
|
job_title: c.job_title,
|
|
6248
6292
|
recommended: c.recommended,
|
|
6249
6293
|
enrichment: c.enrichment,
|
|
6294
|
+
// Org contacts only — the backend's PaidContactPayload carries no
|
|
6295
|
+
// pin state, because a paid candidate cannot be pinned.
|
|
6296
|
+
pinned: c.pinned ?? false,
|
|
6297
|
+
pinned_by_ai: c.pinned_by_ai ?? false,
|
|
6250
6298
|
source: "org"
|
|
6251
6299
|
})),
|
|
6252
6300
|
...paidContacts.map((c) => ({
|
|
@@ -11785,11 +11833,12 @@ var init_remove_contact = __esm({
|
|
|
11785
11833
|
});
|
|
11786
11834
|
|
|
11787
11835
|
// ../core/dist/tools/pin-contact.js
|
|
11788
|
-
var pinContact;
|
|
11836
|
+
var NOT_PINNABLE_HINT, pinContact;
|
|
11789
11837
|
var init_pin_contact = __esm({
|
|
11790
11838
|
"../core/dist/tools/pin-contact.js"() {
|
|
11791
11839
|
"use strict";
|
|
11792
11840
|
init_tool_descriptions_generated();
|
|
11841
|
+
NOT_PINNABLE_HINT = 'This contact id is not in your organization\'s contact directory, so it cannot be pinned or unpinned. Almost always it is a `source: "paid"` enrichment candidate from leadbay_research_lead_by_id\'s `candidates` list; only `source: "org"` contacts are pinnable. The id is not wrong and the tool is not broken, so do NOT retry it. To act on this person, enrich them by job title with leadbay_enrich_titles, or add them with leadbay_add_contact \u2014 either produces a NEW org contact with a different id, which is pinnable. Note that pinning does not decide who gets enriched; enrichment selects people by job title.';
|
|
11793
11842
|
pinContact = {
|
|
11794
11843
|
name: "leadbay_pin_contact",
|
|
11795
11844
|
description: leadbay_pin_contact,
|
|
@@ -11813,7 +11862,13 @@ var init_pin_contact = __esm({
|
|
|
11813
11862
|
additionalProperties: false
|
|
11814
11863
|
},
|
|
11815
11864
|
execute: async (client, params, _ctx) => {
|
|
11816
|
-
|
|
11865
|
+
try {
|
|
11866
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/pin`);
|
|
11867
|
+
} catch (e) {
|
|
11868
|
+
if (e?.code === "NOT_FOUND")
|
|
11869
|
+
throw { ...e, hint: NOT_PINNABLE_HINT };
|
|
11870
|
+
throw e;
|
|
11871
|
+
}
|
|
11817
11872
|
return { pinned: true, contact_id: params.contact_id, action: "pinned" };
|
|
11818
11873
|
}
|
|
11819
11874
|
};
|
|
@@ -11826,6 +11881,7 @@ var init_unpin_contact = __esm({
|
|
|
11826
11881
|
"../core/dist/tools/unpin-contact.js"() {
|
|
11827
11882
|
"use strict";
|
|
11828
11883
|
init_tool_descriptions_generated();
|
|
11884
|
+
init_pin_contact();
|
|
11829
11885
|
unpinContact = {
|
|
11830
11886
|
name: "leadbay_unpin_contact",
|
|
11831
11887
|
description: leadbay_unpin_contact,
|
|
@@ -11849,7 +11905,13 @@ var init_unpin_contact = __esm({
|
|
|
11849
11905
|
additionalProperties: false
|
|
11850
11906
|
},
|
|
11851
11907
|
execute: async (client, params, _ctx) => {
|
|
11852
|
-
|
|
11908
|
+
try {
|
|
11909
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/unpin`);
|
|
11910
|
+
} catch (e) {
|
|
11911
|
+
if (e?.code === "NOT_FOUND")
|
|
11912
|
+
throw { ...e, hint: NOT_PINNABLE_HINT };
|
|
11913
|
+
throw e;
|
|
11914
|
+
}
|
|
11853
11915
|
return { pinned: false, contact_id: params.contact_id, action: "unpinned" };
|
|
11854
11916
|
}
|
|
11855
11917
|
};
|
|
@@ -13965,7 +14027,8 @@ ${firm.short_description}`);
|
|
|
13965
14027
|
const ln = c.last_name ?? "";
|
|
13966
14028
|
const title = c.job_title ?? "\u2014";
|
|
13967
14029
|
const channel = c.email ?? c.phone_number ?? "\u2014";
|
|
13968
|
-
|
|
14030
|
+
const pin = c.pinned ? " \u{1F4CC}" : "";
|
|
14031
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}**${pin} \u2014 ${title} \xB7 ${channel}`);
|
|
13969
14032
|
}
|
|
13970
14033
|
}
|
|
13971
14034
|
const candidates = Array.isArray(contacts.candidates) ? contacts.candidates : [];
|
|
@@ -13977,7 +14040,8 @@ ${firm.short_description}`);
|
|
|
13977
14040
|
const ln = c.last_name ?? "";
|
|
13978
14041
|
const title = c.job_title ?? "\u2014";
|
|
13979
14042
|
const li = c.linkedin_page ? `LinkedIn` : "no LinkedIn";
|
|
13980
|
-
|
|
14043
|
+
const pin = c.pinned ? " \u{1F4CC}" : "";
|
|
14044
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}**${pin} \u2014 ${title} \xB7 ${li}`);
|
|
13981
14045
|
}
|
|
13982
14046
|
if (candidates.length > 10)
|
|
13983
14047
|
out.push(`- _${candidates.length - 10} more \u2026_`);
|
|
@@ -14189,7 +14253,7 @@ var init_research_lead_by_id = __esm({
|
|
|
14189
14253
|
},
|
|
14190
14254
|
contacts: {
|
|
14191
14255
|
type: "object",
|
|
14192
|
-
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.',
|
|
14256
|
+
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 / leadbay_pin_contact / leadbay_unpin_contact \u2014 a `"paid"` id returns NOT_FOUND there, which means "this candidate is not an org contact yet", NOT that the tool is broken. Only `source:"org"` contacts carry `pinned` (someone flagged this person as the priority) and `pinned_by_ai` (that someone was Leadbay\'s AI, not a human); paid candidates have no pin state because they cannot be pinned.',
|
|
14193
14257
|
properties: {
|
|
14194
14258
|
reachable: { type: "array", items: { type: "object" } },
|
|
14195
14259
|
candidates: { type: "array", items: { type: "object" } }
|
|
@@ -14314,6 +14378,12 @@ var init_research_lead_by_id = __esm({
|
|
|
14314
14378
|
linkedin_page: normalizeLinkedinPage5(c.linkedin_page ?? null),
|
|
14315
14379
|
recommended: c.recommended,
|
|
14316
14380
|
enrichment_done: true,
|
|
14381
|
+
// Pin state exists on org contacts only — mirror the backend, which
|
|
14382
|
+
// omits it entirely from PaidContactPayload. `pinned` is what makes a
|
|
14383
|
+
// pin readable at all; without it the agent can only infer the pin from
|
|
14384
|
+
// `recommended`, which also moves for non-pin reasons.
|
|
14385
|
+
pinned: c.pinned ?? false,
|
|
14386
|
+
pinned_by_ai: c.pinned_by_ai ?? false,
|
|
14317
14387
|
source: "org"
|
|
14318
14388
|
});
|
|
14319
14389
|
const allContacts = [
|
|
@@ -27901,7 +27971,7 @@ var OAUTH_BASE_URLS = {
|
|
|
27901
27971
|
fr: "https://staging.api.leadbay.app"
|
|
27902
27972
|
}
|
|
27903
27973
|
};
|
|
27904
|
-
var VERSION = "0.33.
|
|
27974
|
+
var VERSION = "0.33.3";
|
|
27905
27975
|
var HELP = `
|
|
27906
27976
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
27907
27977
|
|
package/dist/http-server.js
CHANGED
|
@@ -6279,7 +6279,7 @@ Trigger phrases: "pin this contact", "mark this person as priority", "make this
|
|
|
6279
6279
|
|
|
6280
6280
|
Do NOT use for: "unpin / remove the pin" \u2192 \`leadbay_unpin_contact\`; "add a contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
6281
6281
|
|
|
6282
|
-
Prefer when: user wants ONE person flagged as the priority on a company \u2014 pass that contact's own \`contact_id\`
|
|
6282
|
+
Prefer when: user wants ONE person flagged as the priority on a company \u2014 pass that contact's own \`contact_id\`, and ONLY a \`source:"org"\` contact can be pinned (a \`source:"paid"\` candidate returns 'contact not found')
|
|
6283
6283
|
|
|
6284
6284
|
Examples that SHOULD invoke this tool:
|
|
6285
6285
|
- "Pin Jane Doe as the main contact on this company."
|
|
@@ -6301,9 +6301,24 @@ Pin a single contact on a company so it surfaces first as a priority / favourite
|
|
|
6301
6301
|
|
|
6302
6302
|
Pass the contact's **own** \`contact_id\` (the \`id\` field on a contact object from \`leadbay_research_lead_by_id\` or a contacts list) \u2014 **not** the parent lead id.
|
|
6303
6303
|
|
|
6304
|
+
**Only \`source: "org"\` contacts are pinnable.** Every contact returned by \`leadbay_research_lead_by_id\` carries a \`source\` field, and the two sources are separate id namespaces on the backend:
|
|
6305
|
+
|
|
6306
|
+
- \`source: "org"\` \u2014 a row in your organization's own contact directory. Pinnable. Also carries \`pinned\` (true when someone has pinned it) and \`pinned_by_ai\` (true when Leadbay's AI pinned it rather than a human).
|
|
6307
|
+
- \`source: "paid"\` \u2014 an enrichment *candidate* (the \`candidates\` bucket): a person Leadbay suggests but has not yet resolved into your directory. NOT pinnable, and carries no \`pinned\` field at all.
|
|
6308
|
+
|
|
6309
|
+
Passing a \`source: "paid"\` id here returns **\`contact not found\`**. That is the expected answer for a candidate, not an outage and not a transient error: nothing is broken, the person is simply not an org contact yet. Do not retry, do not re-fetch the lead hoping for a different result, and do not tell the user that pinning is failing or unavailable.
|
|
6310
|
+
|
|
6311
|
+
To pin someone who is currently only a candidate, first make them an org contact:
|
|
6312
|
+
|
|
6313
|
+
- \`leadbay_enrich_titles\` (or \`leadbay_prepare_outreach\` with \`enrich: true\`) resolves the candidate and writes a NEW org contact for that person. It has a **different \`id\`** from the paid candidate, so re-read the contacts list afterwards and pin the \`source: "org"\` row.
|
|
6314
|
+
- Or add them directly with \`leadbay_add_contact\`, which returns the new org contact's \`id\` \u2014 that id is pinnable immediately.
|
|
6315
|
+
|
|
6316
|
+
**Pinning does not steer enrichment.** It only marks who the priority is on a company the user already has. Enrichment picks people by JOB TITLE, so "enrich the Directeur G\xE9n\xE9ral rather than the Pr\xE9sident" is \`leadbay_enrich_titles\` with the wanted title \u2014 not a pin. Pinning first and enriching after changes nothing about who gets enriched.
|
|
6317
|
+
|
|
6318
|
+
|
|
6304
6319
|
Backend: \`POST /contacts/{contact_id}/pin\` \u2192 204. Idempotent. The inverse is \`leadbay_unpin_contact\`.
|
|
6305
6320
|
|
|
6306
|
-
Returns \`{ pinned: true, contact_id, action: "pinned" }\`.
|
|
6321
|
+
Returns \`{ pinned: true, contact_id, action: "pinned" }\`. To read the resulting state back, re-call \`leadbay_research_lead_by_id\` \u2014 the pinned contact's \`pinned\` flips to \`true\` and it becomes the lead's \`recommended\` contact.
|
|
6307
6322
|
|
|
6308
6323
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
6309
6324
|
`;
|
|
@@ -8248,7 +8263,7 @@ Trigger phrases: "unpin this contact", "remove the pin from this contact", "this
|
|
|
8248
8263
|
|
|
8249
8264
|
Do NOT use for: "pin / mark as priority" \u2192 \`leadbay_pin_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
8250
8265
|
|
|
8251
|
-
Prefer when: user wants to clear the pinned flag on a contact (but keep the contact) \u2014 pass that contact's own \`contact_id\`
|
|
8266
|
+
Prefer when: user wants to clear the pinned flag on a contact (but keep the contact) \u2014 pass that contact's own \`contact_id\`, and ONLY a \`source:"org"\` contact can be unpinned (a \`source:"paid"\` candidate returns 'contact not found')
|
|
8252
8267
|
|
|
8253
8268
|
Examples that SHOULD invoke this tool:
|
|
8254
8269
|
- "Unpin Jane Doe \u2014 she's not the priority anymore."
|
|
@@ -8271,9 +8286,26 @@ Unpin a single contact on a company \u2014 clears its priority / favourite flag.
|
|
|
8271
8286
|
|
|
8272
8287
|
Pass the contact's **own** \`contact_id\` \u2014 not the parent lead id.
|
|
8273
8288
|
|
|
8289
|
+
**Only \`source: "org"\` contacts are pinnable.** Every contact returned by \`leadbay_research_lead_by_id\` carries a \`source\` field, and the two sources are separate id namespaces on the backend:
|
|
8290
|
+
|
|
8291
|
+
- \`source: "org"\` \u2014 a row in your organization's own contact directory. Pinnable. Also carries \`pinned\` (true when someone has pinned it) and \`pinned_by_ai\` (true when Leadbay's AI pinned it rather than a human).
|
|
8292
|
+
- \`source: "paid"\` \u2014 an enrichment *candidate* (the \`candidates\` bucket): a person Leadbay suggests but has not yet resolved into your directory. NOT pinnable, and carries no \`pinned\` field at all.
|
|
8293
|
+
|
|
8294
|
+
Passing a \`source: "paid"\` id here returns **\`contact not found\`**. That is the expected answer for a candidate, not an outage and not a transient error: nothing is broken, the person is simply not an org contact yet. Do not retry, do not re-fetch the lead hoping for a different result, and do not tell the user that pinning is failing or unavailable.
|
|
8295
|
+
|
|
8296
|
+
To pin someone who is currently only a candidate, first make them an org contact:
|
|
8297
|
+
|
|
8298
|
+
- \`leadbay_enrich_titles\` (or \`leadbay_prepare_outreach\` with \`enrich: true\`) resolves the candidate and writes a NEW org contact for that person. It has a **different \`id\`** from the paid candidate, so re-read the contacts list afterwards and pin the \`source: "org"\` row.
|
|
8299
|
+
- Or add them directly with \`leadbay_add_contact\`, which returns the new org contact's \`id\` \u2014 that id is pinnable immediately.
|
|
8300
|
+
|
|
8301
|
+
**Pinning does not steer enrichment.** It only marks who the priority is on a company the user already has. Enrichment picks people by JOB TITLE, so "enrich the Directeur G\xE9n\xE9ral rather than the Pr\xE9sident" is \`leadbay_enrich_titles\` with the wanted title \u2014 not a pin. Pinning first and enriching after changes nothing about who gets enriched.
|
|
8302
|
+
|
|
8303
|
+
|
|
8304
|
+
A \`source: "org"\` contact that was never pinned is a no-op here, not an error \u2014 the backend answers 204 either way. Check \`pinned\` on the contact before calling if you need to tell the user whether anything actually changed.
|
|
8305
|
+
|
|
8274
8306
|
Backend: \`POST /contacts/{contact_id}/unpin\` \u2192 204. Idempotent. The inverse is \`leadbay_pin_contact\`.
|
|
8275
8307
|
|
|
8276
|
-
Returns \`{ pinned: false, contact_id, action: "unpinned" }\`.
|
|
8308
|
+
Returns \`{ pinned: false, contact_id, action: "unpinned" }\`. To read the resulting state back, re-call \`leadbay_research_lead_by_id\` \u2014 the contact's \`pinned\` flips to \`false\` and the lead's \`recommended\` contact reverts to the title-matched default.
|
|
8277
8309
|
|
|
8278
8310
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
8279
8311
|
`;
|
|
@@ -8648,6 +8680,10 @@ var getLeadProfile = {
|
|
|
8648
8680
|
job_title: c.job_title,
|
|
8649
8681
|
recommended: c.recommended,
|
|
8650
8682
|
enrichment: c.enrichment,
|
|
8683
|
+
// Org contacts only — the backend's PaidContactPayload carries no pin
|
|
8684
|
+
// state, because a paid candidate cannot be pinned.
|
|
8685
|
+
pinned: c.pinned ?? false,
|
|
8686
|
+
pinned_by_ai: c.pinned_by_ai ?? false,
|
|
8651
8687
|
source: "org"
|
|
8652
8688
|
})),
|
|
8653
8689
|
...paidContacts.map((c) => ({
|
|
@@ -8732,7 +8768,7 @@ var getContacts = {
|
|
|
8732
8768
|
properties: {
|
|
8733
8769
|
contacts: {
|
|
8734
8770
|
type: "array",
|
|
8735
|
-
description: "Merged org+paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}.",
|
|
8771
|
+
description: "Merged org+paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}. `source:'org'` entries additionally carry {pinned, pinned_by_ai}; `source:'paid'` entries do not, because a paid candidate cannot be pinned \u2014 passing its id to leadbay_pin_contact / leadbay_unpin_contact returns NOT_FOUND.",
|
|
8736
8772
|
items: {
|
|
8737
8773
|
type: "object",
|
|
8738
8774
|
properties: {
|
|
@@ -8744,6 +8780,14 @@ var getContacts = {
|
|
|
8744
8780
|
linkedin_page: { type: ["string", "null"] },
|
|
8745
8781
|
job_title: { type: ["string", "null"] },
|
|
8746
8782
|
recommended: { type: "boolean" },
|
|
8783
|
+
pinned: {
|
|
8784
|
+
type: "boolean",
|
|
8785
|
+
description: "Someone flagged this person as the priority on the company. Present on source:'org' contacts only."
|
|
8786
|
+
},
|
|
8787
|
+
pinned_by_ai: {
|
|
8788
|
+
type: "boolean",
|
|
8789
|
+
description: "The pin came from Leadbay's AI rather than a human. Present on source:'org' contacts only."
|
|
8790
|
+
},
|
|
8747
8791
|
source: { type: "string", enum: ["org", "paid"] },
|
|
8748
8792
|
enrichment: {
|
|
8749
8793
|
type: ["object", "null"],
|
|
@@ -8802,6 +8846,10 @@ var getContacts = {
|
|
|
8802
8846
|
job_title: c.job_title,
|
|
8803
8847
|
recommended: c.recommended,
|
|
8804
8848
|
enrichment: c.enrichment,
|
|
8849
|
+
// Org contacts only — the backend's PaidContactPayload carries no
|
|
8850
|
+
// pin state, because a paid candidate cannot be pinned.
|
|
8851
|
+
pinned: c.pinned ?? false,
|
|
8852
|
+
pinned_by_ai: c.pinned_by_ai ?? false,
|
|
8805
8853
|
source: "org"
|
|
8806
8854
|
})),
|
|
8807
8855
|
...paidContacts.map((c) => ({
|
|
@@ -13931,6 +13979,7 @@ var removeContact = {
|
|
|
13931
13979
|
};
|
|
13932
13980
|
|
|
13933
13981
|
// ../core/dist/tools/pin-contact.js
|
|
13982
|
+
var NOT_PINNABLE_HINT = 'This contact id is not in your organization\'s contact directory, so it cannot be pinned or unpinned. Almost always it is a `source: "paid"` enrichment candidate from leadbay_research_lead_by_id\'s `candidates` list; only `source: "org"` contacts are pinnable. The id is not wrong and the tool is not broken, so do NOT retry it. To act on this person, enrich them by job title with leadbay_enrich_titles, or add them with leadbay_add_contact \u2014 either produces a NEW org contact with a different id, which is pinnable. Note that pinning does not decide who gets enriched; enrichment selects people by job title.';
|
|
13934
13983
|
var pinContact = {
|
|
13935
13984
|
name: "leadbay_pin_contact",
|
|
13936
13985
|
description: leadbay_pin_contact,
|
|
@@ -13954,7 +14003,13 @@ var pinContact = {
|
|
|
13954
14003
|
additionalProperties: false
|
|
13955
14004
|
},
|
|
13956
14005
|
execute: async (client, params, _ctx) => {
|
|
13957
|
-
|
|
14006
|
+
try {
|
|
14007
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/pin`);
|
|
14008
|
+
} catch (e) {
|
|
14009
|
+
if (e?.code === "NOT_FOUND")
|
|
14010
|
+
throw { ...e, hint: NOT_PINNABLE_HINT };
|
|
14011
|
+
throw e;
|
|
14012
|
+
}
|
|
13958
14013
|
return { pinned: true, contact_id: params.contact_id, action: "pinned" };
|
|
13959
14014
|
}
|
|
13960
14015
|
};
|
|
@@ -13983,7 +14038,13 @@ var unpinContact = {
|
|
|
13983
14038
|
additionalProperties: false
|
|
13984
14039
|
},
|
|
13985
14040
|
execute: async (client, params, _ctx) => {
|
|
13986
|
-
|
|
14041
|
+
try {
|
|
14042
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/unpin`);
|
|
14043
|
+
} catch (e) {
|
|
14044
|
+
if (e?.code === "NOT_FOUND")
|
|
14045
|
+
throw { ...e, hint: NOT_PINNABLE_HINT };
|
|
14046
|
+
throw e;
|
|
14047
|
+
}
|
|
13987
14048
|
return { pinned: false, contact_id: params.contact_id, action: "unpinned" };
|
|
13988
14049
|
}
|
|
13989
14050
|
};
|
|
@@ -15977,7 +16038,8 @@ ${firm.short_description}`);
|
|
|
15977
16038
|
const ln = c.last_name ?? "";
|
|
15978
16039
|
const title = c.job_title ?? "\u2014";
|
|
15979
16040
|
const channel = c.email ?? c.phone_number ?? "\u2014";
|
|
15980
|
-
|
|
16041
|
+
const pin = c.pinned ? " \u{1F4CC}" : "";
|
|
16042
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}**${pin} \u2014 ${title} \xB7 ${channel}`);
|
|
15981
16043
|
}
|
|
15982
16044
|
}
|
|
15983
16045
|
const candidates = Array.isArray(contacts.candidates) ? contacts.candidates : [];
|
|
@@ -15989,7 +16051,8 @@ ${firm.short_description}`);
|
|
|
15989
16051
|
const ln = c.last_name ?? "";
|
|
15990
16052
|
const title = c.job_title ?? "\u2014";
|
|
15991
16053
|
const li = c.linkedin_page ? `LinkedIn` : "no LinkedIn";
|
|
15992
|
-
|
|
16054
|
+
const pin = c.pinned ? " \u{1F4CC}" : "";
|
|
16055
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}**${pin} \u2014 ${title} \xB7 ${li}`);
|
|
15993
16056
|
}
|
|
15994
16057
|
if (candidates.length > 10)
|
|
15995
16058
|
out.push(`- _${candidates.length - 10} more \u2026_`);
|
|
@@ -16195,7 +16258,7 @@ var researchLeadById = {
|
|
|
16195
16258
|
},
|
|
16196
16259
|
contacts: {
|
|
16197
16260
|
type: "object",
|
|
16198
|
-
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.',
|
|
16261
|
+
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 / leadbay_pin_contact / leadbay_unpin_contact \u2014 a `"paid"` id returns NOT_FOUND there, which means "this candidate is not an org contact yet", NOT that the tool is broken. Only `source:"org"` contacts carry `pinned` (someone flagged this person as the priority) and `pinned_by_ai` (that someone was Leadbay\'s AI, not a human); paid candidates have no pin state because they cannot be pinned.',
|
|
16199
16262
|
properties: {
|
|
16200
16263
|
reachable: { type: "array", items: { type: "object" } },
|
|
16201
16264
|
candidates: { type: "array", items: { type: "object" } }
|
|
@@ -16320,6 +16383,12 @@ var researchLeadById = {
|
|
|
16320
16383
|
linkedin_page: normalizeLinkedinPage5(c.linkedin_page ?? null),
|
|
16321
16384
|
recommended: c.recommended,
|
|
16322
16385
|
enrichment_done: true,
|
|
16386
|
+
// Pin state exists on org contacts only — mirror the backend, which
|
|
16387
|
+
// omits it entirely from PaidContactPayload. `pinned` is what makes a
|
|
16388
|
+
// pin readable at all; without it the agent can only infer the pin from
|
|
16389
|
+
// `recommended`, which also moves for non-pin reasons.
|
|
16390
|
+
pinned: c.pinned ?? false,
|
|
16391
|
+
pinned_by_ai: c.pinned_by_ai ?? false,
|
|
16323
16392
|
source: "org"
|
|
16324
16393
|
});
|
|
16325
16394
|
const allContacts = [
|
|
@@ -24668,7 +24737,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
24668
24737
|
}
|
|
24669
24738
|
|
|
24670
24739
|
// src/http-server.ts
|
|
24671
|
-
var VERSION = true ? "0.33.
|
|
24740
|
+
var VERSION = true ? "0.33.3" : "0.0.0-dev";
|
|
24672
24741
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
24673
24742
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
24674
24743
|
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.33.
|
|
3
|
+
"version": "0.33.3",
|
|
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",
|