@agifyai/leadify-mcp 8.5.5 → 8.6.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.
@@ -1,15 +1,43 @@
1
1
  import { z } from "zod";
2
2
  import { getClient } from "../client.js";
3
3
  import { handleToolError, toolResult } from "../types.js";
4
- const CONTEXT_KINDS = ["company_brain", "gtm_playbook"];
4
+ const CONTEXT_KIND = "company_brain";
5
5
  const MAX_READINESS_ISSUES = 20;
6
- const kindSchema = z.enum(CONTEXT_KINDS);
7
- const sectionSchema = z.object({
8
- value: z.unknown(),
6
+ const ACTIVE_CONTEXT_SECTIONS = [
7
+ "identity",
8
+ "positioning",
9
+ "allowedVocabulary",
10
+ "prohibitedVocabulary",
11
+ "legalConstraints",
12
+ ];
13
+ const shortText = z.string().trim().max(500);
14
+ const longText = z.string().trim().max(20_000);
15
+ const section = (value) => z.object({
16
+ value,
9
17
  evidenceIds: z.array(z.string().min(1)).max(100),
10
18
  approved: z.boolean(),
11
19
  }).strict();
12
- const sectionPatchSchema = z.record(z.string().min(1), sectionSchema).refine(sections => Object.keys(sections).length > 0, "At least one section is required");
20
+ const sectionPatchSchema = z.object({
21
+ identity: section(z.object({
22
+ companyName: shortText.optional(),
23
+ website: shortText.optional(),
24
+ industry: shortText.optional(),
25
+ summary: longText.optional(),
26
+ }).strict()).optional(),
27
+ positioning: section(z.object({
28
+ statement: longText.optional(),
29
+ elevatorPitch: longText.optional(),
30
+ geographies: z.array(shortText).max(50).optional(),
31
+ }).strict()).optional(),
32
+ allowedVocabulary: section(z.array(shortText).max(100)).optional(),
33
+ prohibitedVocabulary: section(z.array(shortText).max(100)).optional(),
34
+ legalConstraints: section(z.array(z.object({
35
+ id: z.string().trim().min(1).max(100),
36
+ title: shortText,
37
+ description: longText.optional(),
38
+ }).strict()).max(50)).optional(),
39
+ }).strict()
40
+ .refine((sections) => Object.keys(sections).length > 0, "At least one active section is required");
13
41
  function compactReadiness(readiness) {
14
42
  if (!readiness || typeof readiness !== "object" || Array.isArray(readiness))
15
43
  return readiness;
@@ -25,6 +53,12 @@ function compactReadiness(readiness) {
25
53
  : {}),
26
54
  };
27
55
  }
56
+ function activeContent(content) {
57
+ const source = record(content);
58
+ return Object.fromEntries(ACTIVE_CONTEXT_SECTIONS
59
+ .filter(section => Object.prototype.hasOwnProperty.call(source, section))
60
+ .map(section => [section, source[section]]));
61
+ }
28
62
  function compactDraft(draft) {
29
63
  if (!draft || typeof draft !== "object" || Array.isArray(draft))
30
64
  return draft;
@@ -34,7 +68,7 @@ function compactDraft(draft) {
34
68
  base_version: source.baseVersion,
35
69
  rollback_from_version: source.rollbackFromVersion,
36
70
  readiness: compactReadiness(source.readiness),
37
- content: source.content,
71
+ content: activeContent(source.content),
38
72
  created_at: source.createdAt,
39
73
  updated_at: source.updatedAt,
40
74
  };
@@ -47,7 +81,7 @@ function compactPublished(published) {
47
81
  version: source.version,
48
82
  source_draft_id: source.sourceDraftId,
49
83
  readiness: compactReadiness(source.readiness),
50
- content: source.content,
84
+ content: activeContent(source.content),
51
85
  published_at: source.publishedAt,
52
86
  };
53
87
  }
@@ -57,10 +91,19 @@ export function projectContextWorkspaceRead(data) {
57
91
  ? data
58
92
  : {};
59
93
  const published = Array.isArray(source.published) ? source.published : [];
94
+ if (source.kind !== undefined && source.kind !== CONTEXT_KIND) {
95
+ throw new Error("Only company_brain remains exposed by the MCP Context Workspace.");
96
+ }
97
+ const latestPublished = published.reduce((latest, candidate) => {
98
+ const current = record(candidate);
99
+ const currentVersion = typeof current.version === "number" ? current.version : -1;
100
+ const latestVersion = typeof latest?.version === "number" ? latest.version : -1;
101
+ return currentVersion > latestVersion ? current : latest;
102
+ }, null);
60
103
  return {
61
- kind: source.kind,
104
+ kind: CONTEXT_KIND,
62
105
  draft: compactDraft(source.draft),
63
- latest_published: compactPublished(published[0] ?? null),
106
+ latest_published: compactPublished(latestPublished),
64
107
  published_version_count: published.length,
65
108
  ...(published.length > 1 ? { historical_versions_omitted: published.length - 1 } : {}),
66
109
  };
@@ -82,20 +125,34 @@ function mergeSections(read, sections) {
82
125
  const published = Array.isArray(state.published) ? state.published : [];
83
126
  // Start a new draft from the latest published document, then change only the
84
127
  // named sections. This preserves every untouched section across revisions.
85
- const base = Object.keys(draft).length > 0
128
+ const latestPublished = published.reduce((latest, candidate) => {
129
+ const current = record(candidate);
130
+ const currentVersion = typeof current.version === "number" ? current.version : -1;
131
+ const latestVersion = typeof latest?.version === "number" ? latest.version : -1;
132
+ return currentVersion > latestVersion ? current : latest;
133
+ }, null);
134
+ const rawBase = Object.keys(draft).length > 0
86
135
  ? record(draft.content)
87
- : record(record(published[0]).content);
88
- return { ...base, ...sections };
136
+ : record(latestPublished?.content);
137
+ return { ...activeContent(rawBase), ...sections };
138
+ }
139
+ function assertActiveSections(sections) {
140
+ const allowed = new Set(ACTIVE_CONTEXT_SECTIONS);
141
+ const frozen = Object.keys(sections).filter(section => !allowed.has(section));
142
+ if (frozen.length > 0) {
143
+ throw new Error(`PRD-1415 strict ownership: company_brain.${frozen.sort().join(", company_brain.")} is historical and cannot be edited.`);
144
+ }
89
145
  }
90
146
  async function updateSections(client, input) {
91
- const read = await client.get(`/api/context-workspace/${input.kind}`, new URLSearchParams({ organizationId: input.organizationId }));
147
+ assertActiveSections(input.sections);
148
+ const read = await client.get(`/api/context-workspace/${CONTEXT_KIND}`, new URLSearchParams({ organizationId: input.organizationId }));
92
149
  const actualRevision = currentRevision(record(read).draft);
93
150
  if (actualRevision !== input.expectedRevision) {
94
151
  throw new Error(`Revision conflict before write: expected ${input.expectedRevision}, current ${actualRevision}. Re-read and retry without overwriting concurrent sections.`);
95
152
  }
96
153
  return client.put("/api/context-workspace/draft", {
97
154
  organizationId: input.organizationId,
98
- kind: input.kind,
155
+ kind: CONTEXT_KIND,
99
156
  content: mergeSections(read, input.sections),
100
157
  expectedRevision: input.expectedRevision,
101
158
  idempotencyKey: input.idempotencyKey,
@@ -135,50 +192,49 @@ export function registerContextWorkspaceTools(server, client = getClient()) {
135
192
  });
136
193
  server.registerTool("get_context_workspace", {
137
194
  title: "Read canonical versioned Context Workspace",
138
- description: "Read the canonical Leadify Context Workspace for one explicitly selected organization and kind. " +
139
- "Returns the current draft plus the latest published Company Brain or GTM Playbook, with bounded readiness metadata. " +
140
- "Use this for every Leadify workflow. The Data Room is no longer exposed by this MCP.",
195
+ description: "Read the versioned Company Brain for one explicitly selected organization. " +
196
+ "Returns the current draft plus the latest published version, with bounded readiness metadata. " +
197
+ "Verticals, Offers and Personas use their dedicated non-versioned tools; historical GTM Playbook rows are not exposed.",
141
198
  inputSchema: {
142
199
  organization_id: z.string().trim().min(1).describe("Required organization ID. Always select it explicitly; there is no default tenant."),
143
- kind: kindSchema.describe("Canonical workspace document: `company_brain` or `gtm_playbook`."),
144
200
  },
145
201
  annotations: { readOnlyHint: true },
146
- }, async ({ organization_id, kind }) => {
202
+ }, async ({ organization_id }) => {
147
203
  try {
148
204
  const params = new URLSearchParams({ organizationId: organization_id });
149
- const data = await client.get(`/api/context-workspace/${kind}`, params);
205
+ const data = await client.get(`/api/context-workspace/${CONTEXT_KIND}`, params);
150
206
  return toolResult(projectContextWorkspaceRead(data));
151
207
  }
152
208
  catch (error) {
153
209
  return handleToolError(error);
154
210
  }
155
211
  });
156
- const sectionTools = [
157
- { name: "update_company_brain_sections", kind: "company_brain", title: "Update Company Brain sections" },
158
- { name: "update_gtm_playbook_sections", kind: "gtm_playbook", title: "Update GTM Playbook sections" },
159
- ];
160
- for (const tool of sectionTools) {
161
- server.registerTool(tool.name, {
162
- title: tool.title,
163
- description: "Update only the named canonical sections. The MCP re-reads the current draft (or latest published version), merges only those sections, " +
164
- "and saves with optimistic concurrency. Unnamed sections are preserved; a concurrent revision is never overwritten.",
165
- inputSchema: {
166
- organization_id: z.string().trim().min(1).describe("Required organization ID; section writes require admin access."),
167
- sections: sectionPatchSchema.describe("Only sections to change. Each section must include `value`, `evidenceIds`, and `approved`."),
168
- expected_revision: z.number().int().nonnegative().describe("Current draft revision, or 0 when no draft exists. Re-read first."),
169
- idempotency_key: z.string().trim().min(1).max(200).describe("Stable unique key for this exact section patch."),
170
- },
171
- annotations: { idempotentHint: true },
172
- }, async ({ organization_id, sections, expected_revision, idempotency_key }) => {
173
- try {
174
- const data = await updateSections(client, { organizationId: organization_id, kind: tool.kind, sections, expectedRevision: expected_revision, idempotencyKey: idempotency_key });
175
- return toolResult(compactDraft(data));
176
- }
177
- catch (error) {
178
- return handleToolError(error);
179
- }
180
- });
181
- }
212
+ server.registerTool("update_company_brain_sections", {
213
+ title: "Update Company Brain sections",
214
+ description: "Update only the named Company Brain sections. The MCP re-reads the current draft (or latest published version), merges only those sections, " +
215
+ `and saves with optimistic concurrency. Allowed sections: ${ACTIVE_CONTEXT_SECTIONS.join(", ")}. ` +
216
+ "Historical fields moved to Vertical, Offer, Persona or Lead Group are rejected by the closed schema.",
217
+ inputSchema: {
218
+ organization_id: z.string().trim().min(1).describe("Required organization ID; section writes require admin access."),
219
+ sections: sectionPatchSchema.describe("Only active Company Brain sections to change. Each section includes value, evidenceIds, and approved."),
220
+ expected_revision: z.number().int().nonnegative().describe("Current draft revision, or 0 when no draft exists. Re-read first."),
221
+ idempotency_key: z.string().trim().min(1).max(200).describe("Stable unique key for this exact section patch."),
222
+ },
223
+ annotations: { idempotentHint: true },
224
+ }, async ({ organization_id, sections, expected_revision, idempotency_key }) => {
225
+ try {
226
+ const data = await updateSections(client, {
227
+ organizationId: organization_id,
228
+ sections,
229
+ expectedRevision: expected_revision,
230
+ idempotencyKey: idempotency_key,
231
+ });
232
+ return toolResult(compactDraft(data));
233
+ }
234
+ catch (error) {
235
+ return handleToolError(error);
236
+ }
237
+ });
182
238
  server.registerTool("publish_context_workspace", {
183
239
  title: "Publish a ready Context Workspace draft",
184
240
  description: "Publish exactly one ready draft revision of the canonical Context Workspace. This is a consequential action: it requires " +
@@ -186,17 +242,16 @@ export function registerContextWorkspaceTools(server, client = getClient()) {
186
242
  "content with its readiness reasons and rejects stale revisions without overwriting anything.",
187
243
  inputSchema: {
188
244
  organization_id: z.string().trim().min(1).describe("Required organization ID; publication is restricted to an admin key for this tenant."),
189
- kind: kindSchema,
190
245
  expected_revision: z.number().int().positive().describe("Exact ready draft revision to publish, obtained from `get_context_workspace`."),
191
246
  idempotency_key: z.string().trim().min(1).max(200).describe("Stable unique key for this exact publication request."),
192
247
  confirm_publish: z.literal(true).describe("Must be explicitly true after the user has confirmed this publication."),
193
248
  },
194
249
  annotations: { destructiveHint: true, idempotentHint: true },
195
- }, async ({ organization_id, kind, expected_revision, idempotency_key }) => {
250
+ }, async ({ organization_id, expected_revision, idempotency_key }) => {
196
251
  try {
197
252
  const data = await client.post("/api/context-workspace/publish", {
198
253
  organizationId: organization_id,
199
- kind,
254
+ kind: CONTEXT_KIND,
200
255
  expectedRevision: expected_revision,
201
256
  idempotencyKey: idempotency_key,
202
257
  });
@@ -19,13 +19,14 @@ const role = z.enum([
19
19
  "OTHER",
20
20
  ]);
21
21
  const status = z.enum(["UNVERIFIED", "VERIFIED", "DISPUTED", "REFUTED"]);
22
- const confidence = z.enum(["LOW", "MEDIUM", "HIGH"]);
23
22
  const nullableLabel = z.string().trim().min(1).max(500).nullable().optional();
23
+ const nullableNarrative = z.string().trim().min(1).max(5_000).nullable().optional();
24
24
  const nullableUrl = z.string().trim().url().max(2048).nullable().optional();
25
25
  const idempotencyKey = z.string().trim().min(1).max(255);
26
26
  const eventFields = {
27
27
  name: z.string().trim().min(1).max(300),
28
28
  edition: z.string().trim().min(1).max(200),
29
+ description: nullableNarrative.describe("Readable description of the event, its audience, purpose and importance."),
29
30
  starts_on: dateOnly,
30
31
  ends_on: dateOnly,
31
32
  city: nullableLabel,
@@ -50,6 +51,7 @@ const updateEventShape = {
50
51
  event_id: z.string().trim().min(1),
51
52
  name: eventFields.name.optional(),
52
53
  edition: eventFields.edition.optional(),
54
+ description: eventFields.description,
53
55
  starts_on: eventFields.starts_on.optional(),
54
56
  ends_on: eventFields.ends_on.optional(),
55
57
  city: eventFields.city,
@@ -62,7 +64,7 @@ const updateEventShape = {
62
64
  idempotency_key: idempotencyKey,
63
65
  };
64
66
  const updateEventInput = z.object(updateEventShape).strict().superRefine((value, context) => {
65
- const mutable = ["name", "edition", "starts_on", "ends_on", "city", "country_code", "venue", "official_url", "last_verified_on"];
67
+ const mutable = ["name", "edition", "description", "starts_on", "ends_on", "city", "country_code", "venue", "official_url", "last_verified_on"];
66
68
  const supplied = mutable.filter((key) => value[key] !== undefined);
67
69
  if (value.archive && supplied.length > 0) {
68
70
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["archive"], message: "Archive and field updates must be separate idempotent operations." });
@@ -83,10 +85,10 @@ const participationItem = z.object({
83
85
  stands: z.array(z.string().trim().min(1).max(200)).max(50).optional().default([]),
84
86
  source_urls: z.array(z.string().trim().url().max(2048)).max(50).optional().default([]),
85
87
  source_provider: nullableLabel,
88
+ activity_summary: nullableNarrative.describe("Readable summary of what the company does, presents or targets at this event."),
86
89
  proof_excerpt: z.string().trim().min(1).max(20_000),
87
90
  observed_on: dateOnly,
88
91
  verified_on: dateOnly.nullable().optional(),
89
- confidence,
90
92
  status,
91
93
  correction_reason: z.string().trim().min(1).max(5_000).optional(),
92
94
  idempotency_key: idempotencyKey,
@@ -121,10 +123,10 @@ function participationPayload(item) {
121
123
  stands: item.stands,
122
124
  sourceUrls: item.source_urls,
123
125
  sourceProvider: item.source_provider,
126
+ activitySummary: item.activity_summary,
124
127
  proofExcerpt: item.proof_excerpt,
125
128
  observedOn: item.observed_on,
126
129
  verifiedOn: item.verified_on,
127
- confidence: item.confidence,
128
130
  status: item.status,
129
131
  correctionReason: item.correction_reason,
130
132
  idempotencyKey: item.idempotency_key,
@@ -208,6 +210,7 @@ export function registerEventTools(server, client = getClient()) {
208
210
  slug: input.slug,
209
211
  name: input.name,
210
212
  edition: input.edition,
213
+ description: input.description,
211
214
  startsOn: input.starts_on,
212
215
  endsOn: input.ends_on,
213
216
  city: input.city,
@@ -236,6 +239,7 @@ export function registerEventTools(server, client = getClient()) {
236
239
  organizationId: input.organization_id,
237
240
  name: input.name,
238
241
  edition: input.edition,
242
+ description: input.description,
239
243
  startsOn: input.starts_on,
240
244
  endsOn: input.ends_on,
241
245
  city: input.city,
@@ -260,13 +264,12 @@ export function registerEventTools(server, client = getClient()) {
260
264
  editions: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
261
265
  roles: z.array(role).max(20).optional(),
262
266
  statuses: z.array(status).min(1).max(10).optional().default(["VERIFIED"]),
263
- confidences: z.array(confidence).max(10).optional(),
264
267
  starts_on_from: dateOnly.optional(),
265
268
  starts_on_to: dateOnly.optional(),
266
269
  include_facts: z.boolean().optional().default(false),
267
270
  limit: z.number().int().min(1).max(100).optional().default(50),
268
271
  cursor: z.string().trim().min(1).optional(),
269
- }, async ({ organization_id, event_ids, account_ids, lead_id, editions, roles, statuses, confidences, starts_on_from, starts_on_to, include_facts, limit, cursor }) => {
272
+ }, async ({ organization_id, event_ids, account_ids, lead_id, editions, roles, statuses, starts_on_from, starts_on_to, include_facts, limit, cursor }) => {
270
273
  try {
271
274
  const params = new URLSearchParams({
272
275
  organizationId: organization_id,
@@ -280,7 +283,6 @@ export function registerEventTools(server, client = getClient()) {
280
283
  params.set("leadId", lead_id);
281
284
  setCsv(params, "editions", editions);
282
285
  setCsv(params, "roles", roles);
283
- setCsv(params, "confidences", confidences);
284
286
  if (starts_on_from)
285
287
  params.set("startsOnFrom", starts_on_from);
286
288
  if (starts_on_to)
@@ -318,7 +320,7 @@ export function registerEventTools(server, client = getClient()) {
318
320
  return handleToolError(error);
319
321
  }
320
322
  });
321
- server.tool("record_event_participations", "Validate or record 1-100 company-event participation observations. Set validate_only=true for a no-write dry-run. Each item targets exactly one account_id or resolvable lead_id and has its own idempotency key. Corrections append an immutable fact and require correction_reason; there is no delete operation. VERIFIED requires a source URL, proof excerpt and verification date. Uncertain or absent data cannot be cited as attendance.", {
323
+ server.tool("record_event_participations", "Create participation evidence, add a corroborating source, or correct 1-100 company-event observations. Set validate_only=true for a no-write dry-run. activity_summary is the readable account of what the company does at the event; proof_excerpt remains the exact agent/audit evidence. Each item targets exactly one account_id or resolvable lead_id and has its own idempotency key. Corrections append an immutable fact and require correction_reason; there is no delete operation. VERIFIED requires a source URL, proof excerpt and verification date. Uncertain or absent data cannot be cited as attendance.", {
322
324
  organization_id: z.string().trim().min(1),
323
325
  batch_id: z.string().trim().min(1).max(255),
324
326
  validate_only: z.boolean().optional().default(false),
@@ -10,7 +10,6 @@ const eventFilterSchema = z.object({
10
10
  "CONTRIBUTOR", "PARTICIPANT", "UNKNOWN", "OTHER",
11
11
  ])).max(20).optional(),
12
12
  statuses: z.array(z.enum(["UNVERIFIED", "VERIFIED", "DISPUTED", "REFUTED"])).min(1).max(10).optional(),
13
- confidences: z.array(z.enum(["LOW", "MEDIUM", "HIGH"])).max(10).optional(),
14
13
  starts_on_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
15
14
  starts_on_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
16
15
  }).strict().superRefine((value, context) => {
@@ -193,7 +192,7 @@ export function registerLeadTools(server, client = getClient()) {
193
192
  "operator to value. Operators: equals, not_equals, contains, not_contains, " +
194
193
  "gte, lte, is_true, is_false, is_null, is_not_null. " +
195
194
  'Example: {"industry": {"contains": "tech"}, "company": {"is_not_null": "true"}}'),
196
- event_filter: eventFilterSchema.optional().describe("Structured company-event participation filter. Values within one list use OR; event, edition, role, status, confidence and date dimensions use AND against the same participation. statuses defaults to [VERIFIED]. Pass uncertain statuses explicitly for review. There is no negative or absence filter."),
195
+ event_filter: eventFilterSchema.optional().describe("Structured company-event participation filter. Values within one list use OR; event, edition, role, status and date dimensions use AND against the same participation. statuses defaults to [VERIFIED]. Pass uncertain statuses explicitly for review. There is no confidence score, negative operator or absence filter."),
197
196
  }, async ({ group_id, view_id, page, limit, search, fields, include_schema, filters, event_filter }) => {
198
197
  try {
199
198
  const params = new URLSearchParams();
@@ -220,7 +219,6 @@ export function registerLeadTools(server, client = getClient()) {
220
219
  ...(checked.editions ? { editions: checked.editions } : {}),
221
220
  ...(checked.roles ? { roles: checked.roles } : {}),
222
221
  statuses: checked.statuses ?? ["VERIFIED"],
223
- ...(checked.confidences ? { confidences: checked.confidences } : {}),
224
222
  ...(checked.starts_on_from ? { startsOnFrom: checked.starts_on_from } : {}),
225
223
  ...(checked.starts_on_to ? { startsOnTo: checked.starts_on_to } : {}),
226
224
  }));