@leadbay/mcp 0.33.1 → 0.33.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 +50 -0
- package/dist/bin.js +57 -18
- package/dist/http-server.js +57 -18
- 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,55 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.33.2 — 2026-09-02
|
|
4
|
+
|
|
5
|
+
Editing one field on a contact erased the others (product#4046).
|
|
6
|
+
|
|
7
|
+
Reproduced on production while verifying leadbay/mcp#194: sending a contact's
|
|
8
|
+
own current `first_name` + `last_name` + `job_title`, changing nothing, deleted
|
|
9
|
+
that contact's email. The contact moved from `contacts.reachable` to
|
|
10
|
+
`contacts.candidates` and `_meta.has_reachable_contact` flipped to false, so the
|
|
11
|
+
lead stopped being contactable. Restored with a second call.
|
|
12
|
+
|
|
13
|
+
This became reachable only the day before: until #194 the tool returned 404 on
|
|
14
|
+
100% of calls, so it destroyed nothing. A tool that always failed now succeeded
|
|
15
|
+
and deleted data.
|
|
16
|
+
|
|
17
|
+
**The backend already had the safe behaviour and we were choosing the other
|
|
18
|
+
one.** Both routes take the same payload
|
|
19
|
+
(`OrgContactRoutes.kt:110,152` → `OrgContactsDaoImpl.kt:265-272`):
|
|
20
|
+
|
|
21
|
+
| Route | `forceUpdateIfNullOrEmpty` | A field absent from the body |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `/contacts/{id}/update` | `true` | written as null — **erased** |
|
|
24
|
+
| `/contacts/{id}/merge` | `false` | skipped — **kept** |
|
|
25
|
+
|
|
26
|
+
So the fix needs no read-modify-write, no extra round-trip and no race window.
|
|
27
|
+
`leadbay_update_contact` now routes by intent:
|
|
28
|
+
|
|
29
|
+
- **Nothing being erased → `/merge`.** Every field the caller omitted survives.
|
|
30
|
+
- **Any field passed as `null` → `/update`**, which rewrites the record — so
|
|
31
|
+
that call must carry all four optional fields. If it does not, the tool
|
|
32
|
+
refuses with `CONTACT_CLEAR_NEEDS_FULL_RECORD` rather than deleting what was
|
|
33
|
+
not mentioned. The destructive path costs more effort than the safe one,
|
|
34
|
+
which is the intended asymmetry.
|
|
35
|
+
- The result carries `mode` (`merge` / `replace`), `preserved` and `cleared`, so
|
|
36
|
+
a wrong edit is visible rather than silent.
|
|
37
|
+
|
|
38
|
+
Verified live on staging, same body to each route:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
POST /contacts/{id}/merge {first_name, last_name, job_title:"CEO"}
|
|
42
|
+
→ job_title CEO, email merge.probe@example.test, phone +15550001111 (kept)
|
|
43
|
+
POST /contacts/{id}/update {first_name, last_name, job_title:"CTO"}
|
|
44
|
+
→ job_title CTO, email absent, phone absent (erased)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Two existing test files were changed rather than added, because both asserted
|
|
48
|
+
the contract this fixes: `update-contact.test.ts`'s happy path now expects
|
|
49
|
+
`/merge`, and `update-contact-null-clear.test.ts`'s clear case now supplies the
|
|
50
|
+
whole record. The second had encoded the data loss as correct — it asserted that
|
|
51
|
+
unmentioned fields are not sent, while posting to the route that deletes them.
|
|
52
|
+
|
|
3
53
|
## 0.33.1 — 2026-09-02
|
|
4
54
|
|
|
5
55
|
Follow-up to 0.32.0 (product#4007). A review finding landed after the merge.
|
package/dist/bin.js
CHANGED
|
@@ -5692,7 +5692,7 @@ Trigger phrases: "update this contact", "fix this contact's title", "change thei
|
|
|
5692
5692
|
|
|
5693
5693
|
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\`.
|
|
5694
5694
|
|
|
5695
|
-
Prefer when: user wants to change details on a contact that is in their own directory (\`source: "org"\`) \u2014 pass
|
|
5695
|
+
Prefer when: user wants to change details on a contact that is in their own directory (\`source: "org"\`) \u2014 pass its \`contact_id\`, first_name, last_name and only the fields being changed
|
|
5696
5696
|
|
|
5697
5697
|
Examples that SHOULD invoke this tool:
|
|
5698
5698
|
- "Update Jane's title to SVP Engineering."
|
|
@@ -5725,11 +5725,30 @@ Pass the contact's **own** \`contact_id\` \u2014 **not** the parent lead id.
|
|
|
5725
5725
|
|
|
5726
5726
|
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.
|
|
5727
5727
|
|
|
5728
|
-
|
|
5728
|
+
## Omitting a field keeps it. Erasing one takes an explicit \`null\`.
|
|
5729
5729
|
|
|
5730
|
-
|
|
5730
|
+
Send only what you are changing. Any field you leave out keeps its current value \u2014 you do NOT need to read the contact first and echo everything back.
|
|
5731
5731
|
|
|
5732
|
-
|
|
5732
|
+
\`\`\`
|
|
5733
|
+
{ contact_id, first_name, last_name, job_title: "CEO" }
|
|
5734
|
+
\u2192 title becomes CEO. email, phone and LinkedIn are untouched.
|
|
5735
|
+
\`\`\`
|
|
5736
|
+
|
|
5737
|
+
**To erase a field, pass it as \`null\`.** Because erasing rewrites the whole record, that call must carry ALL of \`job_title\`, \`linkedin_page\`, \`email\`, \`phone_number\` \u2014 current value for the ones to keep, \`null\` for the ones to erase. If any are missing the call is refused with \`CONTACT_CLEAR_NEEDS_FULL_RECORD\` rather than deleting them; read the contact with \`leadbay_research_lead_by_id\` and re-call.
|
|
5738
|
+
|
|
5739
|
+
\`\`\`
|
|
5740
|
+
{ contact_id, first_name, last_name, email: null,
|
|
5741
|
+
job_title: "CEO", phone_number: "+33\u2026", linkedin_page: "https://\u2026" }
|
|
5742
|
+
\u2192 email erased, everything else as given.
|
|
5743
|
+
\`\`\`
|
|
5744
|
+
|
|
5745
|
+
\`first_name\` + \`last_name\` are required on every call. The backend validates the contact's identity and rejects a body without them (\`invalid contact\`), so pass the current values when you are not changing the name.
|
|
5746
|
+
|
|
5747
|
+
The result tells you which happened: \`mode\` is \`merge\` or \`replace\`, \`preserved\` lists the fields left untouched, \`cleared\` lists the fields erased. **Check \`cleared\` is what you intended.**
|
|
5748
|
+
|
|
5749
|
+
Backend: \`POST /contacts/{contact_id}/merge\` when nothing is being erased, \`POST /contacts/{contact_id}/update\` when something is. Snake_case body; camel-case is rejected. Edits in place (same id).
|
|
5750
|
+
|
|
5751
|
+
Returns \`{ updated: true, contact_id, mode, preserved, cleared, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number } }\`.
|
|
5733
5752
|
|
|
5734
5753
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
5735
5754
|
`;
|
|
@@ -11873,39 +11892,59 @@ var init_update_contact = __esm({
|
|
|
11873
11892
|
// new value. execute forwards null verbatim; the backend accepts it.
|
|
11874
11893
|
job_title: {
|
|
11875
11894
|
type: ["string", "null"],
|
|
11876
|
-
description: "Contact job title. Pass null to
|
|
11895
|
+
description: "Contact job title. Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
11877
11896
|
},
|
|
11878
11897
|
linkedin_page: {
|
|
11879
11898
|
type: ["string", "null"],
|
|
11880
|
-
description: "Contact LinkedIn URL. Pass null to
|
|
11899
|
+
description: "Contact LinkedIn URL. Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
11881
11900
|
},
|
|
11882
11901
|
email: {
|
|
11883
11902
|
type: ["string", "null"],
|
|
11884
|
-
description: "Contact email. Pass null to
|
|
11903
|
+
description: "Contact email. Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
11885
11904
|
},
|
|
11886
11905
|
phone_number: {
|
|
11887
11906
|
type: ["string", "null"],
|
|
11888
|
-
description: "Contact phone (free-form). Pass null to
|
|
11907
|
+
description: "Contact phone (free-form). Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
11889
11908
|
}
|
|
11890
11909
|
},
|
|
11891
11910
|
required: ["contact_id", "first_name", "last_name"],
|
|
11892
11911
|
additionalProperties: false
|
|
11893
11912
|
},
|
|
11894
11913
|
execute: async (client, params, _ctx) => {
|
|
11914
|
+
const OPTIONAL = ["job_title", "linkedin_page", "email", "phone_number"];
|
|
11915
|
+
const asked = (f) => params[f] !== void 0;
|
|
11916
|
+
const clearing = OPTIONAL.filter((f) => params[f] === null);
|
|
11895
11917
|
const body = {
|
|
11896
11918
|
first_name: params.first_name,
|
|
11897
11919
|
last_name: params.last_name
|
|
11898
11920
|
};
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11921
|
+
for (const f of OPTIONAL)
|
|
11922
|
+
if (asked(f))
|
|
11923
|
+
body[f] = params[f];
|
|
11924
|
+
if (clearing.length === 0) {
|
|
11925
|
+
const contact2 = await client.request("POST", `/contacts/${params.contact_id}/merge`, body);
|
|
11926
|
+
return {
|
|
11927
|
+
updated: true,
|
|
11928
|
+
contact_id: params.contact_id,
|
|
11929
|
+
contact: contact2,
|
|
11930
|
+
mode: "merge",
|
|
11931
|
+
preserved: OPTIONAL.filter((f) => !asked(f)),
|
|
11932
|
+
cleared: []
|
|
11933
|
+
};
|
|
11934
|
+
}
|
|
11935
|
+
const missing = OPTIONAL.filter((f) => !asked(f));
|
|
11936
|
+
if (missing.length > 0) {
|
|
11937
|
+
throw client.makeError("CONTACT_CLEAR_NEEDS_FULL_RECORD", `Clearing ${clearing.join(", ")} rewrites the whole contact, and ${missing.join(", ")} ${missing.length === 1 ? "was" : "were"} not supplied`, `Read the contact (leadbay_research_lead_by_id) and re-call with ALL of ${OPTIONAL.join(", ")} \u2014 current value to keep it, null to clear it. Omitting a field here would delete it.`, `POST /contacts/${params.contact_id}/update`);
|
|
11938
|
+
}
|
|
11907
11939
|
const contact = await client.request("POST", `/contacts/${params.contact_id}/update`, body);
|
|
11908
|
-
return {
|
|
11940
|
+
return {
|
|
11941
|
+
updated: true,
|
|
11942
|
+
contact_id: params.contact_id,
|
|
11943
|
+
contact,
|
|
11944
|
+
mode: "replace",
|
|
11945
|
+
preserved: [],
|
|
11946
|
+
cleared: clearing
|
|
11947
|
+
};
|
|
11909
11948
|
}
|
|
11910
11949
|
};
|
|
11911
11950
|
}
|
|
@@ -27862,7 +27901,7 @@ var OAUTH_BASE_URLS = {
|
|
|
27862
27901
|
fr: "https://staging.api.leadbay.app"
|
|
27863
27902
|
}
|
|
27864
27903
|
};
|
|
27865
|
-
var VERSION = "0.33.
|
|
27904
|
+
var VERSION = "0.33.2";
|
|
27866
27905
|
var HELP = `
|
|
27867
27906
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
27868
27907
|
|
package/dist/http-server.js
CHANGED
|
@@ -8283,7 +8283,7 @@ Trigger phrases: "update this contact", "fix this contact's title", "change thei
|
|
|
8283
8283
|
|
|
8284
8284
|
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\`.
|
|
8285
8285
|
|
|
8286
|
-
Prefer when: user wants to change details on a contact that is in their own directory (\`source: "org"\`) \u2014 pass
|
|
8286
|
+
Prefer when: user wants to change details on a contact that is in their own directory (\`source: "org"\`) \u2014 pass its \`contact_id\`, first_name, last_name and only the fields being changed
|
|
8287
8287
|
|
|
8288
8288
|
Examples that SHOULD invoke this tool:
|
|
8289
8289
|
- "Update Jane's title to SVP Engineering."
|
|
@@ -8316,11 +8316,30 @@ Pass the contact's **own** \`contact_id\` \u2014 **not** the parent lead id.
|
|
|
8316
8316
|
|
|
8317
8317
|
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.
|
|
8318
8318
|
|
|
8319
|
-
|
|
8319
|
+
## Omitting a field keeps it. Erasing one takes an explicit \`null\`.
|
|
8320
8320
|
|
|
8321
|
-
|
|
8321
|
+
Send only what you are changing. Any field you leave out keeps its current value \u2014 you do NOT need to read the contact first and echo everything back.
|
|
8322
8322
|
|
|
8323
|
-
|
|
8323
|
+
\`\`\`
|
|
8324
|
+
{ contact_id, first_name, last_name, job_title: "CEO" }
|
|
8325
|
+
\u2192 title becomes CEO. email, phone and LinkedIn are untouched.
|
|
8326
|
+
\`\`\`
|
|
8327
|
+
|
|
8328
|
+
**To erase a field, pass it as \`null\`.** Because erasing rewrites the whole record, that call must carry ALL of \`job_title\`, \`linkedin_page\`, \`email\`, \`phone_number\` \u2014 current value for the ones to keep, \`null\` for the ones to erase. If any are missing the call is refused with \`CONTACT_CLEAR_NEEDS_FULL_RECORD\` rather than deleting them; read the contact with \`leadbay_research_lead_by_id\` and re-call.
|
|
8329
|
+
|
|
8330
|
+
\`\`\`
|
|
8331
|
+
{ contact_id, first_name, last_name, email: null,
|
|
8332
|
+
job_title: "CEO", phone_number: "+33\u2026", linkedin_page: "https://\u2026" }
|
|
8333
|
+
\u2192 email erased, everything else as given.
|
|
8334
|
+
\`\`\`
|
|
8335
|
+
|
|
8336
|
+
\`first_name\` + \`last_name\` are required on every call. The backend validates the contact's identity and rejects a body without them (\`invalid contact\`), so pass the current values when you are not changing the name.
|
|
8337
|
+
|
|
8338
|
+
The result tells you which happened: \`mode\` is \`merge\` or \`replace\`, \`preserved\` lists the fields left untouched, \`cleared\` lists the fields erased. **Check \`cleared\` is what you intended.**
|
|
8339
|
+
|
|
8340
|
+
Backend: \`POST /contacts/{contact_id}/merge\` when nothing is being erased, \`POST /contacts/{contact_id}/update\` when something is. Snake_case body; camel-case is rejected. Edits in place (same id).
|
|
8341
|
+
|
|
8342
|
+
Returns \`{ updated: true, contact_id, mode, preserved, cleared, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number } }\`.
|
|
8324
8343
|
|
|
8325
8344
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
8326
8345
|
`;
|
|
@@ -14000,39 +14019,59 @@ var updateContact = {
|
|
|
14000
14019
|
// new value. execute forwards null verbatim; the backend accepts it.
|
|
14001
14020
|
job_title: {
|
|
14002
14021
|
type: ["string", "null"],
|
|
14003
|
-
description: "Contact job title. Pass null to
|
|
14022
|
+
description: "Contact job title. Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
14004
14023
|
},
|
|
14005
14024
|
linkedin_page: {
|
|
14006
14025
|
type: ["string", "null"],
|
|
14007
|
-
description: "Contact LinkedIn URL. Pass null to
|
|
14026
|
+
description: "Contact LinkedIn URL. Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
14008
14027
|
},
|
|
14009
14028
|
email: {
|
|
14010
14029
|
type: ["string", "null"],
|
|
14011
|
-
description: "Contact email. Pass null to
|
|
14030
|
+
description: "Contact email. Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
14012
14031
|
},
|
|
14013
14032
|
phone_number: {
|
|
14014
14033
|
type: ["string", "null"],
|
|
14015
|
-
description: "Contact phone (free-form). Pass null to
|
|
14034
|
+
description: "Contact phone (free-form). Omit it to leave it unchanged. Pass null to ERASE it \u2014 an erase rewrites the whole contact, so that call must also carry every other optional field, or it is refused."
|
|
14016
14035
|
}
|
|
14017
14036
|
},
|
|
14018
14037
|
required: ["contact_id", "first_name", "last_name"],
|
|
14019
14038
|
additionalProperties: false
|
|
14020
14039
|
},
|
|
14021
14040
|
execute: async (client, params, _ctx) => {
|
|
14041
|
+
const OPTIONAL = ["job_title", "linkedin_page", "email", "phone_number"];
|
|
14042
|
+
const asked = (f) => params[f] !== void 0;
|
|
14043
|
+
const clearing = OPTIONAL.filter((f) => params[f] === null);
|
|
14022
14044
|
const body = {
|
|
14023
14045
|
first_name: params.first_name,
|
|
14024
14046
|
last_name: params.last_name
|
|
14025
14047
|
};
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14048
|
+
for (const f of OPTIONAL)
|
|
14049
|
+
if (asked(f))
|
|
14050
|
+
body[f] = params[f];
|
|
14051
|
+
if (clearing.length === 0) {
|
|
14052
|
+
const contact2 = await client.request("POST", `/contacts/${params.contact_id}/merge`, body);
|
|
14053
|
+
return {
|
|
14054
|
+
updated: true,
|
|
14055
|
+
contact_id: params.contact_id,
|
|
14056
|
+
contact: contact2,
|
|
14057
|
+
mode: "merge",
|
|
14058
|
+
preserved: OPTIONAL.filter((f) => !asked(f)),
|
|
14059
|
+
cleared: []
|
|
14060
|
+
};
|
|
14061
|
+
}
|
|
14062
|
+
const missing = OPTIONAL.filter((f) => !asked(f));
|
|
14063
|
+
if (missing.length > 0) {
|
|
14064
|
+
throw client.makeError("CONTACT_CLEAR_NEEDS_FULL_RECORD", `Clearing ${clearing.join(", ")} rewrites the whole contact, and ${missing.join(", ")} ${missing.length === 1 ? "was" : "were"} not supplied`, `Read the contact (leadbay_research_lead_by_id) and re-call with ALL of ${OPTIONAL.join(", ")} \u2014 current value to keep it, null to clear it. Omitting a field here would delete it.`, `POST /contacts/${params.contact_id}/update`);
|
|
14065
|
+
}
|
|
14034
14066
|
const contact = await client.request("POST", `/contacts/${params.contact_id}/update`, body);
|
|
14035
|
-
return {
|
|
14067
|
+
return {
|
|
14068
|
+
updated: true,
|
|
14069
|
+
contact_id: params.contact_id,
|
|
14070
|
+
contact,
|
|
14071
|
+
mode: "replace",
|
|
14072
|
+
preserved: [],
|
|
14073
|
+
cleared: clearing
|
|
14074
|
+
};
|
|
14036
14075
|
}
|
|
14037
14076
|
};
|
|
14038
14077
|
|
|
@@ -24629,7 +24668,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
24629
24668
|
}
|
|
24630
24669
|
|
|
24631
24670
|
// src/http-server.ts
|
|
24632
|
-
var VERSION = true ? "0.33.
|
|
24671
|
+
var VERSION = true ? "0.33.2" : "0.0.0-dev";
|
|
24633
24672
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
24634
24673
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
24635
24674
|
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.2",
|
|
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",
|