@agifyai/leadify-mcp 8.5.4 → 8.6.1

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.
@@ -63,6 +63,17 @@ export const CRM_SCHEMA_PACKS = [
63
63
  "recruitment_hr",
64
64
  "community_creator",
65
65
  ];
66
+ const optimisticPersonaMutationShape = {
67
+ organization_id: z
68
+ .string()
69
+ .trim()
70
+ .min(1)
71
+ .describe("Required organization ID; no tenant is inferred."),
72
+ expected_updated_at: z
73
+ .string()
74
+ .datetime({ offset: true })
75
+ .describe("Exact Persona updatedAt returned by the read that this edit is based on. A stale value is rejected with 409."),
76
+ };
66
77
  const SECTOR_BASE_COMPATIBILITY = {
67
78
  healthcare: ["base_person", "base_company"],
68
79
  medtech: ["base_person", "base_company"],
@@ -174,10 +185,10 @@ class PersonaPatchError extends Error {
174
185
  * Strategy: fast-path the direct GET; on 404, fall back to the permissive
175
186
  * list endpoint and find by id. Other errors propagate immediately.
176
187
  */
177
- async function fetchPersona(id, client = getClient()) {
188
+ async function fetchPersona(id, organizationId, client = getClient()) {
178
189
  const directPath = `/api/persona/${encodeURIComponent(id)}`;
179
190
  try {
180
- const data = await client.get(directPath);
191
+ const data = await client.get(directPath, new URLSearchParams({ organizationId }));
181
192
  if (!data || typeof data !== "object") {
182
193
  throw new PersonaPatchError("read", directPath, null, data, "Unexpected response shape (expected an object).");
183
194
  }
@@ -192,12 +203,11 @@ async function fetchPersona(id, client = getClient()) {
192
203
  }
193
204
  // 404 on direct GET — fall through to list-based lookup.
194
205
  }
195
- // Permissive fallback: list returns everything the API key can see
196
- // (own org + globals for regular users; cross-org for super admins).
206
+ // Tenant-scoped fallback keeps the same explicit organization boundary.
197
207
  const listPath = "/api/persona";
198
208
  let listed;
199
209
  try {
200
- listed = await client.get(listPath);
210
+ listed = await client.get(listPath, new URLSearchParams({ organizationId }));
201
211
  }
202
212
  catch (err) {
203
213
  if (err instanceof LeadifyApiError) {
@@ -218,12 +228,26 @@ async function fetchPersona(id, client = getClient()) {
218
228
  }
219
229
  return found;
220
230
  }
221
- async function postPersonaUpdate(id, name, fieldsToWrite, client = getClient()) {
231
+ async function postPersonaUpdate(persona, fieldsToWrite, organizationId, expectedUpdatedAt, client = getClient()) {
222
232
  const path = "/api/persona";
233
+ if (typeof persona.verticalId !== "string" || persona.verticalId.length === 0) {
234
+ throw new PersonaPatchError("write", path, 409, { error: "PERSONA_VERTICAL_REQUIRED" }, "Historical global/orphaned Personas cannot be edited.");
235
+ }
236
+ if (typeof persona.updatedAt !== "string" ||
237
+ new Date(persona.updatedAt).toISOString() !== new Date(expectedUpdatedAt).toISOString()) {
238
+ throw new PersonaPatchError("write", path, 409, {
239
+ error: "CONTEXT_ENTITY_UPDATE_CONFLICT",
240
+ expectedUpdatedAt,
241
+ actualUpdatedAt: persona.updatedAt,
242
+ }, "Re-read the Persona and reapply the intended patch.");
243
+ }
223
244
  try {
224
245
  return await client.post(path, {
225
- id,
226
- name,
246
+ id: persona.id,
247
+ name: persona.name,
248
+ organizationId,
249
+ verticalId: persona.verticalId,
250
+ expectedUpdatedAt,
227
251
  ...fieldsToWrite,
228
252
  });
229
253
  }
@@ -271,18 +295,29 @@ export function registerPersonaTools(server, client) {
271
295
  "rewrite a persona. For partial edits, ALWAYS prefer the granular " +
272
296
  "update_persona_* tools — they do safe read-modify-write merges and avoid " +
273
297
  "wiping JSON columns you didn't intend to touch (each JSON column passed in " +
274
- "this tool is replaced wholesale, so omitted sub-keys are LOST).", {
298
+ "this tool is replaced wholesale, so omitted sub-keys are LOST). PRD-1415 requires " +
299
+ "one explicit organization and one explicit Vertical; global/orphaned Personas are rejected.", {
275
300
  // core
276
301
  id: z
277
302
  .string()
278
303
  .optional()
279
304
  .describe("Persona ID to update an existing persona. Omit to create a new one."),
305
+ expected_updated_at: z
306
+ .string()
307
+ .datetime({ offset: true })
308
+ .optional()
309
+ .describe("Required on update: exact updatedAt from the Persona read this rewrite is based on. Omit on create."),
280
310
  name: z.string().describe("Persona name (required)."),
281
311
  organization_id: z
282
312
  .string()
283
- .nullable()
284
- .optional()
285
- .describe("Target organization ID (Clerk org ID). Absent = caller's org. Pass null to make the persona global (admin only)."),
313
+ .trim()
314
+ .min(1)
315
+ .describe("Required target organization ID. There is no implicit tenant and no global Persona."),
316
+ vertical_id: z
317
+ .string()
318
+ .trim()
319
+ .min(1)
320
+ .describe("Required Vertical ID in the same organization."),
286
321
  description: z.string().optional().describe("Detailed persona description."),
287
322
  // targeting
288
323
  target_profile: z
@@ -386,6 +421,15 @@ export function registerPersonaTools(server, client) {
386
421
  .describe("Snippets inserted at named points. scope: 'qualify' | 'outreach'; insertion_point: 'prefix' | 'phase_1_5' | 'phase_2_extra' | 'suffix'."),
387
422
  }, async (params) => {
388
423
  try {
424
+ if (!params.organization_id) {
425
+ throw new Error("organization_id is required; no implicit tenant or global Persona is allowed.");
426
+ }
427
+ if (!params.vertical_id) {
428
+ throw new Error("vertical_id is required; an orphan Persona cannot be created or updated.");
429
+ }
430
+ if (params.id !== undefined && params.expected_updated_at === undefined) {
431
+ throw new Error("expected_updated_at is required when updating a Persona. Re-read it first; stale writes are rejected with 409.");
432
+ }
389
433
  if (params.id === undefined && params.schema_packs === undefined) {
390
434
  throw new Error("schema_packs is required when creating a Persona. Call list_crm_schema_packs, then select exactly one base pack and at most one compatible sector pack.");
391
435
  }
@@ -394,11 +438,15 @@ export function registerPersonaTools(server, client) {
394
438
  if (selectionError)
395
439
  throw new Error(selectionError);
396
440
  }
397
- const body = { name: params.name };
441
+ const body = {
442
+ name: params.name,
443
+ organizationId: params.organization_id,
444
+ verticalId: params.vertical_id,
445
+ };
398
446
  if (params.id !== undefined)
399
447
  body.id = params.id;
400
- if (params.organization_id !== undefined)
401
- body.organizationId = params.organization_id;
448
+ if (params.expected_updated_at !== undefined)
449
+ body.expectedUpdatedAt = params.expected_updated_at;
402
450
  if (params.description !== undefined)
403
451
  body.description = params.description;
404
452
  if (params.target_profile !== undefined)
@@ -467,20 +515,21 @@ export function registerPersonaTools(server, client) {
467
515
  "Reads the current Persona, validates exactly one workflow-compatible base and at most one " +
468
516
  "compatible sector pack, then writes schemaPacks through the canonical Persona endpoint. " +
469
517
  "The response includes the persisted Persona and readiness proofs for every linked group.", {
518
+ ...optimisticPersonaMutationShape,
470
519
  id: z.string().describe("Persona ID to update."),
471
520
  schema_packs: z
472
521
  .array(z.enum(CRM_SCHEMA_PACKS))
473
522
  .min(1)
474
523
  .describe("Complete replacement selection. Call list_crm_schema_packs first."),
475
- }, async ({ id, schema_packs }) => {
524
+ }, async ({ organization_id, expected_updated_at, id, schema_packs }) => {
476
525
  try {
477
526
  const api = client ?? getClient();
478
- const persona = await fetchPersona(id, api);
527
+ const persona = await fetchPersona(id, organization_id, api);
479
528
  const workflowMode = persona.workflowMode === "company_first" ? "company_first" : "person_first";
480
529
  const selectionError = crmSchemaPackSelectionError(schema_packs, workflowMode);
481
530
  if (selectionError)
482
531
  throw new Error(selectionError);
483
- const data = await postPersonaUpdate(id, persona.name, { schemaPacks: schema_packs }, api);
532
+ const data = await postPersonaUpdate(persona, { schemaPacks: schema_packs }, organization_id, expected_updated_at, api);
484
533
  return toolResult(data);
485
534
  }
486
535
  catch (error) {
@@ -494,6 +543,11 @@ export function registerPersonaTools(server, client) {
494
543
  "qualification concerns (~37k chars instead of ~63k). Pass include_outreach=true to " +
495
544
  "get the full payload — but for outreach-only reads prefer get_outreach_templates " +
496
545
  "(a much smaller, dedicated read).", {
546
+ organization_id: z
547
+ .string()
548
+ .trim()
549
+ .min(1)
550
+ .describe("Required organization ID; no tenant is inferred."),
497
551
  id: z.string().describe("Persona ID."),
498
552
  include_outreach: z
499
553
  .boolean()
@@ -502,9 +556,9 @@ export function registerPersonaTools(server, client) {
502
556
  .describe("If true, keep the outreach trio in the response (outreachTemplates, " +
503
557
  "outreachSignalRouting, outreachFields). Default false (SILO: outreach lives " +
504
558
  "in its own tool)."),
505
- }, async ({ id, include_outreach }) => {
559
+ }, async ({ organization_id, id, include_outreach }) => {
506
560
  try {
507
- const data = await (client ?? getClient()).get(`/api/persona/${encodeURIComponent(id)}`);
561
+ const data = await (client ?? getClient()).get(`/api/persona/${encodeURIComponent(id)}`, new URLSearchParams({ organizationId: organization_id }));
508
562
  // SILO defense in depth: strip the outreach trio locally unless the
509
563
  // caller explicitly opted in. Outreach data belongs to the dedicated
510
564
  // outreach tools — never mix it with the persona read by default.
@@ -548,12 +602,13 @@ export function registerPersonaTools(server, client) {
548
602
  // persona below to grab the outreach trio.
549
603
  const groupPersona = (await (client ?? getClient()).get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/persona`));
550
604
  const personaId = groupPersona?.persona?.id;
551
- if (!personaId) {
552
- return toolError(`No persona is assigned to lead group "${lead_group_id}". ` +
553
- `Assign one via update_lead_group({ persona_id }) before reading outreach templates.`);
605
+ const organizationId = groupPersona?.organizationId;
606
+ if (!personaId || !organizationId) {
607
+ return toolError(`No usable tenant-scoped Persona is assigned to lead group "${lead_group_id}". ` +
608
+ `Assign one ACTIVE Persona linked to a Vertical before reading outreach templates.`);
554
609
  }
555
610
  // Step 2: fetch the full persona so we can project the trio.
556
- const full = (await (client ?? getClient()).get(`/api/persona/${encodeURIComponent(personaId)}`));
611
+ const full = (await (client ?? getClient()).get(`/api/persona/${encodeURIComponent(personaId)}`, new URLSearchParams({ organizationId })));
557
612
  // The API may return either a bare persona or {persona: {...}}.
558
613
  const persona = full.persona ?? full;
559
614
  const trio = {
@@ -573,32 +628,42 @@ export function registerPersonaTools(server, client) {
573
628
  return handlePersonaToolError(error);
574
629
  }
575
630
  });
631
+ // ── change_persona_status ─────────────────────────────────────────────
632
+ server.tool("change_persona_status", "Change one tenant-scoped Persona between DRAFT, ACTIVE, and ARCHIVED. " +
633
+ "The exact expected_updated_at from the caller's read is mandatory. Activation requires an ACTIVE Vertical " +
634
+ "plus qualification-ready content; archive is blocked while durable references remain; ARCHIVED can only return to DRAFT.", {
635
+ ...optimisticPersonaMutationShape,
636
+ id: z.string().min(1).describe("Persona ID."),
637
+ status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]),
638
+ }, async ({ organization_id, expected_updated_at, id, status }) => {
639
+ try {
640
+ const query = new URLSearchParams({ organizationId: organization_id });
641
+ const data = await (client ?? getClient()).post(`/api/persona/${encodeURIComponent(id)}/status?${query.toString()}`, { expectedUpdatedAt: expected_updated_at, status });
642
+ return toolResult(data);
643
+ }
644
+ catch (error) {
645
+ return handlePersonaToolError(error);
646
+ }
647
+ });
576
648
  // ── list_personas ──────────────────────────────────────────────────────
577
- server.tool("list_personas", "List all personas accessible to the caller, optionally scoped to a specific organization " +
578
- "and/or including global personas. Use this to discover persona IDs before calling " +
649
+ server.tool("list_personas", "List all usable Persona records in one explicit organization. Historical global/orphaned " +
650
+ "Personas are excluded and available only through the reviewed migration inventory. Use this to discover persona IDs before calling " +
579
651
  "get_persona, upsert_persona (for updates), or assigning a persona to a lead group. " +
580
652
  "Returns a COMPACT view by default (id, name, organizationId, truncated description) " +
581
653
  "to avoid blowing up context — full persona objects can be heavy (~10k chars each). " +
582
654
  "Pass verbose=true to get the full objects, or call get_persona(id) for a single full persona.", {
583
655
  organization_id: z
584
656
  .string()
585
- .optional()
586
- .describe("Filter by organization ID. Pass the literal string 'null' to list only global personas."),
587
- include_globals: z
588
- .boolean()
589
- .optional()
590
- .describe("If true (default), include global personas (organizationId === null) alongside org-scoped ones."),
657
+ .trim()
658
+ .min(1)
659
+ .describe("Required organization ID. The literal string 'null' and implicit tenant selection are forbidden."),
591
660
  verbose: z
592
661
  .boolean()
593
662
  .optional()
594
663
  .describe("If true, return full persona objects with every field. Defaults to false (compact view: id, name, organizationId, short description)."),
595
- }, async ({ organization_id, include_globals, verbose }) => {
664
+ }, async ({ organization_id, verbose }) => {
596
665
  try {
597
- const params = new URLSearchParams();
598
- if (organization_id !== undefined)
599
- params.set("organizationId", organization_id);
600
- if (include_globals !== undefined)
601
- params.set("includeGlobals", String(include_globals));
666
+ const params = new URLSearchParams({ organizationId: organization_id });
602
667
  const data = await (client ?? getClient()).get("/api/persona", params);
603
668
  if (verbose)
604
669
  return toolResult(data);
@@ -615,7 +680,9 @@ export function registerPersonaTools(server, client) {
615
680
  return {
616
681
  id: obj.id,
617
682
  name: obj.name,
618
- organizationId: obj.organizationId ?? null,
683
+ organizationId: obj.organizationId,
684
+ verticalId: obj.verticalId,
685
+ status: obj.status,
619
686
  ...(desc !== undefined && {
620
687
  description: desc.length > DESC_MAX ? `${desc.slice(0, DESC_MAX)}…` : desc,
621
688
  }),
@@ -631,38 +698,6 @@ export function registerPersonaTools(server, client) {
631
698
  return handlePersonaToolError(error);
632
699
  }
633
700
  });
634
- // ── delete_persona ─────────────────────────────────────────────────────
635
- server.tool("delete_persona", "Permanently delete a persona by ID. Irreversible. Fails if the persona is still assigned " +
636
- "to any lead group or referenced by an active campaign. Always confirm with the user " +
637
- "before calling this tool.", {
638
- id: z.string().describe("Persona ID to delete."),
639
- }, async ({ id }) => {
640
- try {
641
- const data = await (client ?? getClient()).delete(`/api/persona/${encodeURIComponent(id)}`);
642
- return toolResult(data);
643
- }
644
- catch (error) {
645
- return handlePersonaToolError(error);
646
- }
647
- });
648
- // ── move_persona ───────────────────────────────────────────────────────
649
- server.tool("move_persona", "Move a persona to a different organization (or make it global by passing null). " +
650
- "Admin permission required. Useful when promoting an org-scoped persona to a shared " +
651
- "global template, or reassigning between orgs.", {
652
- id: z.string().describe("Persona ID to move."),
653
- organization_id: z
654
- .string()
655
- .nullable()
656
- .describe("Target organization ID (Clerk org ID). Pass null to make the persona global."),
657
- }, async ({ id, organization_id }) => {
658
- try {
659
- const data = await (client ?? getClient()).post(`/api/persona/${encodeURIComponent(id)}/move`, { organizationId: organization_id });
660
- return toolResult(data);
661
- }
662
- catch (error) {
663
- return handlePersonaToolError(error);
664
- }
665
- });
666
701
  // ════════════════════════════════════════════════════════════════════════
667
702
  // GRANULAR PATCH TOOLS — read-modify-write, scoped to one section each
668
703
  // ════════════════════════════════════════════════════════════════════════
@@ -670,6 +705,7 @@ export function registerPersonaTools(server, client) {
670
705
  server.tool("update_persona_identity", "Patch top-level scalar fields of a persona (name, description, " +
671
706
  "output_language, workflow_mode, strict_match, enable_signals). " +
672
707
  "Pass only the fields you want to change. Other persona sections are untouched.", {
708
+ ...optimisticPersonaMutationShape,
673
709
  id: z.string().describe("Persona ID."),
674
710
  name: z.string().min(1).optional().describe("New persona name."),
675
711
  description: z.string().nullable().optional().describe("Detailed description."),
@@ -691,7 +727,7 @@ export function registerPersonaTools(server, client) {
691
727
  .boolean()
692
728
  .optional()
693
729
  .describe("Enable business signal detection for this persona."),
694
- }, async ({ id, name, description, output_language, workflow_mode, strict_match, enable_signals, }) => {
730
+ }, async ({ organization_id, expected_updated_at, id, name, description, output_language, workflow_mode, strict_match, enable_signals, }) => {
695
731
  try {
696
732
  if (name === undefined &&
697
733
  description === undefined &&
@@ -701,7 +737,7 @@ export function registerPersonaTools(server, client) {
701
737
  enable_signals === undefined) {
702
738
  return toolError("Provide at least one field to update.");
703
739
  }
704
- const current = await fetchPersona(id, client ?? getClient());
740
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
705
741
  const body = {};
706
742
  if (description !== undefined)
707
743
  body.description = description;
@@ -713,7 +749,7 @@ export function registerPersonaTools(server, client) {
713
749
  body.strictMatch = strict_match;
714
750
  if (enable_signals !== undefined)
715
751
  body.enableSignals = enable_signals;
716
- const data = await postPersonaUpdate(id, name ?? current.name, body, client ?? getClient());
752
+ const data = await postPersonaUpdate({ ...current, name: name ?? current.name }, body, organization_id, expected_updated_at, client ?? getClient());
717
753
  return toolResult(data);
718
754
  }
719
755
  catch (error) {
@@ -726,6 +762,7 @@ export function registerPersonaTools(server, client) {
726
762
  "keys: job_titles, specialties, seniority_min, decision_maker, buying_power, " +
727
763
  "target_company_types, min_company_size, interest_topics. Other targetProfile " +
728
764
  "keys remain intact.", {
765
+ ...optimisticPersonaMutationShape,
729
766
  id: z.string().describe("Persona ID."),
730
767
  patch: z
731
768
  .record(z.unknown())
@@ -735,17 +772,17 @@ export function registerPersonaTools(server, client) {
735
772
  .array(z.string())
736
773
  .optional()
737
774
  .describe("Keys to delete from targetProfile."),
738
- }, async ({ id, patch, remove_keys }) => {
775
+ }, async ({ organization_id, expected_updated_at, id, patch, remove_keys }) => {
739
776
  try {
740
777
  if ((patch === undefined || Object.keys(patch).length === 0) &&
741
778
  (remove_keys === undefined || remove_keys.length === 0)) {
742
779
  return toolError("Provide at least one of: patch (non-empty), remove_keys (non-empty).");
743
780
  }
744
- const current = await fetchPersona(id, client ?? getClient());
781
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
745
782
  const next = { ...asObject(current.targetProfile), ...(patch ?? {}) };
746
783
  for (const k of remove_keys ?? [])
747
784
  delete next[k];
748
- const data = await postPersonaUpdate(id, current.name, { targetProfile: next }, client ?? getClient());
785
+ const data = await postPersonaUpdate(current, { targetProfile: next }, organization_id, expected_updated_at, client ?? getClient());
749
786
  return toolResult(data);
750
787
  }
751
788
  catch (error) {
@@ -755,6 +792,7 @@ export function registerPersonaTools(server, client) {
755
792
  // ── update_persona_disqualification ────────────────────────────────────
756
793
  server.tool("update_persona_disqualification", "Patch the disqualificationCriteria JSON column (shallow merge). Common key: " +
757
794
  "reasons (array of strings). Other keys remain intact.", {
795
+ ...optimisticPersonaMutationShape,
758
796
  id: z.string().describe("Persona ID."),
759
797
  patch: z
760
798
  .record(z.unknown())
@@ -764,17 +802,17 @@ export function registerPersonaTools(server, client) {
764
802
  .array(z.string())
765
803
  .optional()
766
804
  .describe("Keys to delete from disqualificationCriteria."),
767
- }, async ({ id, patch, remove_keys }) => {
805
+ }, async ({ organization_id, expected_updated_at, id, patch, remove_keys }) => {
768
806
  try {
769
807
  if ((patch === undefined || Object.keys(patch).length === 0) &&
770
808
  (remove_keys === undefined || remove_keys.length === 0)) {
771
809
  return toolError("Provide at least one of: patch (non-empty), remove_keys (non-empty).");
772
810
  }
773
- const current = await fetchPersona(id, client ?? getClient());
811
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
774
812
  const next = { ...asObject(current.disqualificationCriteria), ...(patch ?? {}) };
775
813
  for (const k of remove_keys ?? [])
776
814
  delete next[k];
777
- const data = await postPersonaUpdate(id, current.name, { disqualificationCriteria: next }, client ?? getClient());
815
+ const data = await postPersonaUpdate(current, { disqualificationCriteria: next }, organization_id, expected_updated_at, client ?? getClient());
778
816
  return toolResult(data);
779
817
  }
780
818
  catch (error) {
@@ -785,6 +823,7 @@ export function registerPersonaTools(server, client) {
785
823
  server.tool("update_persona_active_tools", "Replace the activeTools list wholesale, OR add/remove specific entries while " +
786
824
  "preserving the rest. Use 'tools' to fully replace; use 'add' / 'remove' for " +
787
825
  "incremental edits (deduped).", {
826
+ ...optimisticPersonaMutationShape,
788
827
  id: z.string().describe("Persona ID."),
789
828
  tools: z
790
829
  .array(z.string())
@@ -798,7 +837,7 @@ export function registerPersonaTools(server, client) {
798
837
  .array(z.string())
799
838
  .optional()
800
839
  .describe("Tools to remove (no-op if absent)."),
801
- }, async ({ id, tools, add, remove }) => {
840
+ }, async ({ organization_id, expected_updated_at, id, tools, add, remove }) => {
802
841
  try {
803
842
  if (tools === undefined && add === undefined && remove === undefined) {
804
843
  return toolError("Provide one of: tools, add, remove.");
@@ -806,7 +845,7 @@ export function registerPersonaTools(server, client) {
806
845
  if (tools !== undefined && (add !== undefined || remove !== undefined)) {
807
846
  return toolError("'tools' is mutually exclusive with 'add' / 'remove'.");
808
847
  }
809
- const current = await fetchPersona(id, client ?? getClient());
848
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
810
849
  let next;
811
850
  if (tools !== undefined) {
812
851
  next = tools;
@@ -819,7 +858,7 @@ export function registerPersonaTools(server, client) {
819
858
  set.delete(t);
820
859
  next = Array.from(set);
821
860
  }
822
- const data = await postPersonaUpdate(id, current.name, { activeTools: next }, client ?? getClient());
861
+ const data = await postPersonaUpdate(current, { activeTools: next }, organization_id, expected_updated_at, client ?? getClient());
823
862
  return toolResult(data);
824
863
  }
825
864
  catch (error) {
@@ -832,6 +871,7 @@ export function registerPersonaTools(server, client) {
832
871
  "{criteria, examples, weight}. When passing a structured patch, sub-fields are " +
833
872
  "merged into the existing structured tier (if any) — pass only what you want " +
834
873
  "to change. Passing a string REPLACES the tier entirely.", {
874
+ ...optimisticPersonaMutationShape,
835
875
  id: z.string().describe("Persona ID."),
836
876
  tier: z
837
877
  .enum(["hot", "warm", "cold", "disqualified"])
@@ -848,7 +888,7 @@ export function registerPersonaTools(server, client) {
848
888
  .passthrough(),
849
889
  ])
850
890
  .describe("New tier value. String replaces wholesale; object is merged."),
851
- }, async ({ id, tier, criteria }) => {
891
+ }, async ({ organization_id, expected_updated_at, id, tier, criteria }) => {
852
892
  try {
853
893
  const fieldMap = {
854
894
  hot: "hotCriteria",
@@ -857,7 +897,7 @@ export function registerPersonaTools(server, client) {
857
897
  disqualified: "disqualifiedCriteria",
858
898
  };
859
899
  const field = fieldMap[tier];
860
- const current = await fetchPersona(id, client ?? getClient());
900
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
861
901
  let nextValue;
862
902
  if (typeof criteria === "string") {
863
903
  nextValue = criteria;
@@ -869,7 +909,7 @@ export function registerPersonaTools(server, client) {
869
909
  : {};
870
910
  nextValue = { ...base, ...criteria };
871
911
  }
872
- const data = await postPersonaUpdate(id, current.name, { [field]: nextValue }, client ?? getClient());
912
+ const data = await postPersonaUpdate(current, { [field]: nextValue }, organization_id, expected_updated_at, client ?? getClient());
873
913
  return toolResult(data);
874
914
  }
875
915
  catch (error) {
@@ -881,6 +921,7 @@ export function registerPersonaTools(server, client) {
881
921
  "list. Lookups are by client `name` (case-sensitive exact match). Reads the " +
882
922
  "current lookalikeClients array, mutates the matched entry, writes the full " +
883
923
  "list back. Use this instead of upsert_persona to avoid wiping the others.", {
924
+ ...optimisticPersonaMutationShape,
884
925
  id: z.string().describe("Persona ID."),
885
926
  action: z
886
927
  .enum(["add", "replace", "remove"])
@@ -892,7 +933,7 @@ export function registerPersonaTools(server, client) {
892
933
  client: lookalikeClient
893
934
  .optional()
894
935
  .describe("Client payload. Required for 'add' and 'replace'. Schema: {name, sector?, size?, segment?, comparison_criteria?}."),
895
- }, async ({ id, action, name, client: lookalikeClientInput }) => {
936
+ }, async ({ organization_id, expected_updated_at, id, action, name, client: lookalikeClientInput }) => {
896
937
  try {
897
938
  if ((action === "add" || action === "replace") && !lookalikeClientInput) {
898
939
  return toolError(`client is required for action '${action}'.`);
@@ -900,7 +941,7 @@ export function registerPersonaTools(server, client) {
900
941
  if ((action === "replace" || action === "remove") && !name) {
901
942
  return toolError(`name is required for action '${action}'.`);
902
943
  }
903
- const current = await fetchPersona(id, client ?? getClient());
944
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
904
945
  const list = asArray(current.lookalikeClients).map((c) => ({ ...c }));
905
946
  if (action === "add") {
906
947
  const newName = lookalikeClientInput.name;
@@ -925,9 +966,7 @@ export function registerPersonaTools(server, client) {
925
966
  list.length = 0;
926
967
  list.push(...filtered);
927
968
  }
928
- const data = await postPersonaUpdate(id, current.name, {
929
- lookalikeClients: list,
930
- }, client ?? getClient());
969
+ const data = await postPersonaUpdate(current, { lookalikeClients: list }, organization_id, expected_updated_at, client ?? getClient());
931
970
  return toolResult(data);
932
971
  }
933
972
  catch (error) {
@@ -938,6 +977,7 @@ export function registerPersonaTools(server, client) {
938
977
  server.tool("update_persona_pain_point", "Add, replace, or remove a single pain point without re-sending the whole list. " +
939
978
  "Pain points have no stable id — entries are addressed by zero-based index. " +
940
979
  "Beware: indices shift after a remove, so re-fetch before chaining edits.", {
980
+ ...optimisticPersonaMutationShape,
941
981
  id: z.string().describe("Persona ID."),
942
982
  action: z
943
983
  .enum(["add", "replace", "remove"])
@@ -951,7 +991,7 @@ export function registerPersonaTools(server, client) {
951
991
  pain: painPoint
952
992
  .optional()
953
993
  .describe("Pain payload {title?, description?}. Required for 'add' and 'replace'."),
954
- }, async ({ id, action, index, pain }) => {
994
+ }, async ({ organization_id, expected_updated_at, id, action, index, pain }) => {
955
995
  try {
956
996
  if ((action === "add" || action === "replace") && !pain) {
957
997
  return toolError(`pain is required for action '${action}'.`);
@@ -959,7 +999,7 @@ export function registerPersonaTools(server, client) {
959
999
  if ((action === "replace" || action === "remove") && index === undefined) {
960
1000
  return toolError(`index is required for action '${action}'.`);
961
1001
  }
962
- const current = await fetchPersona(id, client ?? getClient());
1002
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
963
1003
  const list = asArray(current.painPoints).map((p) => ({ ...p }));
964
1004
  if (action === "add") {
965
1005
  list.push(pain);
@@ -976,7 +1016,7 @@ export function registerPersonaTools(server, client) {
976
1016
  }
977
1017
  list.splice(index, 1);
978
1018
  }
979
- const data = await postPersonaUpdate(id, current.name, { painPoints: list }, client ?? getClient());
1019
+ const data = await postPersonaUpdate(current, { painPoints: list }, organization_id, expected_updated_at, client ?? getClient());
980
1020
  return toolResult(data);
981
1021
  }
982
1022
  catch (error) {
@@ -988,6 +1028,7 @@ export function registerPersonaTools(server, client) {
988
1028
  "tone_instructions, context). Each is a separate scalar JSON column — passing " +
989
1029
  "one replaces it (pass null to clear). Other prompt fields and persona sections " +
990
1030
  "remain intact.", {
1031
+ ...optimisticPersonaMutationShape,
991
1032
  id: z.string().describe("Persona ID."),
992
1033
  normalization_rules: z
993
1034
  .string()
@@ -1009,7 +1050,7 @@ export function registerPersonaTools(server, client) {
1009
1050
  .nullable()
1010
1051
  .optional()
1011
1052
  .describe("Markdown appended to the ICP prompt. Null clears it."),
1012
- }, async ({ id, normalization_rules, signal_guidance, tone_instructions, context, }) => {
1053
+ }, async ({ organization_id, expected_updated_at, id, normalization_rules, signal_guidance, tone_instructions, context, }) => {
1013
1054
  try {
1014
1055
  if (normalization_rules === undefined &&
1015
1056
  signal_guidance === undefined &&
@@ -1017,7 +1058,7 @@ export function registerPersonaTools(server, client) {
1017
1058
  context === undefined) {
1018
1059
  return toolError("Provide at least one field to update.");
1019
1060
  }
1020
- const current = await fetchPersona(id, client ?? getClient());
1061
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
1021
1062
  const body = {};
1022
1063
  if (normalization_rules !== undefined)
1023
1064
  body.normalizationRules = normalization_rules;
@@ -1027,7 +1068,7 @@ export function registerPersonaTools(server, client) {
1027
1068
  body.toneInstructions = tone_instructions;
1028
1069
  if (context !== undefined)
1029
1070
  body.context = context;
1030
- const data = await postPersonaUpdate(id, current.name, body, client ?? getClient());
1071
+ const data = await postPersonaUpdate(current, body, organization_id, expected_updated_at, client ?? getClient());
1031
1072
  return toolResult(data);
1032
1073
  }
1033
1074
  catch (error) {
@@ -1040,6 +1081,7 @@ export function registerPersonaTools(server, client) {
1040
1081
  "decisionDrivers[]}. Reads current icpStrategy, shallow-merges the keys you " +
1041
1082
  "pass, optionally removes specified keys. Arrays (mainPains, keyObjections, " +
1042
1083
  "decisionDrivers) are REPLACED wholesale when passed.", {
1084
+ ...optimisticPersonaMutationShape,
1043
1085
  id: z.string().describe("Persona ID."),
1044
1086
  dealType: z
1045
1087
  .string()
@@ -1069,7 +1111,7 @@ export function registerPersonaTools(server, client) {
1069
1111
  .array(z.string())
1070
1112
  .optional()
1071
1113
  .describe("Keys to delete from icpStrategy."),
1072
- }, async ({ id, dealType, dreamOutcome, mainPains, keyObjections, decisionDrivers, patch, remove_keys, }) => {
1114
+ }, async ({ organization_id, expected_updated_at, id, dealType, dreamOutcome, mainPains, keyObjections, decisionDrivers, patch, remove_keys, }) => {
1073
1115
  try {
1074
1116
  if (dealType === undefined &&
1075
1117
  dreamOutcome === undefined &&
@@ -1080,7 +1122,7 @@ export function registerPersonaTools(server, client) {
1080
1122
  (remove_keys === undefined || remove_keys.length === 0)) {
1081
1123
  return toolError("Provide at least one field to update.");
1082
1124
  }
1083
- const current = await fetchPersona(id, client ?? getClient());
1125
+ const current = await fetchPersona(id, organization_id, client ?? getClient());
1084
1126
  const next = { ...asObject(current.icpStrategy) };
1085
1127
  if (dealType !== undefined)
1086
1128
  next.dealType = dealType;
@@ -1096,7 +1138,7 @@ export function registerPersonaTools(server, client) {
1096
1138
  Object.assign(next, patch);
1097
1139
  for (const k of remove_keys ?? [])
1098
1140
  delete next[k];
1099
- const data = await postPersonaUpdate(id, current.name, { icpStrategy: next }, client ?? getClient());
1141
+ const data = await postPersonaUpdate(current, { icpStrategy: next }, organization_id, expected_updated_at, client ?? getClient());
1100
1142
  return toolResult(data);
1101
1143
  }
1102
1144
  catch (error) {
@@ -1,2 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerPipelineTools(server: McpServer): void;
2
+ import { type LeadifyClient } from "../client.js";
3
+ type PipelineClient = Pick<LeadifyClient, "get" | "post">;
4
+ export declare function registerPipelineTools(server: McpServer, client?: PipelineClient): void;
5
+ export {};