@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.
@@ -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
  });