@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 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 that contact's own \`contact_id\` plus first_name + last_name (required) and the fields to change
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
- **\`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.
5728
+ ## Omitting a field keeps it. Erasing one takes an explicit \`null\`.
5729
5729
 
5730
- Backend: \`POST /contacts/{contact_id}/update\` (snake_case body) \u2192 200 with the updated contact. Edits in place (same id). Camel-case bodies are rejected.
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
- Returns \`{ updated: true, contact_id, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number } }\`.
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
- await pollPreprocess(client, importId, RESUME_COMMIT_BUDGET_MS, bgCtx, void 0);
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, RESUME_COMMIT_BUDGET_MS;
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
- RESUME_COMMIT_BUDGET_MS = 10 * 6e4;
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 clear it."
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 clear it."
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 clear it."
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 clear it."
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
- if (params.job_title !== void 0)
11891
- body.job_title = params.job_title;
11892
- if (params.linkedin_page !== void 0)
11893
- body.linkedin_page = params.linkedin_page;
11894
- if (params.email !== void 0)
11895
- body.email = params.email;
11896
- if (params.phone_number !== void 0)
11897
- body.phone_number = params.phone_number;
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 { updated: true, contact_id: params.contact_id, contact };
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.0";
27904
+ var VERSION = "0.33.2";
27857
27905
  var HELP = `
27858
27906
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
27859
27907
 
@@ -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 that contact's own \`contact_id\` plus first_name + last_name (required) and the fields to change
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
- **\`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.
8319
+ ## Omitting a field keeps it. Erasing one takes an explicit \`null\`.
8320
8320
 
8321
- Backend: \`POST /contacts/{contact_id}/update\` (snake_case body) \u2192 200 with the updated contact. Edits in place (same id). Camel-case bodies are rejected.
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
- Returns \`{ updated: true, contact_id, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number } }\`.
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 RESUME_COMMIT_BUDGET_MS = 10 * 6e4;
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
- await pollPreprocess(client, importId, RESUME_COMMIT_BUDGET_MS, bgCtx, void 0);
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 clear it."
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 clear it."
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 clear it."
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 clear it."
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
- if (params.job_title !== void 0)
14018
- body.job_title = params.job_title;
14019
- if (params.linkedin_page !== void 0)
14020
- body.linkedin_page = params.linkedin_page;
14021
- if (params.email !== void 0)
14022
- body.email = params.email;
14023
- if (params.phone_number !== void 0)
14024
- body.phone_number = params.phone_number;
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 { updated: true, contact_id: params.contact_id, contact };
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.0" : "0.0.0-dev";
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 = {
@@ -1804,7 +1804,7 @@ var init_installer_gui = __esm({
1804
1804
  init_install_dxt();
1805
1805
  init_install_shared();
1806
1806
  init_oauth();
1807
- VERSION = true ? "0.33.0" : "0.0.0-dev";
1807
+ VERSION = true ? "0.33.2" : "0.0.0-dev";
1808
1808
  MESSAGES = {
1809
1809
  en: {
1810
1810
  installer: {
@@ -1067,7 +1067,7 @@ async function oauthLogin(opts) {
1067
1067
  }
1068
1068
 
1069
1069
  // installer/installer-gui.ts
1070
- var VERSION = true ? "0.33.0" : "0.0.0-dev";
1070
+ var VERSION = true ? "0.33.2" : "0.0.0-dev";
1071
1071
  var MESSAGES = {
1072
1072
  en: {
1073
1073
  installer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.33.0",
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",