@leadbay/mcp 0.33.0 → 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 +69 -0
- package/dist/bin.js +69 -21
- package/dist/http-server.js +68 -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,74 @@
|
|
|
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
|
+
|
|
53
|
+
## 0.33.1 — 2026-09-02
|
|
54
|
+
|
|
55
|
+
Follow-up to 0.32.0 (product#4007). A review finding landed after the merge.
|
|
56
|
+
|
|
57
|
+
- **Only a REFUSAL counts as a commit failure.** The detached finisher's catch
|
|
58
|
+
recorded ANY rejection from the `pollPreprocess` → `commitMappings` chain as a
|
|
59
|
+
permanent commit failure — including the `ImportPhaseTimeout` raised when
|
|
60
|
+
preprocess still hasn't finished after the finisher's own 10-minute window.
|
|
61
|
+
That is not a backend refusal; the import is merely slow and may yet finish.
|
|
62
|
+
The registry has no expiry, so `leadbay_import_status` would have answered
|
|
63
|
+
`failed` for ever on a healthy import — worse than the forever-polling 0.32.0
|
|
64
|
+
set out to remove. A budget timeout now leaves the status at `committing`,
|
|
65
|
+
which is the honest reading; only an error from `commitMappings` itself is
|
|
66
|
+
recorded.
|
|
67
|
+
- The finisher's window is deliberately not caller-controllable — a caller
|
|
68
|
+
shrinking it is the bug that budget exists to prevent — so the test reaches it
|
|
69
|
+
through a named module seam beside the existing `clearCommitFailures`, rather
|
|
70
|
+
than a parameter that would leak into the tool's public schema.
|
|
71
|
+
|
|
3
72
|
## 0.33.0 — 2026-09-01
|
|
4
73
|
|
|
5
74
|
Agent memory is removed (product#3996). The three `leadbay_agent_memory_*`
|
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
|
`;
|
|
@@ -9389,7 +9408,15 @@ function resumeParkedUpload(client, upload, mappings, ctx) {
|
|
|
9389
9408
|
const { importId } = upload;
|
|
9390
9409
|
setTimeout(() => {
|
|
9391
9410
|
void (async () => {
|
|
9392
|
-
|
|
9411
|
+
try {
|
|
9412
|
+
await pollPreprocess(client, importId, resumeCommitBudgetMs, bgCtx, void 0);
|
|
9413
|
+
} catch (err) {
|
|
9414
|
+
if (err instanceof ImportPhaseTimeout) {
|
|
9415
|
+
bgCtx.logger?.warn?.(`import-leads: parked upload ${importId} still preprocessing after ${resumeCommitBudgetMs}ms; leaving it to the backend`);
|
|
9416
|
+
return;
|
|
9417
|
+
}
|
|
9418
|
+
throw err;
|
|
9419
|
+
}
|
|
9393
9420
|
await commitMappings(client, importId, mappings, bgCtx);
|
|
9394
9421
|
})().then(() => ctx?.logger?.info?.(`import-leads: parked upload ${importId} committed; backend is processing`), (err) => {
|
|
9395
9422
|
recordCommitFailure(importId, err?.message ?? err?.code ?? "mapping commit failed");
|
|
@@ -9437,7 +9464,7 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
9437
9464
|
})();
|
|
9438
9465
|
}, 0);
|
|
9439
9466
|
}
|
|
9440
|
-
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, ImportPhaseTimeout, LEAD_STATUSES, LEAD_STATUS_SET, importLeads,
|
|
9467
|
+
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, ImportPhaseTimeout, LEAD_STATUSES, LEAD_STATUS_SET, importLeads, DEFAULT_RESUME_COMMIT_BUDGET_MS, resumeCommitBudgetMs;
|
|
9441
9468
|
var init_import_leads = __esm({
|
|
9442
9469
|
"../core/dist/composite/import-leads.js"() {
|
|
9443
9470
|
"use strict";
|
|
@@ -9829,7 +9856,8 @@ var init_import_leads = __esm({
|
|
|
9829
9856
|
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
9830
9857
|
}
|
|
9831
9858
|
};
|
|
9832
|
-
|
|
9859
|
+
DEFAULT_RESUME_COMMIT_BUDGET_MS = 10 * 6e4;
|
|
9860
|
+
resumeCommitBudgetMs = DEFAULT_RESUME_COMMIT_BUDGET_MS;
|
|
9833
9861
|
}
|
|
9834
9862
|
});
|
|
9835
9863
|
|
|
@@ -11864,39 +11892,59 @@ var init_update_contact = __esm({
|
|
|
11864
11892
|
// new value. execute forwards null verbatim; the backend accepts it.
|
|
11865
11893
|
job_title: {
|
|
11866
11894
|
type: ["string", "null"],
|
|
11867
|
-
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."
|
|
11868
11896
|
},
|
|
11869
11897
|
linkedin_page: {
|
|
11870
11898
|
type: ["string", "null"],
|
|
11871
|
-
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."
|
|
11872
11900
|
},
|
|
11873
11901
|
email: {
|
|
11874
11902
|
type: ["string", "null"],
|
|
11875
|
-
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."
|
|
11876
11904
|
},
|
|
11877
11905
|
phone_number: {
|
|
11878
11906
|
type: ["string", "null"],
|
|
11879
|
-
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."
|
|
11880
11908
|
}
|
|
11881
11909
|
},
|
|
11882
11910
|
required: ["contact_id", "first_name", "last_name"],
|
|
11883
11911
|
additionalProperties: false
|
|
11884
11912
|
},
|
|
11885
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);
|
|
11886
11917
|
const body = {
|
|
11887
11918
|
first_name: params.first_name,
|
|
11888
11919
|
last_name: params.last_name
|
|
11889
11920
|
};
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
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
|
+
}
|
|
11898
11939
|
const contact = await client.request("POST", `/contacts/${params.contact_id}/update`, body);
|
|
11899
|
-
return {
|
|
11940
|
+
return {
|
|
11941
|
+
updated: true,
|
|
11942
|
+
contact_id: params.contact_id,
|
|
11943
|
+
contact,
|
|
11944
|
+
mode: "replace",
|
|
11945
|
+
preserved: [],
|
|
11946
|
+
cleared: clearing
|
|
11947
|
+
};
|
|
11900
11948
|
}
|
|
11901
11949
|
};
|
|
11902
11950
|
}
|
|
@@ -27853,7 +27901,7 @@ var OAUTH_BASE_URLS = {
|
|
|
27853
27901
|
fr: "https://staging.api.leadbay.app"
|
|
27854
27902
|
}
|
|
27855
27903
|
};
|
|
27856
|
-
var VERSION = "0.33.
|
|
27904
|
+
var VERSION = "0.33.2";
|
|
27857
27905
|
var HELP = `
|
|
27858
27906
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
27859
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
|
`;
|
|
@@ -12159,13 +12178,22 @@ var importLeads = {
|
|
|
12159
12178
|
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
12160
12179
|
}
|
|
12161
12180
|
};
|
|
12162
|
-
var
|
|
12181
|
+
var DEFAULT_RESUME_COMMIT_BUDGET_MS = 10 * 6e4;
|
|
12182
|
+
var resumeCommitBudgetMs = DEFAULT_RESUME_COMMIT_BUDGET_MS;
|
|
12163
12183
|
function resumeParkedUpload(client, upload, mappings, ctx) {
|
|
12164
12184
|
const bgCtx = { logger: ctx?.logger };
|
|
12165
12185
|
const { importId } = upload;
|
|
12166
12186
|
setTimeout(() => {
|
|
12167
12187
|
void (async () => {
|
|
12168
|
-
|
|
12188
|
+
try {
|
|
12189
|
+
await pollPreprocess(client, importId, resumeCommitBudgetMs, bgCtx, void 0);
|
|
12190
|
+
} catch (err) {
|
|
12191
|
+
if (err instanceof ImportPhaseTimeout) {
|
|
12192
|
+
bgCtx.logger?.warn?.(`import-leads: parked upload ${importId} still preprocessing after ${resumeCommitBudgetMs}ms; leaving it to the backend`);
|
|
12193
|
+
return;
|
|
12194
|
+
}
|
|
12195
|
+
throw err;
|
|
12196
|
+
}
|
|
12169
12197
|
await commitMappings(client, importId, mappings, bgCtx);
|
|
12170
12198
|
})().then(() => ctx?.logger?.info?.(`import-leads: parked upload ${importId} committed; backend is processing`), (err) => {
|
|
12171
12199
|
recordCommitFailure(importId, err?.message ?? err?.code ?? "mapping commit failed");
|
|
@@ -13991,39 +14019,59 @@ var updateContact = {
|
|
|
13991
14019
|
// new value. execute forwards null verbatim; the backend accepts it.
|
|
13992
14020
|
job_title: {
|
|
13993
14021
|
type: ["string", "null"],
|
|
13994
|
-
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."
|
|
13995
14023
|
},
|
|
13996
14024
|
linkedin_page: {
|
|
13997
14025
|
type: ["string", "null"],
|
|
13998
|
-
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."
|
|
13999
14027
|
},
|
|
14000
14028
|
email: {
|
|
14001
14029
|
type: ["string", "null"],
|
|
14002
|
-
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."
|
|
14003
14031
|
},
|
|
14004
14032
|
phone_number: {
|
|
14005
14033
|
type: ["string", "null"],
|
|
14006
|
-
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."
|
|
14007
14035
|
}
|
|
14008
14036
|
},
|
|
14009
14037
|
required: ["contact_id", "first_name", "last_name"],
|
|
14010
14038
|
additionalProperties: false
|
|
14011
14039
|
},
|
|
14012
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);
|
|
14013
14044
|
const body = {
|
|
14014
14045
|
first_name: params.first_name,
|
|
14015
14046
|
last_name: params.last_name
|
|
14016
14047
|
};
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
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
|
+
}
|
|
14025
14066
|
const contact = await client.request("POST", `/contacts/${params.contact_id}/update`, body);
|
|
14026
|
-
return {
|
|
14067
|
+
return {
|
|
14068
|
+
updated: true,
|
|
14069
|
+
contact_id: params.contact_id,
|
|
14070
|
+
contact,
|
|
14071
|
+
mode: "replace",
|
|
14072
|
+
preserved: [],
|
|
14073
|
+
cleared: clearing
|
|
14074
|
+
};
|
|
14027
14075
|
}
|
|
14028
14076
|
};
|
|
14029
14077
|
|
|
@@ -24620,7 +24668,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
24620
24668
|
}
|
|
24621
24669
|
|
|
24622
24670
|
// src/http-server.ts
|
|
24623
|
-
var VERSION = true ? "0.33.
|
|
24671
|
+
var VERSION = true ? "0.33.2" : "0.0.0-dev";
|
|
24624
24672
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
24625
24673
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
24626
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",
|