@agifyai/leadify-mcp 8.5.5 → 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.
- package/README.md +22 -19
- package/dist/server.js +2 -0
- package/dist/tools/campaigns.js +3 -8
- package/dist/tools/context_entities.d.ts +668 -0
- package/dist/tools/context_entities.js +528 -0
- package/dist/tools/context_workspace.js +104 -49
- package/dist/tools/personas.js +142 -100
- package/dist/tools/pipeline.d.ts +4 -1
- package/dist/tools/pipeline.js +26 -35
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/tools/dataroom.d.ts +0 -2
- package/dist/tools/dataroom.js +0 -889
package/dist/tools/dataroom.js
DELETED
|
@@ -1,889 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { getClient } from "../client.js";
|
|
3
|
-
import { toolResult, toolError, handleToolError } from "../types.js";
|
|
4
|
-
// ─── Mirror of the backend companyInfo v2 schema ───────────────────────────
|
|
5
|
-
//
|
|
6
|
-
// SOURCE OF TRUTH:
|
|
7
|
-
// github.com/AgifyAI/leadify → apps/server/src/lib/data-room-schemas.ts
|
|
8
|
-
//
|
|
9
|
-
// Keep in sync. The backend validates every PUT against the full v2 schema
|
|
10
|
-
// and REPLACES companyInfo wholesale (no server-side merge). Every granular
|
|
11
|
-
// patch tool below therefore fetches the current companyInfo, mutates the
|
|
12
|
-
// target block, and PUTs the FULL merged object back.
|
|
13
|
-
//
|
|
14
|
-
// The backend now returns ALL zod issues at once under `issues[]` in the
|
|
15
|
-
// 422 body (see formatZodError in apps/server/src/lib/persona-schemas.ts).
|
|
16
|
-
// handleToolError in ../types.ts surfaces that array in the tool error.
|
|
17
|
-
const COMPANY_INFO_SCHEMA_VERSION = 2;
|
|
18
|
-
const STAGES = ["early", "growth", "scaling", "enterprise"];
|
|
19
|
-
const PRODUCT_CATEGORIES = ["saas", "medical_device", "service", "mixed"];
|
|
20
|
-
const CE_CLASSES = ["I", "IIa", "IIb", "III"];
|
|
21
|
-
const REIMBURSEMENT_STATUSES = ["none", "in_progress", "local", "national"];
|
|
22
|
-
const PRICING_MODELS = ["saas_per_site", "saas_per_exam", "capex", "service", "mixed"];
|
|
23
|
-
const CURRENCIES = ["EUR", "USD"];
|
|
24
|
-
const CONSTRAINT_TYPES = ["regulatory", "brand", "legal"];
|
|
25
|
-
const GEO_REGIONS = [
|
|
26
|
-
"eu_western_fr",
|
|
27
|
-
"eu_western_others",
|
|
28
|
-
"eu_nordics",
|
|
29
|
-
"eu_central_eastern",
|
|
30
|
-
"north_america",
|
|
31
|
-
"apac",
|
|
32
|
-
"latam",
|
|
33
|
-
"mena_africa",
|
|
34
|
-
];
|
|
35
|
-
const CHAR_LIMITS = {
|
|
36
|
-
name: 200,
|
|
37
|
-
website: 500,
|
|
38
|
-
pitchOneLiner: 200,
|
|
39
|
-
marketSpecialty: 100,
|
|
40
|
-
constraintLabel: 150,
|
|
41
|
-
constraintNote: 150,
|
|
42
|
-
topUseCase: 150,
|
|
43
|
-
differentiator: 150,
|
|
44
|
-
notFor: 200,
|
|
45
|
-
triggerItem: 150,
|
|
46
|
-
defaultBaseline: 200,
|
|
47
|
-
keyMetric: 150,
|
|
48
|
-
miniStory: 300,
|
|
49
|
-
productName: 100,
|
|
50
|
-
categoryDetail: 150,
|
|
51
|
-
outcomeUser: 200,
|
|
52
|
-
outcomeBuyer: 200,
|
|
53
|
-
establishmentType: 150,
|
|
54
|
-
technicalPrereq: 200,
|
|
55
|
-
championRole: 150,
|
|
56
|
-
budgetDeciderRole: 150,
|
|
57
|
-
coDeciderRole: 100,
|
|
58
|
-
forbiddenWord: 100,
|
|
59
|
-
};
|
|
60
|
-
const PRODUCT_HARD_LIMIT = 5;
|
|
61
|
-
// ─── Sub-schemas ───────────────────────────────────────────────────────────
|
|
62
|
-
const constraintSchema = z.object({
|
|
63
|
-
label: z.string().max(CHAR_LIMITS.constraintLabel),
|
|
64
|
-
type: z.enum(CONSTRAINT_TYPES),
|
|
65
|
-
note: z.string().max(CHAR_LIMITS.constraintNote).optional(),
|
|
66
|
-
});
|
|
67
|
-
const geoSchema = z.object({
|
|
68
|
-
regions: z.array(z.enum(GEO_REGIONS)).default([]),
|
|
69
|
-
countriesIncluded: z.array(z.string().length(2)).default([]),
|
|
70
|
-
countriesExcluded: z.array(z.string().length(2)).default([]),
|
|
71
|
-
});
|
|
72
|
-
const snapshotSchema = z.object({
|
|
73
|
-
pitchOneLiner: z.string().max(CHAR_LIMITS.pitchOneLiner),
|
|
74
|
-
marketSpecialty: z.string().max(CHAR_LIMITS.marketSpecialty),
|
|
75
|
-
stage: z.enum(STAGES),
|
|
76
|
-
salesTeamSizeFR: z.number().int().min(0),
|
|
77
|
-
geo: geoSchema,
|
|
78
|
-
constraints: z.array(constraintSchema).max(10).default([]),
|
|
79
|
-
});
|
|
80
|
-
const regulatorySchema = z.object({
|
|
81
|
-
ceMarked: z.boolean(),
|
|
82
|
-
ceClass: z.enum(CE_CLASSES).nullable(),
|
|
83
|
-
reimbursementStatus: z.enum(REIMBURSEMENT_STATUSES),
|
|
84
|
-
aoRequired: z.boolean(),
|
|
85
|
-
});
|
|
86
|
-
const productSchema = z.object({
|
|
87
|
-
name: z.string().max(CHAR_LIMITS.productName),
|
|
88
|
-
category: z.enum(PRODUCT_CATEGORIES),
|
|
89
|
-
categoryDetail: z.string().max(CHAR_LIMITS.categoryDetail).optional(),
|
|
90
|
-
outcomeUser: z.string().max(CHAR_LIMITS.outcomeUser),
|
|
91
|
-
outcomeBuyer: z.string().max(CHAR_LIMITS.outcomeBuyer),
|
|
92
|
-
topUseCases: z.array(z.string().max(CHAR_LIMITS.topUseCase)).max(3).default([]),
|
|
93
|
-
differentiators: z.array(z.string().max(CHAR_LIMITS.differentiator)).max(3).default([]),
|
|
94
|
-
notFor: z.string().max(CHAR_LIMITS.notFor),
|
|
95
|
-
regulatory: regulatorySchema,
|
|
96
|
-
});
|
|
97
|
-
const economicsSchema = z
|
|
98
|
-
.object({
|
|
99
|
-
pricingModel: z.enum(PRICING_MODELS),
|
|
100
|
-
ticketRange: z
|
|
101
|
-
.object({
|
|
102
|
-
min: z.number().min(0),
|
|
103
|
-
max: z.number().min(0),
|
|
104
|
-
currency: z.enum(CURRENCIES),
|
|
105
|
-
})
|
|
106
|
-
.refine((r) => r.min <= r.max, {
|
|
107
|
-
message: "ticketRange.min must be ≤ ticketRange.max",
|
|
108
|
-
path: ["min"],
|
|
109
|
-
}),
|
|
110
|
-
salesCycleMonths: z
|
|
111
|
-
.object({
|
|
112
|
-
min: z.number().int().min(0),
|
|
113
|
-
max: z.number().int().min(0),
|
|
114
|
-
})
|
|
115
|
-
.refine((r) => r.min <= r.max, {
|
|
116
|
-
message: "salesCycleMonths.min must be ≤ salesCycleMonths.max",
|
|
117
|
-
path: ["min"],
|
|
118
|
-
}),
|
|
119
|
-
triggers: z.array(z.string().max(CHAR_LIMITS.triggerItem)).max(5).default([]),
|
|
120
|
-
defaultBaseline: z.string().max(CHAR_LIMITS.defaultBaseline),
|
|
121
|
-
});
|
|
122
|
-
const productICPSchema = z.object({
|
|
123
|
-
productId: z.string().max(CHAR_LIMITS.productName),
|
|
124
|
-
establishmentType: z.string().max(CHAR_LIMITS.establishmentType),
|
|
125
|
-
technicalPrereqs: z.string().max(CHAR_LIMITS.technicalPrereq),
|
|
126
|
-
roles: z.object({
|
|
127
|
-
champion: z.string().max(CHAR_LIMITS.championRole),
|
|
128
|
-
budgetDecider: z.string().max(CHAR_LIMITS.budgetDeciderRole),
|
|
129
|
-
coDeciders: z.array(z.string().max(CHAR_LIMITS.coDeciderRole)).max(5).default([]),
|
|
130
|
-
}),
|
|
131
|
-
});
|
|
132
|
-
const proofWordingSchema = z.object({
|
|
133
|
-
keyMetrics: z.array(z.string().max(CHAR_LIMITS.keyMetric)).max(3).default([]),
|
|
134
|
-
miniStories: z.array(z.string().max(CHAR_LIMITS.miniStory)).max(2).default([]),
|
|
135
|
-
forbiddenWords: z.array(z.string().max(CHAR_LIMITS.forbiddenWord)).default([]),
|
|
136
|
-
});
|
|
137
|
-
const companyInfoV2Schema = z
|
|
138
|
-
.object({
|
|
139
|
-
schemaVersion: z.literal(COMPANY_INFO_SCHEMA_VERSION),
|
|
140
|
-
name: z.string().max(CHAR_LIMITS.name),
|
|
141
|
-
website: z.string().max(CHAR_LIMITS.website).optional(),
|
|
142
|
-
snapshot: snapshotSchema,
|
|
143
|
-
products: z.array(productSchema).max(PRODUCT_HARD_LIMIT).default([]),
|
|
144
|
-
economics: economicsSchema,
|
|
145
|
-
productICPs: z.array(productICPSchema).max(PRODUCT_HARD_LIMIT).default([]),
|
|
146
|
-
proofWording: proofWordingSchema,
|
|
147
|
-
})
|
|
148
|
-
.refine((data) => {
|
|
149
|
-
const productNames = new Set(data.products.map((p) => p.name).filter((n) => n.length > 0));
|
|
150
|
-
return data.productICPs.every((icp) => !icp.productId || productNames.has(icp.productId));
|
|
151
|
-
}, {
|
|
152
|
-
message: "productICPs[].productId must reference an existing products[].name",
|
|
153
|
-
path: ["productICPs"],
|
|
154
|
-
});
|
|
155
|
-
// ─── Document schema (unchanged) ───────────────────────────────────────────
|
|
156
|
-
const DOCUMENT_CATEGORIES = [
|
|
157
|
-
"PRODUCT_CATALOG",
|
|
158
|
-
"CONGRESS_LIST",
|
|
159
|
-
"TARGET_LIST",
|
|
160
|
-
"COMPANY_INFO",
|
|
161
|
-
"COMPETITOR_INFO",
|
|
162
|
-
"CAMPAIGN_BRIEF",
|
|
163
|
-
"OTHER",
|
|
164
|
-
];
|
|
165
|
-
// ─── Common descriptions ───────────────────────────────────────────────────
|
|
166
|
-
const ORG_ID_DESC = "REQUIRED Clerk organization ID targeting the org whose data room you act on. " +
|
|
167
|
-
"Resolve it from the lead group you are working on (e.g. via list_organizations) " +
|
|
168
|
-
"and pass it explicitly. NEVER omit it: there is no safe default — the API key " +
|
|
169
|
-
"spans multiple orgs and an empty value would silently hit the wrong company's " +
|
|
170
|
-
"data room (read or write). Requires admin access for writes.";
|
|
171
|
-
const DRY_RUN_DESC = "If true, validate the merged payload + return the preview WITHOUT writing. " +
|
|
172
|
-
"Use to dry-fit a change before persisting.";
|
|
173
|
-
async function fetchDataRoom(organizationId, opts) {
|
|
174
|
-
const params = new URLSearchParams();
|
|
175
|
-
if (organizationId)
|
|
176
|
-
params.set("organizationId", organizationId);
|
|
177
|
-
if (opts?.includePersonas)
|
|
178
|
-
params.set("includePersonas", "true");
|
|
179
|
-
const data = (await getClient().get("/api/data-room", params.toString() ? params : undefined));
|
|
180
|
-
return data ?? {};
|
|
181
|
-
}
|
|
182
|
-
function currentCompanyInfo(state) {
|
|
183
|
-
const ci = state.companyInfo;
|
|
184
|
-
if (!ci || typeof ci !== "object" || Array.isArray(ci))
|
|
185
|
-
return {};
|
|
186
|
-
return ci;
|
|
187
|
-
}
|
|
188
|
-
async function putCompanyInfo(companyInfo, organizationId) {
|
|
189
|
-
const body = { companyInfo };
|
|
190
|
-
if (organizationId)
|
|
191
|
-
body.organizationId = organizationId;
|
|
192
|
-
return getClient().put("/api/data-room", body);
|
|
193
|
-
}
|
|
194
|
-
// Validate MCP-side against the mirrored v2 schema and surface ALL issues
|
|
195
|
-
// at once (not just the first). Returns null if value passes, or a tool
|
|
196
|
-
// error otherwise.
|
|
197
|
-
function validateOrError(value, hint) {
|
|
198
|
-
const result = companyInfoV2Schema.safeParse(value);
|
|
199
|
-
if (result.success)
|
|
200
|
-
return null;
|
|
201
|
-
const issues = result.error.issues.map((i) => ({
|
|
202
|
-
path: i.path.map(String).join("."),
|
|
203
|
-
message: i.message,
|
|
204
|
-
code: i.code,
|
|
205
|
-
}));
|
|
206
|
-
return toolError(`MCP validation failed against companyInfo v2 schema (${issues.length} issue${issues.length === 1 ? "" : "s"}). ${hint}`, { issues });
|
|
207
|
-
}
|
|
208
|
-
// Assert that the existing companyInfo is v2-valid. Granular patch tools
|
|
209
|
-
// require a bootstrapped v2 payload to work — otherwise the merged result
|
|
210
|
-
// will fail v2 validation server-side, since the backend replaces wholesale.
|
|
211
|
-
function requireV2Bootstrap(ci) {
|
|
212
|
-
if (ci.schemaVersion !== COMPANY_INFO_SCHEMA_VERSION) {
|
|
213
|
-
return toolError(`Data room companyInfo is not v${COMPANY_INFO_SCHEMA_VERSION} (got schemaVersion=${JSON.stringify(ci.schemaVersion)}). ` +
|
|
214
|
-
"Bootstrap with update_data_room (passing a full v2 payload) before using granular patch tools. " +
|
|
215
|
-
"See describe_company_info_schema for the contract.");
|
|
216
|
-
}
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
function dryRunResult(merged) {
|
|
220
|
-
return toolResult({
|
|
221
|
-
ok: true,
|
|
222
|
-
dry_run: true,
|
|
223
|
-
preview_company_info: merged,
|
|
224
|
-
note: "No write performed. Re-call without dry_run to persist.",
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
// Run the full pipeline for a granular tool: fetch current, mutate via the
|
|
228
|
-
// caller-provided patcher, validate, dry-run or persist.
|
|
229
|
-
async function mergeAndPut(organizationId, dry_run, patcher, validationHint) {
|
|
230
|
-
const current = await fetchDataRoom(organizationId);
|
|
231
|
-
const ci = currentCompanyInfo(current);
|
|
232
|
-
const bootstrapErr = requireV2Bootstrap(ci);
|
|
233
|
-
if (bootstrapErr)
|
|
234
|
-
return bootstrapErr;
|
|
235
|
-
const merged = patcher(ci);
|
|
236
|
-
const validationErr = validateOrError(merged, validationHint);
|
|
237
|
-
if (validationErr)
|
|
238
|
-
return validationErr;
|
|
239
|
-
if (dry_run)
|
|
240
|
-
return dryRunResult(merged);
|
|
241
|
-
const data = await putCompanyInfo(merged, organizationId);
|
|
242
|
-
return toolResult(data);
|
|
243
|
-
}
|
|
244
|
-
// ─── Tool registrations ────────────────────────────────────────────────────
|
|
245
|
-
// ── get_outreach_context ─────────────────────────────────────────────────
|
|
246
|
-
// Dedicated tool for the outreach pipeline Phase 0. Single call replaces the
|
|
247
|
-
// old multi-step dance (get_data_room + org resolution + get_lead_group_persona)
|
|
248
|
-
// and eliminates the cross-org footgun by accepting lead_group_id instead of
|
|
249
|
-
// organization_id. The backend resolves the org server-side.
|
|
250
|
-
export function registerDataRoomTools(server) {
|
|
251
|
-
// ── get_data_room ──────────────────────────────────────────────────────
|
|
252
|
-
server.tool("get_data_room", "Retrieve compact data-room metadata by default. Documents are paginated and their content " +
|
|
253
|
-
"is omitted unless explicitly requested for named document IDs.", {
|
|
254
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
255
|
-
page: z.number().int().positive().optional().default(1).describe("Document page, starting at 1."),
|
|
256
|
-
limit: z.number().int().positive().max(50).optional().default(20).describe("Documents per page, max 50."),
|
|
257
|
-
category: z.string().optional().describe("Filter documents by category."),
|
|
258
|
-
search: z.string().optional().describe("Search document title and metadata."),
|
|
259
|
-
include_document_content_for_ids: z.array(z.string()).max(10).optional().describe("Explicit document IDs whose content is required."),
|
|
260
|
-
include_personas: z.boolean().optional().default(false).describe("Include personas only when explicitly required."),
|
|
261
|
-
}, async ({ organization_id, page, limit, category, search, include_document_content_for_ids, include_personas }) => {
|
|
262
|
-
try {
|
|
263
|
-
const state = await fetchDataRoom(organization_id, { includePersonas: include_personas === true });
|
|
264
|
-
const room = state.dataRoom && typeof state.dataRoom === "object" ? state.dataRoom : state;
|
|
265
|
-
const documents = Array.isArray(room.documents) ? room.documents : [];
|
|
266
|
-
const term = search?.toLowerCase();
|
|
267
|
-
const filtered = documents.filter((document) => {
|
|
268
|
-
const value = document && typeof document === "object" ? document : {};
|
|
269
|
-
if (category && value.category !== category)
|
|
270
|
-
return false;
|
|
271
|
-
return !term || JSON.stringify({ title: value.title, name: value.name, category: value.category }).toLowerCase().includes(term);
|
|
272
|
-
});
|
|
273
|
-
const start = (page - 1) * limit;
|
|
274
|
-
const requested = new Set(include_document_content_for_ids ?? []);
|
|
275
|
-
const items = filtered.slice(start, start + limit).map((document) => {
|
|
276
|
-
const value = document && typeof document === "object" ? document : {};
|
|
277
|
-
const compact = { id: value.id, title: value.title ?? value.name, category: value.category, createdAt: value.createdAt, updatedAt: value.updatedAt };
|
|
278
|
-
if (requested.has(String(value.id)))
|
|
279
|
-
compact.content = value.content;
|
|
280
|
-
return compact;
|
|
281
|
-
});
|
|
282
|
-
return toolResult({ company_info: room.companyInfo, documents: items, pagination: { page, limit, total: filtered.length, totalPages: Math.ceil(filtered.length / limit) } });
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
return handleToolError(error);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
// ── get_outreach_context ──────────────────────────────────────────────
|
|
289
|
-
server.tool("get_outreach_context", "Get a lean outreach context for a lead group. Returns the shape " +
|
|
290
|
-
"`{ leadGroupId, leadGroupName, organizationId, company }` where `company` is the " +
|
|
291
|
-
"outreach-relevant slice of companyInfo (identity, pitch, constraints, curated " +
|
|
292
|
-
"products, economics triggers/baseline, product ICP roles, proof wording). " +
|
|
293
|
-
"Takes a lead_group_id (NOT organization_id) — the org is resolved server-side, " +
|
|
294
|
-
"eliminating the cross-org footgun of get_data_room. " +
|
|
295
|
-
"Does NOT require a persona: the group needs no linked persona and an empty data " +
|
|
296
|
-
"room returns success (not an error). No persona or outreach block is returned. " +
|
|
297
|
-
"Outreach configuration (positioning, tone, rules, templates, etc.) is managed " +
|
|
298
|
-
"exclusively via the Fine Tuning tools (append_fine_tuning / set_fine_tuning / " +
|
|
299
|
-
"get_fine_tuning). " +
|
|
300
|
-
"Use this instead of get_data_room + get_lead_group_persona in the outreach " +
|
|
301
|
-
"pipeline Phase 0.", {
|
|
302
|
-
lead_group_id: z.string().describe("ID of the lead group to resolve context for."),
|
|
303
|
-
}, async ({ lead_group_id }) => {
|
|
304
|
-
try {
|
|
305
|
-
const params = new URLSearchParams();
|
|
306
|
-
params.set("lead_group_id", lead_group_id);
|
|
307
|
-
const data = await getClient().get("/api/outreach-context", params);
|
|
308
|
-
// Strip the `outreach` block — outreach config is managed via Fine Tuning only.
|
|
309
|
-
const { outreach, ...rest } = data;
|
|
310
|
-
return toolResult(rest);
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
return handleToolError(error);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
// ── describe_company_info_schema ───────────────────────────────────────
|
|
317
|
-
server.tool("describe_company_info_schema", "Return the companyInfo v2 schema mirrored at the MCP layer: all sections, enums, " +
|
|
318
|
-
"max-length caps, required fields, and which patch tool owns each section. Call this " +
|
|
319
|
-
"BEFORE the first update of a session to know what the API will accept. The mirror " +
|
|
320
|
-
"tracks apps/server/src/lib/data-room-schemas.ts in the Leadify backend.", {}, async () => {
|
|
321
|
-
try {
|
|
322
|
-
return toolResult({
|
|
323
|
-
schemaVersion: COMPANY_INFO_SCHEMA_VERSION,
|
|
324
|
-
source: "Mirror of apps/server/src/lib/data-room-schemas.ts in github.com/AgifyAI/leadify.",
|
|
325
|
-
enums: {
|
|
326
|
-
stage: STAGES,
|
|
327
|
-
product_category: PRODUCT_CATEGORIES,
|
|
328
|
-
ce_class: CE_CLASSES,
|
|
329
|
-
reimbursement_status: REIMBURSEMENT_STATUSES,
|
|
330
|
-
pricing_model: PRICING_MODELS,
|
|
331
|
-
currency: CURRENCIES,
|
|
332
|
-
constraint_type: CONSTRAINT_TYPES,
|
|
333
|
-
geo_region: GEO_REGIONS,
|
|
334
|
-
},
|
|
335
|
-
char_limits: CHAR_LIMITS,
|
|
336
|
-
hard_limits: {
|
|
337
|
-
products_max: PRODUCT_HARD_LIMIT,
|
|
338
|
-
productICPs_max: PRODUCT_HARD_LIMIT,
|
|
339
|
-
constraints_max: 10,
|
|
340
|
-
topUseCases_max_per_product: 3,
|
|
341
|
-
differentiators_max_per_product: 3,
|
|
342
|
-
triggers_max: 5,
|
|
343
|
-
coDeciders_max: 5,
|
|
344
|
-
keyMetrics_max: 3,
|
|
345
|
-
miniStories_max: 2,
|
|
346
|
-
},
|
|
347
|
-
sections: {
|
|
348
|
-
identity: {
|
|
349
|
-
fields: ["name (required, ≤200)", "website (optional, ≤500)"],
|
|
350
|
-
patch_tool: "update_company_info_identity",
|
|
351
|
-
},
|
|
352
|
-
snapshot: {
|
|
353
|
-
fields: [
|
|
354
|
-
"pitchOneLiner (required, ≤200)",
|
|
355
|
-
"marketSpecialty (required, ≤100)",
|
|
356
|
-
"stage (required, enum)",
|
|
357
|
-
"salesTeamSizeFR (required, int ≥0)",
|
|
358
|
-
"geo {regions[], countriesIncluded[2-letter codes], countriesExcluded[2-letter codes]}",
|
|
359
|
-
"constraints[] (max 10, see update_company_info_constraint)",
|
|
360
|
-
],
|
|
361
|
-
patch_tool: "update_company_info_snapshot",
|
|
362
|
-
},
|
|
363
|
-
products: {
|
|
364
|
-
note: "Up to 5 products. Each: {name ≤100, category enum, categoryDetail? ≤150, outcomeUser ≤200, outcomeBuyer ≤200, topUseCases[≤3, ≤150 each], differentiators[≤3, ≤150 each], notFor ≤200, regulatory {ceMarked, ceClass nullable enum, reimbursementStatus enum, aoRequired}}.",
|
|
365
|
-
patch_tool: "update_company_info_product",
|
|
366
|
-
},
|
|
367
|
-
economics: {
|
|
368
|
-
fields: [
|
|
369
|
-
"pricingModel (required, enum)",
|
|
370
|
-
"ticketRange {min, max, currency} (min ≤ max)",
|
|
371
|
-
"salesCycleMonths {min, max} (min ≤ max)",
|
|
372
|
-
"triggers[] (max 5, each ≤150)",
|
|
373
|
-
"defaultBaseline (required, ≤200)",
|
|
374
|
-
],
|
|
375
|
-
patch_tool: "update_company_info_economics",
|
|
376
|
-
},
|
|
377
|
-
productICPs: {
|
|
378
|
-
note: "Up to 5 ICPs. Each: {productId (must reference products[].name), establishmentType ≤150, technicalPrereqs ≤200, roles {champion ≤150, budgetDecider ≤150, coDeciders[≤5, ≤100 each]}}.",
|
|
379
|
-
patch_tool: "update_company_info_product_icp",
|
|
380
|
-
},
|
|
381
|
-
proofWording: {
|
|
382
|
-
fields: [
|
|
383
|
-
"keyMetrics[] (max 3, each ≤150)",
|
|
384
|
-
"miniStories[] (max 2, each ≤300)",
|
|
385
|
-
"forbiddenWords[] (each ≤100)",
|
|
386
|
-
],
|
|
387
|
-
patch_tool: "update_company_info_proof_wording",
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
escape_hatch: {
|
|
391
|
-
tool: "update_data_room",
|
|
392
|
-
note: "WHOLESALE replacement. Required for first-time bootstrap (no existing v2 payload).",
|
|
393
|
-
},
|
|
394
|
-
cross_field_invariants: [
|
|
395
|
-
"productICPs[].productId must equal one of products[].name (or be empty).",
|
|
396
|
-
"economics.ticketRange.min ≤ economics.ticketRange.max.",
|
|
397
|
-
"economics.salesCycleMonths.min ≤ economics.salesCycleMonths.max.",
|
|
398
|
-
],
|
|
399
|
-
error_format: {
|
|
400
|
-
shape: "{ error, path, issues: ZodIssue[] } — issues[] contains EVERY validation failure at once.",
|
|
401
|
-
note: "Backend now returns all issues simultaneously. Fix all listed paths before retrying.",
|
|
402
|
-
},
|
|
403
|
-
notes: [
|
|
404
|
-
"Backend replaces companyInfo wholesale on PUT. Granular tools fetch + merge + send the FULL v2 payload back.",
|
|
405
|
-
"Granular tools require a v2-bootstrapped data room. Call update_data_room with a full v2 payload first if schemaVersion is absent or older.",
|
|
406
|
-
],
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
return handleToolError(error);
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
// ── update_data_room ───────────────────────────────────────────────────
|
|
414
|
-
// Escape hatch: WHOLESALE replacement. The only path for first-time
|
|
415
|
-
// bootstrap (no existing v2 companyInfo). Validates against v2 before
|
|
416
|
-
// hitting the API.
|
|
417
|
-
server.tool("update_data_room", "WHOLESALE update of companyInfo (ESCAPE HATCH). REPLACES the entire companyInfo with the " +
|
|
418
|
-
"payload you pass. Required for first-time bootstrap. For incremental edits on an " +
|
|
419
|
-
"existing v2 payload, ALWAYS prefer the granular update_company_info_* tools — they " +
|
|
420
|
-
"fetch + merge + write the full v2 payload back, so they can't accidentally drop " +
|
|
421
|
-
"required sections. MCP validates against the v2 schema before sending.", {
|
|
422
|
-
company_info: z
|
|
423
|
-
.record(z.unknown())
|
|
424
|
-
.describe("Full v2 companyInfo payload (replaces the existing one). See describe_company_info_schema for the contract."),
|
|
425
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
426
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
427
|
-
}, async ({ company_info, organization_id, dry_run }) => {
|
|
428
|
-
try {
|
|
429
|
-
const validationErr = validateOrError(company_info, "Pass a complete v2 payload — see describe_company_info_schema for the required fields.");
|
|
430
|
-
if (validationErr)
|
|
431
|
-
return validationErr;
|
|
432
|
-
if (dry_run) {
|
|
433
|
-
return dryRunResult(company_info);
|
|
434
|
-
}
|
|
435
|
-
const data = await putCompanyInfo(company_info, organization_id);
|
|
436
|
-
return toolResult(data);
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
return handleToolError(error);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
// ── update_company_info_identity ───────────────────────────────────────
|
|
443
|
-
server.tool("update_company_info_identity", "Patch the top-level identity fields of companyInfo (name, website). Reads the current " +
|
|
444
|
-
"v2 payload, merges your patch, writes the full v2 payload back. Other sections " +
|
|
445
|
-
"(snapshot, products, economics, productICPs, proofWording) remain intact.", {
|
|
446
|
-
name: z
|
|
447
|
-
.string()
|
|
448
|
-
.max(CHAR_LIMITS.name)
|
|
449
|
-
.optional()
|
|
450
|
-
.describe("Company name (max 200)."),
|
|
451
|
-
website: z
|
|
452
|
-
.string()
|
|
453
|
-
.max(CHAR_LIMITS.website)
|
|
454
|
-
.optional()
|
|
455
|
-
.describe("Company website URL (max 500)."),
|
|
456
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
457
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
458
|
-
}, async ({ name, website, organization_id, dry_run }) => {
|
|
459
|
-
try {
|
|
460
|
-
if (name === undefined && website === undefined) {
|
|
461
|
-
return toolError("Provide at least one of: name, website.");
|
|
462
|
-
}
|
|
463
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
464
|
-
const out = { ...ci };
|
|
465
|
-
if (name !== undefined)
|
|
466
|
-
out.name = name;
|
|
467
|
-
if (website !== undefined)
|
|
468
|
-
out.website = website;
|
|
469
|
-
return out;
|
|
470
|
-
}, "Identity patch failed against v2 schema.");
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
return handleToolError(error);
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
// ── update_company_info_snapshot ───────────────────────────────────────
|
|
477
|
-
server.tool("update_company_info_snapshot", "Patch the snapshot block. Reads the current v2 payload, shallow-merges the snapshot " +
|
|
478
|
-
"fields you pass, writes the full v2 payload back. geo is merged shallowly — pass any " +
|
|
479
|
-
"subset of regions/countriesIncluded/countriesExcluded. constraints[] is REPLACED " +
|
|
480
|
-
"wholesale when passed — use update_company_info_constraint for per-item edits.", {
|
|
481
|
-
pitchOneLiner: z
|
|
482
|
-
.string()
|
|
483
|
-
.max(CHAR_LIMITS.pitchOneLiner)
|
|
484
|
-
.optional()
|
|
485
|
-
.describe("One-liner pitch (max 200)."),
|
|
486
|
-
marketSpecialty: z
|
|
487
|
-
.string()
|
|
488
|
-
.max(CHAR_LIMITS.marketSpecialty)
|
|
489
|
-
.optional()
|
|
490
|
-
.describe("Market specialty (max 100)."),
|
|
491
|
-
stage: z
|
|
492
|
-
.enum(STAGES)
|
|
493
|
-
.optional()
|
|
494
|
-
.describe("Company stage enum: early | growth | scaling | enterprise."),
|
|
495
|
-
salesTeamSizeFR: z
|
|
496
|
-
.number()
|
|
497
|
-
.int()
|
|
498
|
-
.min(0)
|
|
499
|
-
.optional()
|
|
500
|
-
.describe("Sales team size in FR (non-negative integer)."),
|
|
501
|
-
geo: z
|
|
502
|
-
.object({
|
|
503
|
-
regions: z.array(z.enum(GEO_REGIONS)).optional(),
|
|
504
|
-
countriesIncluded: z.array(z.string().length(2)).optional(),
|
|
505
|
-
countriesExcluded: z.array(z.string().length(2)).optional(),
|
|
506
|
-
})
|
|
507
|
-
.optional()
|
|
508
|
-
.describe("Geo block (shallow-merged). regions enum: " +
|
|
509
|
-
GEO_REGIONS.join(", ") +
|
|
510
|
-
". countries: ISO 3166-1 alpha-2 (2 letters)."),
|
|
511
|
-
constraints: z
|
|
512
|
-
.array(constraintSchema)
|
|
513
|
-
.max(10)
|
|
514
|
-
.optional()
|
|
515
|
-
.describe("Replaces constraints[] wholesale. For per-item edits use update_company_info_constraint."),
|
|
516
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
517
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
518
|
-
}, async ({ pitchOneLiner, marketSpecialty, stage, salesTeamSizeFR, geo, constraints, organization_id, dry_run, }) => {
|
|
519
|
-
try {
|
|
520
|
-
if (pitchOneLiner === undefined &&
|
|
521
|
-
marketSpecialty === undefined &&
|
|
522
|
-
stage === undefined &&
|
|
523
|
-
salesTeamSizeFR === undefined &&
|
|
524
|
-
geo === undefined &&
|
|
525
|
-
constraints === undefined) {
|
|
526
|
-
return toolError("Provide at least one of: pitchOneLiner, marketSpecialty, stage, salesTeamSizeFR, geo, constraints.");
|
|
527
|
-
}
|
|
528
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
529
|
-
const existingSnapshot = ci.snapshot && typeof ci.snapshot === "object" && !Array.isArray(ci.snapshot)
|
|
530
|
-
? ci.snapshot
|
|
531
|
-
: {};
|
|
532
|
-
const nextSnapshot = { ...existingSnapshot };
|
|
533
|
-
if (pitchOneLiner !== undefined)
|
|
534
|
-
nextSnapshot.pitchOneLiner = pitchOneLiner;
|
|
535
|
-
if (marketSpecialty !== undefined)
|
|
536
|
-
nextSnapshot.marketSpecialty = marketSpecialty;
|
|
537
|
-
if (stage !== undefined)
|
|
538
|
-
nextSnapshot.stage = stage;
|
|
539
|
-
if (salesTeamSizeFR !== undefined)
|
|
540
|
-
nextSnapshot.salesTeamSizeFR = salesTeamSizeFR;
|
|
541
|
-
if (geo !== undefined) {
|
|
542
|
-
const existingGeo = existingSnapshot.geo &&
|
|
543
|
-
typeof existingSnapshot.geo === "object" &&
|
|
544
|
-
!Array.isArray(existingSnapshot.geo)
|
|
545
|
-
? existingSnapshot.geo
|
|
546
|
-
: {};
|
|
547
|
-
nextSnapshot.geo = { ...existingGeo, ...geo };
|
|
548
|
-
}
|
|
549
|
-
if (constraints !== undefined)
|
|
550
|
-
nextSnapshot.constraints = constraints;
|
|
551
|
-
return { ...ci, snapshot: nextSnapshot };
|
|
552
|
-
}, "Snapshot patch failed against v2 schema.");
|
|
553
|
-
}
|
|
554
|
-
catch (error) {
|
|
555
|
-
return handleToolError(error);
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
// ── update_company_info_constraint ─────────────────────────────────────
|
|
559
|
-
server.tool("update_company_info_constraint", "Add, replace, or remove a single business constraint in snapshot.constraints[] without " +
|
|
560
|
-
"re-sending the whole list. Constraints have no stable id — entries are addressed by " +
|
|
561
|
-
"zero-based index. Beware: indices shift after a remove. Cap is 10 constraints. " +
|
|
562
|
-
"Each constraint: {label ≤150, type ∈ regulatory|brand|legal, note? ≤150}.", {
|
|
563
|
-
action: z
|
|
564
|
-
.enum(["add", "replace", "remove"])
|
|
565
|
-
.describe("add = append; replace = overwrite at index; remove = delete at index."),
|
|
566
|
-
index: z
|
|
567
|
-
.number()
|
|
568
|
-
.int()
|
|
569
|
-
.nonnegative()
|
|
570
|
-
.optional()
|
|
571
|
-
.describe("Zero-based index. Required for 'replace' and 'remove'."),
|
|
572
|
-
constraint: constraintSchema
|
|
573
|
-
.optional()
|
|
574
|
-
.describe("Constraint payload {label ≤150, type ∈ regulatory|brand|legal, note? ≤150}. Required for 'add' and 'replace'."),
|
|
575
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
576
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
577
|
-
}, async ({ action, index, constraint, organization_id, dry_run }) => {
|
|
578
|
-
try {
|
|
579
|
-
if ((action === "add" || action === "replace") && !constraint) {
|
|
580
|
-
return toolError(`constraint is required for action '${action}'.`);
|
|
581
|
-
}
|
|
582
|
-
if ((action === "replace" || action === "remove") && index === undefined) {
|
|
583
|
-
return toolError(`index is required for action '${action}'.`);
|
|
584
|
-
}
|
|
585
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
586
|
-
const existingSnapshot = ci.snapshot && typeof ci.snapshot === "object" && !Array.isArray(ci.snapshot)
|
|
587
|
-
? ci.snapshot
|
|
588
|
-
: {};
|
|
589
|
-
const list = Array.isArray(existingSnapshot.constraints)
|
|
590
|
-
? existingSnapshot.constraints.map((c) => ({
|
|
591
|
-
...c,
|
|
592
|
-
}))
|
|
593
|
-
: [];
|
|
594
|
-
if (action === "add") {
|
|
595
|
-
if (list.length >= 10) {
|
|
596
|
-
throw new Error("Constraints list is at cap (10). Remove one before adding.");
|
|
597
|
-
}
|
|
598
|
-
list.push(constraint);
|
|
599
|
-
}
|
|
600
|
-
else if (action === "replace") {
|
|
601
|
-
if (index < 0 || index >= list.length) {
|
|
602
|
-
throw new Error(`Index ${index} out of bounds (constraints list has ${list.length} entries).`);
|
|
603
|
-
}
|
|
604
|
-
list[index] = constraint;
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
if (index < 0 || index >= list.length) {
|
|
608
|
-
throw new Error(`Index ${index} out of bounds (constraints list has ${list.length} entries).`);
|
|
609
|
-
}
|
|
610
|
-
list.splice(index, 1);
|
|
611
|
-
}
|
|
612
|
-
return {
|
|
613
|
-
...ci,
|
|
614
|
-
snapshot: { ...existingSnapshot, constraints: list },
|
|
615
|
-
};
|
|
616
|
-
}, "Constraint patch failed against v2 schema.");
|
|
617
|
-
}
|
|
618
|
-
catch (error) {
|
|
619
|
-
return handleToolError(error);
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
// ── update_company_info_product ────────────────────────────────────────
|
|
623
|
-
server.tool("update_company_info_product", "Add, replace, or remove a single product in companyInfo.products[] without re-sending " +
|
|
624
|
-
"the whole array. Lookups by product `name` (case-sensitive exact match). Cap is 5 " +
|
|
625
|
-
"products. Each product: {name ≤100, category enum, categoryDetail? ≤150, outcomeUser " +
|
|
626
|
-
"≤200, outcomeBuyer ≤200, topUseCases[≤3, ≤150 each], differentiators[≤3, ≤150 each], " +
|
|
627
|
-
"notFor ≤200, regulatory {ceMarked, ceClass nullable, reimbursementStatus enum, aoRequired}}. " +
|
|
628
|
-
"WARNING: if a productICP.productId references this product's name, removing/renaming " +
|
|
629
|
-
"will fail the cross-field refine — fix the ICP first.", {
|
|
630
|
-
action: z
|
|
631
|
-
.enum(["add", "replace", "remove"])
|
|
632
|
-
.describe("add = append (fails if name exists or list at cap 5); replace = overwrite by name; remove = delete by name."),
|
|
633
|
-
name: z
|
|
634
|
-
.string()
|
|
635
|
-
.optional()
|
|
636
|
-
.describe("Lookup key for replace/remove. Required for those actions. For 'add' the name comes from the product payload."),
|
|
637
|
-
product: productSchema
|
|
638
|
-
.optional()
|
|
639
|
-
.describe("Product payload. Required for 'add' and 'replace'."),
|
|
640
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
641
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
642
|
-
}, async ({ action, name, product, organization_id, dry_run }) => {
|
|
643
|
-
try {
|
|
644
|
-
if ((action === "add" || action === "replace") && !product) {
|
|
645
|
-
return toolError(`product is required for action '${action}'.`);
|
|
646
|
-
}
|
|
647
|
-
if ((action === "replace" || action === "remove") && !name) {
|
|
648
|
-
return toolError(`name is required for action '${action}'.`);
|
|
649
|
-
}
|
|
650
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
651
|
-
const list = Array.isArray(ci.products)
|
|
652
|
-
? ci.products.map((p) => ({ ...p }))
|
|
653
|
-
: [];
|
|
654
|
-
if (action === "add") {
|
|
655
|
-
const newName = product.name;
|
|
656
|
-
if (list.some((p) => p.name === newName)) {
|
|
657
|
-
throw new Error(`A product with name "${newName}" already exists. Use action 'replace' instead.`);
|
|
658
|
-
}
|
|
659
|
-
if (list.length >= PRODUCT_HARD_LIMIT) {
|
|
660
|
-
throw new Error(`Products list is at cap (${PRODUCT_HARD_LIMIT}). Remove one before adding.`);
|
|
661
|
-
}
|
|
662
|
-
list.push(product);
|
|
663
|
-
}
|
|
664
|
-
else if (action === "replace") {
|
|
665
|
-
const idx = list.findIndex((p) => p.name === name);
|
|
666
|
-
if (idx === -1) {
|
|
667
|
-
throw new Error(`No product found with name "${name}".`);
|
|
668
|
-
}
|
|
669
|
-
list[idx] = product;
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
const before = list.length;
|
|
673
|
-
const filtered = list.filter((p) => p.name !== name);
|
|
674
|
-
if (filtered.length === before) {
|
|
675
|
-
throw new Error(`No product found with name "${name}".`);
|
|
676
|
-
}
|
|
677
|
-
list.length = 0;
|
|
678
|
-
list.push(...filtered);
|
|
679
|
-
}
|
|
680
|
-
return { ...ci, products: list };
|
|
681
|
-
}, "Product patch failed against v2 schema.");
|
|
682
|
-
}
|
|
683
|
-
catch (error) {
|
|
684
|
-
return handleToolError(error);
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
// ── update_company_info_economics ──────────────────────────────────────
|
|
688
|
-
server.tool("update_company_info_economics", "Patch the economics block. Reads current economics, shallow-merges your fields, writes " +
|
|
689
|
-
"back. ticketRange and salesCycleMonths are REPLACED wholesale when passed (their " +
|
|
690
|
-
"internal refine enforces min ≤ max). triggers[] is replaced wholesale.", {
|
|
691
|
-
pricingModel: z
|
|
692
|
-
.enum(PRICING_MODELS)
|
|
693
|
-
.optional()
|
|
694
|
-
.describe("Pricing model enum: saas_per_site | saas_per_exam | capex | service | mixed."),
|
|
695
|
-
ticketRange: z
|
|
696
|
-
.object({
|
|
697
|
-
min: z.number().min(0),
|
|
698
|
-
max: z.number().min(0),
|
|
699
|
-
currency: z.enum(CURRENCIES),
|
|
700
|
-
})
|
|
701
|
-
.optional()
|
|
702
|
-
.describe("Replaces ticketRange wholesale. min ≤ max enforced. currency ∈ EUR|USD."),
|
|
703
|
-
salesCycleMonths: z
|
|
704
|
-
.object({
|
|
705
|
-
min: z.number().int().min(0),
|
|
706
|
-
max: z.number().int().min(0),
|
|
707
|
-
})
|
|
708
|
-
.optional()
|
|
709
|
-
.describe("Replaces salesCycleMonths wholesale. min ≤ max enforced."),
|
|
710
|
-
triggers: z
|
|
711
|
-
.array(z.string().max(CHAR_LIMITS.triggerItem))
|
|
712
|
-
.max(5)
|
|
713
|
-
.optional()
|
|
714
|
-
.describe("Replaces triggers[] wholesale (max 5 items, each ≤150)."),
|
|
715
|
-
defaultBaseline: z
|
|
716
|
-
.string()
|
|
717
|
-
.max(CHAR_LIMITS.defaultBaseline)
|
|
718
|
-
.optional()
|
|
719
|
-
.describe("Default baseline (max 200)."),
|
|
720
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
721
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
722
|
-
}, async ({ pricingModel, ticketRange, salesCycleMonths, triggers, defaultBaseline, organization_id, dry_run, }) => {
|
|
723
|
-
try {
|
|
724
|
-
if (pricingModel === undefined &&
|
|
725
|
-
ticketRange === undefined &&
|
|
726
|
-
salesCycleMonths === undefined &&
|
|
727
|
-
triggers === undefined &&
|
|
728
|
-
defaultBaseline === undefined) {
|
|
729
|
-
return toolError("Provide at least one of: pricingModel, ticketRange, salesCycleMonths, triggers, defaultBaseline.");
|
|
730
|
-
}
|
|
731
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
732
|
-
const existing = ci.economics && typeof ci.economics === "object" && !Array.isArray(ci.economics)
|
|
733
|
-
? ci.economics
|
|
734
|
-
: {};
|
|
735
|
-
const next = { ...existing };
|
|
736
|
-
if (pricingModel !== undefined)
|
|
737
|
-
next.pricingModel = pricingModel;
|
|
738
|
-
if (ticketRange !== undefined)
|
|
739
|
-
next.ticketRange = ticketRange;
|
|
740
|
-
if (salesCycleMonths !== undefined)
|
|
741
|
-
next.salesCycleMonths = salesCycleMonths;
|
|
742
|
-
if (triggers !== undefined)
|
|
743
|
-
next.triggers = triggers;
|
|
744
|
-
if (defaultBaseline !== undefined)
|
|
745
|
-
next.defaultBaseline = defaultBaseline;
|
|
746
|
-
return { ...ci, economics: next };
|
|
747
|
-
}, "Economics patch failed against v2 schema.");
|
|
748
|
-
}
|
|
749
|
-
catch (error) {
|
|
750
|
-
return handleToolError(error);
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
// ── update_company_info_product_icp ────────────────────────────────────
|
|
754
|
-
server.tool("update_company_info_product_icp", "Add, replace, or remove a single product ICP in companyInfo.productICPs[]. Lookups by " +
|
|
755
|
-
"`productId` (must match an existing products[].name). Cap is 5 ICPs. Each ICP: " +
|
|
756
|
-
"{productId, establishmentType ≤150, technicalPrereqs ≤200, roles {champion ≤150, " +
|
|
757
|
-
"budgetDecider ≤150, coDeciders[≤5, ≤100 each]}}.", {
|
|
758
|
-
action: z
|
|
759
|
-
.enum(["add", "replace", "remove"])
|
|
760
|
-
.describe("add = append (fails if productId exists or list at cap 5); replace = overwrite by productId; remove = delete by productId."),
|
|
761
|
-
productId: z
|
|
762
|
-
.string()
|
|
763
|
-
.optional()
|
|
764
|
-
.describe("Lookup key for replace/remove. For 'add' the productId comes from the icp payload."),
|
|
765
|
-
icp: productICPSchema
|
|
766
|
-
.optional()
|
|
767
|
-
.describe("ICP payload. Required for 'add' and 'replace'."),
|
|
768
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
769
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
770
|
-
}, async ({ action, productId, icp, organization_id, dry_run }) => {
|
|
771
|
-
try {
|
|
772
|
-
if ((action === "add" || action === "replace") && !icp) {
|
|
773
|
-
return toolError(`icp is required for action '${action}'.`);
|
|
774
|
-
}
|
|
775
|
-
if ((action === "replace" || action === "remove") && !productId) {
|
|
776
|
-
return toolError(`productId is required for action '${action}'.`);
|
|
777
|
-
}
|
|
778
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
779
|
-
const list = Array.isArray(ci.productICPs)
|
|
780
|
-
? ci.productICPs.map((i) => ({ ...i }))
|
|
781
|
-
: [];
|
|
782
|
-
if (action === "add") {
|
|
783
|
-
const newId = icp.productId;
|
|
784
|
-
if (list.some((i) => i.productId === newId)) {
|
|
785
|
-
throw new Error(`An ICP for productId "${newId}" already exists. Use 'replace' instead.`);
|
|
786
|
-
}
|
|
787
|
-
if (list.length >= PRODUCT_HARD_LIMIT) {
|
|
788
|
-
throw new Error(`ProductICPs list is at cap (${PRODUCT_HARD_LIMIT}). Remove one before adding.`);
|
|
789
|
-
}
|
|
790
|
-
list.push(icp);
|
|
791
|
-
}
|
|
792
|
-
else if (action === "replace") {
|
|
793
|
-
const idx = list.findIndex((i) => i.productId === productId);
|
|
794
|
-
if (idx === -1) {
|
|
795
|
-
throw new Error(`No ICP found with productId "${productId}".`);
|
|
796
|
-
}
|
|
797
|
-
list[idx] = icp;
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
const before = list.length;
|
|
801
|
-
const filtered = list.filter((i) => i.productId !== productId);
|
|
802
|
-
if (filtered.length === before) {
|
|
803
|
-
throw new Error(`No ICP found with productId "${productId}".`);
|
|
804
|
-
}
|
|
805
|
-
list.length = 0;
|
|
806
|
-
list.push(...filtered);
|
|
807
|
-
}
|
|
808
|
-
return { ...ci, productICPs: list };
|
|
809
|
-
}, "Product ICP patch failed against v2 schema.");
|
|
810
|
-
}
|
|
811
|
-
catch (error) {
|
|
812
|
-
return handleToolError(error);
|
|
813
|
-
}
|
|
814
|
-
});
|
|
815
|
-
// ── update_company_info_proof_wording ──────────────────────────────────
|
|
816
|
-
server.tool("update_company_info_proof_wording", "Patch the proofWording block (keyMetrics, miniStories, forbiddenWords). Each list is " +
|
|
817
|
-
"REPLACED wholesale when passed. keyMetrics max 3 (≤150 each), miniStories max 2 (≤300 " +
|
|
818
|
-
"each), forbiddenWords unbounded (≤100 each).", {
|
|
819
|
-
keyMetrics: z
|
|
820
|
-
.array(z.string().max(CHAR_LIMITS.keyMetric))
|
|
821
|
-
.max(3)
|
|
822
|
-
.optional()
|
|
823
|
-
.describe("Replaces keyMetrics[] wholesale (max 3, each ≤150)."),
|
|
824
|
-
miniStories: z
|
|
825
|
-
.array(z.string().max(CHAR_LIMITS.miniStory))
|
|
826
|
-
.max(2)
|
|
827
|
-
.optional()
|
|
828
|
-
.describe("Replaces miniStories[] wholesale (max 2, each ≤300)."),
|
|
829
|
-
forbiddenWords: z
|
|
830
|
-
.array(z.string().max(CHAR_LIMITS.forbiddenWord))
|
|
831
|
-
.optional()
|
|
832
|
-
.describe("Replaces forbiddenWords[] wholesale (each ≤100)."),
|
|
833
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
834
|
-
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
835
|
-
}, async ({ keyMetrics, miniStories, forbiddenWords, organization_id, dry_run }) => {
|
|
836
|
-
try {
|
|
837
|
-
if (keyMetrics === undefined &&
|
|
838
|
-
miniStories === undefined &&
|
|
839
|
-
forbiddenWords === undefined) {
|
|
840
|
-
return toolError("Provide at least one of: keyMetrics, miniStories, forbiddenWords.");
|
|
841
|
-
}
|
|
842
|
-
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
843
|
-
const existing = ci.proofWording &&
|
|
844
|
-
typeof ci.proofWording === "object" &&
|
|
845
|
-
!Array.isArray(ci.proofWording)
|
|
846
|
-
? ci.proofWording
|
|
847
|
-
: {};
|
|
848
|
-
const next = { ...existing };
|
|
849
|
-
if (keyMetrics !== undefined)
|
|
850
|
-
next.keyMetrics = keyMetrics;
|
|
851
|
-
if (miniStories !== undefined)
|
|
852
|
-
next.miniStories = miniStories;
|
|
853
|
-
if (forbiddenWords !== undefined)
|
|
854
|
-
next.forbiddenWords = forbiddenWords;
|
|
855
|
-
return { ...ci, proofWording: next };
|
|
856
|
-
}, "Proof wording patch failed against v2 schema.");
|
|
857
|
-
}
|
|
858
|
-
catch (error) {
|
|
859
|
-
return handleToolError(error);
|
|
860
|
-
}
|
|
861
|
-
});
|
|
862
|
-
// ── add_data_room_document ─────────────────────────────────────────────
|
|
863
|
-
server.tool("add_data_room_document", "Add a textual/markdown document to the data room under a predefined category. " +
|
|
864
|
-
"Use this to store product catalogs, congress lists, target lists, competitor briefs, " +
|
|
865
|
-
"or campaign briefs that personas and agents can reference.", {
|
|
866
|
-
title: z.string().describe("Document title."),
|
|
867
|
-
category: z
|
|
868
|
-
.enum(DOCUMENT_CATEGORIES)
|
|
869
|
-
.describe("Document classification."),
|
|
870
|
-
content: z
|
|
871
|
-
.string()
|
|
872
|
-
.optional()
|
|
873
|
-
.describe("Text or markdown content."),
|
|
874
|
-
organization_id: z.string().describe(ORG_ID_DESC),
|
|
875
|
-
}, async ({ title, category, content, organization_id }) => {
|
|
876
|
-
try {
|
|
877
|
-
const body = { title, category };
|
|
878
|
-
if (content !== undefined)
|
|
879
|
-
body.content = content;
|
|
880
|
-
if (organization_id)
|
|
881
|
-
body.organizationId = organization_id;
|
|
882
|
-
const data = await getClient().post("/api/data-room/document", body);
|
|
883
|
-
return toolResult(data);
|
|
884
|
-
}
|
|
885
|
-
catch (error) {
|
|
886
|
-
return handleToolError(error);
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
}
|