@agifyai/leadify-mcp 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/dist/server.js +2 -0
- package/dist/tools/dataroom.js +630 -458
- package/dist/tools/pipeline.d.ts +2 -0
- package/dist/tools/pipeline.js +104 -0
- package/dist/types.js +11 -0
- package/package.json +1 -1
package/dist/tools/dataroom.js
CHANGED
|
@@ -1,97 +1,158 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getClient } from "../client.js";
|
|
3
3
|
import { toolResult, toolError, handleToolError } from "../types.js";
|
|
4
|
-
// ───
|
|
4
|
+
// ─── Mirror of the backend companyInfo v2 schema ───────────────────────────
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// data-room-schemas.ts
|
|
8
|
-
// the constraints below are a hand-mirrored subset based on observed 422
|
|
9
|
-
// responses + GET /api/data-room payload shapes. The mirror is INTENTIONALLY
|
|
10
|
-
// strict on what we know and permissive (passthrough) on what we don't —
|
|
11
|
-
// so callers get up-front guidance for the documented fields and the
|
|
12
|
-
// backend still owns final validation for everything else.
|
|
6
|
+
// SOURCE OF TRUTH:
|
|
7
|
+
// github.com/AgifyAI/leadify → apps/server/src/lib/data-room-schemas.ts
|
|
13
8
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// - snapshot.constraints[].note ≤ 150 chars
|
|
19
|
-
// - snapshot.pitchOneLiner ≤ 200 chars
|
|
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.
|
|
20
13
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// .
|
|
24
|
-
|
|
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"];
|
|
25
19
|
const PRODUCT_CATEGORIES = ["saas", "medical_device", "service", "mixed"];
|
|
26
|
-
const
|
|
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"];
|
|
27
24
|
const CONSTRAINT_TYPES = ["regulatory", "brand", "legal"];
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
98
|
.object({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
138
|
.object({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
sender: senderSchema.optional().describe("Default sender identity."),
|
|
91
|
-
products: z.array(productSchema).optional(),
|
|
92
|
-
snapshot: snapshotSchema.optional(),
|
|
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,
|
|
93
147
|
})
|
|
94
|
-
.
|
|
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) ───────────────────────────────────────────
|
|
95
156
|
const DOCUMENT_CATEGORIES = [
|
|
96
157
|
"PRODUCT_CATALOG",
|
|
97
158
|
"CONGRESS_LIST",
|
|
@@ -101,9 +162,10 @@ const DOCUMENT_CATEGORIES = [
|
|
|
101
162
|
"CAMPAIGN_BRIEF",
|
|
102
163
|
"OTHER",
|
|
103
164
|
];
|
|
165
|
+
// ─── Common descriptions ───────────────────────────────────────────────────
|
|
104
166
|
const ORG_ID_DESC = "Optional Clerk organization ID to target a specific org's data room. " +
|
|
105
167
|
"Defaults to the caller's own org. Requires admin access for writes.";
|
|
106
|
-
const DRY_RUN_DESC = "If true, validate the
|
|
168
|
+
const DRY_RUN_DESC = "If true, validate the merged payload + return the preview WITHOUT writing. " +
|
|
107
169
|
"Use to dry-fit a change before persisting.";
|
|
108
170
|
async function fetchDataRoom(organizationId) {
|
|
109
171
|
const params = organizationId
|
|
@@ -112,47 +174,73 @@ async function fetchDataRoom(organizationId) {
|
|
|
112
174
|
const data = (await getClient().get("/api/data-room", params));
|
|
113
175
|
return data ?? {};
|
|
114
176
|
}
|
|
177
|
+
function currentCompanyInfo(state) {
|
|
178
|
+
const ci = state.companyInfo;
|
|
179
|
+
if (!ci || typeof ci !== "object" || Array.isArray(ci))
|
|
180
|
+
return {};
|
|
181
|
+
return ci;
|
|
182
|
+
}
|
|
115
183
|
async function putCompanyInfo(companyInfo, organizationId) {
|
|
116
184
|
const body = { companyInfo };
|
|
117
185
|
if (organizationId)
|
|
118
186
|
body.organizationId = organizationId;
|
|
119
187
|
return getClient().put("/api/data-room", body);
|
|
120
188
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
189
|
+
// Validate MCP-side against the mirrored v2 schema and surface ALL issues
|
|
190
|
+
// at once (not just the first). Returns null if value passes, or a tool
|
|
191
|
+
// error otherwise.
|
|
192
|
+
function validateOrError(value, hint) {
|
|
193
|
+
const result = companyInfoV2Schema.safeParse(value);
|
|
194
|
+
if (result.success)
|
|
195
|
+
return null;
|
|
196
|
+
const issues = result.error.issues.map((i) => ({
|
|
197
|
+
path: i.path.map(String).join("."),
|
|
198
|
+
message: i.message,
|
|
199
|
+
code: i.code,
|
|
200
|
+
}));
|
|
201
|
+
return toolError(`MCP validation failed against companyInfo v2 schema (${issues.length} issue${issues.length === 1 ? "" : "s"}). ${hint}`, { issues });
|
|
126
202
|
}
|
|
127
|
-
|
|
203
|
+
// Assert that the existing companyInfo is v2-valid. Granular patch tools
|
|
204
|
+
// require a bootstrapped v2 payload to work — otherwise the merged result
|
|
205
|
+
// will fail v2 validation server-side, since the backend replaces wholesale.
|
|
206
|
+
function requireV2Bootstrap(ci) {
|
|
207
|
+
if (ci.schemaVersion !== COMPANY_INFO_SCHEMA_VERSION) {
|
|
208
|
+
return toolError(`Data room companyInfo is not v${COMPANY_INFO_SCHEMA_VERSION} (got schemaVersion=${JSON.stringify(ci.schemaVersion)}). ` +
|
|
209
|
+
"Bootstrap with update_data_room (passing a full v2 payload) before using granular patch tools. " +
|
|
210
|
+
"See describe_company_info_schema for the contract.");
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function dryRunResult(merged) {
|
|
128
215
|
return toolResult({
|
|
129
216
|
ok: true,
|
|
130
217
|
dry_run: true,
|
|
131
|
-
|
|
132
|
-
preview_company_info: mergedCompanyInfo,
|
|
218
|
+
preview_company_info: merged,
|
|
133
219
|
note: "No write performed. Re-call without dry_run to persist.",
|
|
134
220
|
});
|
|
135
221
|
}
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
222
|
+
// Run the full pipeline for a granular tool: fetch current, mutate via the
|
|
223
|
+
// caller-provided patcher, validate, dry-run or persist.
|
|
224
|
+
async function mergeAndPut(organizationId, dry_run, patcher, validationHint) {
|
|
225
|
+
const current = await fetchDataRoom(organizationId);
|
|
226
|
+
const ci = currentCompanyInfo(current);
|
|
227
|
+
const bootstrapErr = requireV2Bootstrap(ci);
|
|
228
|
+
if (bootstrapErr)
|
|
229
|
+
return bootstrapErr;
|
|
230
|
+
const merged = patcher(ci);
|
|
231
|
+
const validationErr = validateOrError(merged, validationHint);
|
|
232
|
+
if (validationErr)
|
|
233
|
+
return validationErr;
|
|
234
|
+
if (dry_run)
|
|
235
|
+
return dryRunResult(merged);
|
|
236
|
+
const data = await putCompanyInfo(merged, organizationId);
|
|
237
|
+
return toolResult(data);
|
|
150
238
|
}
|
|
151
239
|
// ─── Tool registrations ────────────────────────────────────────────────────
|
|
152
240
|
export function registerDataRoomTools(server) {
|
|
153
241
|
// ── get_data_room ──────────────────────────────────────────────────────
|
|
154
|
-
server.tool("get_data_room", "Retrieve the organization's full data room: company info, all documents, and
|
|
155
|
-
"persona defined for the workspace. Use this as the entry point when you need to " +
|
|
242
|
+
server.tool("get_data_room", "Retrieve the organization's full data room: company info (v2 schema), all documents, and " +
|
|
243
|
+
"every persona defined for the workspace. Use this as the entry point when you need to " +
|
|
156
244
|
"understand the company's context before writing campaigns or messages.", {
|
|
157
245
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
158
246
|
}, async ({ organization_id }) => {
|
|
@@ -165,87 +253,95 @@ export function registerDataRoomTools(server) {
|
|
|
165
253
|
}
|
|
166
254
|
});
|
|
167
255
|
// ── describe_company_info_schema ───────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
"Call this BEFORE the first update of a session to know what the API will accept. " +
|
|
173
|
-
"The mirror is a subset of the backend schema — fields not listed here still pass " +
|
|
174
|
-
"through, but their constraints are enforced server-side and may surface as 422s.", {}, async () => {
|
|
256
|
+
server.tool("describe_company_info_schema", "Return the companyInfo v2 schema mirrored at the MCP layer: all sections, enums, " +
|
|
257
|
+
"max-length caps, required fields, and which patch tool owns each section. Call this " +
|
|
258
|
+
"BEFORE the first update of a session to know what the API will accept. The mirror " +
|
|
259
|
+
"tracks apps/server/src/lib/data-room-schemas.ts in the Leadify backend.", {}, async () => {
|
|
175
260
|
try {
|
|
176
261
|
return toolResult({
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
262
|
+
schemaVersion: COMPANY_INFO_SCHEMA_VERSION,
|
|
263
|
+
source: "Mirror of apps/server/src/lib/data-room-schemas.ts in github.com/AgifyAI/leadify.",
|
|
264
|
+
enums: {
|
|
265
|
+
stage: STAGES,
|
|
266
|
+
product_category: PRODUCT_CATEGORIES,
|
|
267
|
+
ce_class: CE_CLASSES,
|
|
268
|
+
reimbursement_status: REIMBURSEMENT_STATUSES,
|
|
269
|
+
pricing_model: PRICING_MODELS,
|
|
270
|
+
currency: CURRENCIES,
|
|
271
|
+
constraint_type: CONSTRAINT_TYPES,
|
|
272
|
+
geo_region: GEO_REGIONS,
|
|
273
|
+
},
|
|
274
|
+
char_limits: CHAR_LIMITS,
|
|
275
|
+
hard_limits: {
|
|
276
|
+
products_max: PRODUCT_HARD_LIMIT,
|
|
277
|
+
productICPs_max: PRODUCT_HARD_LIMIT,
|
|
278
|
+
constraints_max: 10,
|
|
279
|
+
topUseCases_max_per_product: 3,
|
|
280
|
+
differentiators_max_per_product: 3,
|
|
281
|
+
triggers_max: 5,
|
|
282
|
+
coDeciders_max: 5,
|
|
283
|
+
keyMetrics_max: 3,
|
|
284
|
+
miniStories_max: 2,
|
|
285
|
+
},
|
|
286
|
+
sections: {
|
|
287
|
+
identity: {
|
|
288
|
+
fields: ["name (required, ≤200)", "website (optional, ≤500)"],
|
|
289
|
+
patch_tool: "update_company_info_identity",
|
|
290
|
+
},
|
|
291
|
+
snapshot: {
|
|
292
|
+
fields: [
|
|
293
|
+
"pitchOneLiner (required, ≤200)",
|
|
294
|
+
"marketSpecialty (required, ≤100)",
|
|
295
|
+
"stage (required, enum)",
|
|
296
|
+
"salesTeamSizeFR (required, int ≥0)",
|
|
297
|
+
"geo {regions[], countriesIncluded[2-letter codes], countriesExcluded[2-letter codes]}",
|
|
298
|
+
"constraints[] (max 10, see update_company_info_constraint)",
|
|
299
|
+
],
|
|
300
|
+
patch_tool: "update_company_info_snapshot",
|
|
194
301
|
},
|
|
195
302
|
products: {
|
|
196
|
-
|
|
197
|
-
item_shape: {
|
|
198
|
-
name: { type: "string", required: true },
|
|
199
|
-
category: {
|
|
200
|
-
type: "enum",
|
|
201
|
-
values: PRODUCT_CATEGORIES,
|
|
202
|
-
required: true,
|
|
203
|
-
},
|
|
204
|
-
description: { type: "string" },
|
|
205
|
-
valueProposition: { type: "string" },
|
|
206
|
-
targetCustomer: { type: "string" },
|
|
207
|
-
pricing: { type: "string" },
|
|
208
|
-
},
|
|
303
|
+
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}}.",
|
|
209
304
|
patch_tool: "update_company_info_product",
|
|
210
305
|
},
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
note: {
|
|
225
|
-
type: "string",
|
|
226
|
-
max_length: 150,
|
|
227
|
-
required: true,
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
patch_tool: "update_company_info_constraint",
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
patch_tool: "update_company_info_snapshot",
|
|
306
|
+
economics: {
|
|
307
|
+
fields: [
|
|
308
|
+
"pricingModel (required, enum)",
|
|
309
|
+
"ticketRange {min, max, currency} (min ≤ max)",
|
|
310
|
+
"salesCycleMonths {min, max} (min ≤ max)",
|
|
311
|
+
"triggers[] (max 5, each ≤150)",
|
|
312
|
+
"defaultBaseline (required, ≤200)",
|
|
313
|
+
],
|
|
314
|
+
patch_tool: "update_company_info_economics",
|
|
315
|
+
},
|
|
316
|
+
productICPs: {
|
|
317
|
+
note: "Up to 5 ICPs. Each: {productId (must reference products[].name), establishmentType ≤150, technicalPrereqs ≤200, roles {champion ≤150, budgetDecider ≤150, coDeciders[≤5, ≤100 each]}}.",
|
|
318
|
+
patch_tool: "update_company_info_product_icp",
|
|
234
319
|
},
|
|
320
|
+
proofWording: {
|
|
321
|
+
fields: [
|
|
322
|
+
"keyMetrics[] (max 3, each ≤150)",
|
|
323
|
+
"miniStories[] (max 2, each ≤300)",
|
|
324
|
+
"forbiddenWords[] (each ≤100)",
|
|
325
|
+
],
|
|
326
|
+
patch_tool: "update_company_info_proof_wording",
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
escape_hatch: {
|
|
330
|
+
tool: "update_data_room",
|
|
331
|
+
note: "WHOLESALE replacement. Required for first-time bootstrap (no existing v2 payload).",
|
|
235
332
|
},
|
|
236
|
-
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"update_company_info_snapshot",
|
|
241
|
-
"update_company_info_product",
|
|
242
|
-
"update_company_info_constraint",
|
|
333
|
+
cross_field_invariants: [
|
|
334
|
+
"productICPs[].productId must equal one of products[].name (or be empty).",
|
|
335
|
+
"economics.ticketRange.min ≤ economics.ticketRange.max.",
|
|
336
|
+
"economics.salesCycleMonths.min ≤ economics.salesCycleMonths.max.",
|
|
243
337
|
],
|
|
244
|
-
|
|
338
|
+
error_format: {
|
|
339
|
+
shape: "{ error, path, issues: ZodIssue[] } — issues[] contains EVERY validation failure at once.",
|
|
340
|
+
note: "Backend now returns all issues simultaneously. Fix all listed paths before retrying.",
|
|
341
|
+
},
|
|
245
342
|
notes: [
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"Prefer the granular patch tools over update_data_room to avoid wiping nested blocks.",
|
|
343
|
+
"Backend replaces companyInfo wholesale on PUT. Granular tools fetch + merge + send the FULL v2 payload back.",
|
|
344
|
+
"Granular tools require a v2-bootstrapped data room. Call update_data_room with a full v2 payload first if schemaVersion is absent or older.",
|
|
249
345
|
],
|
|
250
346
|
});
|
|
251
347
|
}
|
|
@@ -254,29 +350,26 @@ export function registerDataRoomTools(server) {
|
|
|
254
350
|
}
|
|
255
351
|
});
|
|
256
352
|
// ── update_data_room ───────────────────────────────────────────────────
|
|
257
|
-
// Escape hatch:
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
server.tool("update_data_room", "WHOLESALE update of companyInfo (ESCAPE HATCH).
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
353
|
+
// Escape hatch: WHOLESALE replacement. The only path for first-time
|
|
354
|
+
// bootstrap (no existing v2 companyInfo). Validates against v2 before
|
|
355
|
+
// hitting the API.
|
|
356
|
+
server.tool("update_data_room", "WHOLESALE update of companyInfo (ESCAPE HATCH). REPLACES the entire companyInfo with the " +
|
|
357
|
+
"payload you pass. Required for first-time bootstrap. For incremental edits on an " +
|
|
358
|
+
"existing v2 payload, ALWAYS prefer the granular update_company_info_* tools — they " +
|
|
359
|
+
"fetch + merge + write the full v2 payload back, so they can't accidentally drop " +
|
|
360
|
+
"required sections. MCP validates against the v2 schema before sending.", {
|
|
265
361
|
company_info: z
|
|
266
362
|
.record(z.unknown())
|
|
267
|
-
.describe("companyInfo payload
|
|
268
|
-
"(see describe_company_info_schema). Unknown fields pass through."),
|
|
363
|
+
.describe("Full v2 companyInfo payload (replaces the existing one). See describe_company_info_schema for the contract."),
|
|
269
364
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
270
365
|
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
271
366
|
}, async ({ company_info, organization_id, dry_run }) => {
|
|
272
367
|
try {
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
return
|
|
368
|
+
const validationErr = validateOrError(company_info, "Pass a complete v2 payload — see describe_company_info_schema for the required fields.");
|
|
369
|
+
if (validationErr)
|
|
370
|
+
return validationErr;
|
|
276
371
|
if (dry_run) {
|
|
277
|
-
|
|
278
|
-
const merged = { ...currentCompanyInfo(current), ...company_info };
|
|
279
|
-
return dryRunResult(company_info, merged);
|
|
372
|
+
return dryRunResult(company_info);
|
|
280
373
|
}
|
|
281
374
|
const data = await putCompanyInfo(company_info, organization_id);
|
|
282
375
|
return toolResult(data);
|
|
@@ -286,195 +379,180 @@ export function registerDataRoomTools(server) {
|
|
|
286
379
|
}
|
|
287
380
|
});
|
|
288
381
|
// ── update_company_info_identity ───────────────────────────────────────
|
|
289
|
-
server.tool("update_company_info_identity", "Patch
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
name: z
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}, async ({ name, sector, description, size, website, valueProposition, organization_id, dry_run, }) => {
|
|
301
|
-
try {
|
|
302
|
-
const patch = {};
|
|
303
|
-
if (name !== undefined)
|
|
304
|
-
patch.name = name;
|
|
305
|
-
if (sector !== undefined)
|
|
306
|
-
patch.sector = sector;
|
|
307
|
-
if (description !== undefined)
|
|
308
|
-
patch.description = description;
|
|
309
|
-
if (size !== undefined)
|
|
310
|
-
patch.size = size;
|
|
311
|
-
if (website !== undefined)
|
|
312
|
-
patch.website = website;
|
|
313
|
-
if (valueProposition !== undefined)
|
|
314
|
-
patch.valueProposition = valueProposition;
|
|
315
|
-
if (Object.keys(patch).length === 0) {
|
|
316
|
-
return toolError("Provide at least one of: name, sector, description, size, website, valueProposition.");
|
|
317
|
-
}
|
|
318
|
-
const current = await fetchDataRoom(organization_id);
|
|
319
|
-
const merged = { ...currentCompanyInfo(current), ...patch };
|
|
320
|
-
if (dry_run)
|
|
321
|
-
return dryRunResult(patch, merged);
|
|
322
|
-
const data = await putCompanyInfo(patch, organization_id);
|
|
323
|
-
return toolResult(data);
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
return handleToolError(error);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
// ── update_company_info_sender ─────────────────────────────────────────
|
|
330
|
-
server.tool("update_company_info_sender", "Patch the sender block (default SDR identity). Reads current sender, shallow-merges " +
|
|
331
|
-
"the fields you pass, writes the whole sender object back. Pass set_to_null=true to " +
|
|
332
|
-
"clear the sender entirely.", {
|
|
333
|
-
name: z.string().optional().describe("Sender display name."),
|
|
334
|
-
title: z.string().optional().describe("Sender title / role."),
|
|
335
|
-
email: z.string().email().optional().describe("Sender email address."),
|
|
336
|
-
set_to_null: z
|
|
337
|
-
.boolean()
|
|
382
|
+
server.tool("update_company_info_identity", "Patch the top-level identity fields of companyInfo (name, website). Reads the current " +
|
|
383
|
+
"v2 payload, merges your patch, writes the full v2 payload back. Other sections " +
|
|
384
|
+
"(snapshot, products, economics, productICPs, proofWording) remain intact.", {
|
|
385
|
+
name: z
|
|
386
|
+
.string()
|
|
387
|
+
.max(CHAR_LIMITS.name)
|
|
388
|
+
.optional()
|
|
389
|
+
.describe("Company name (max 200)."),
|
|
390
|
+
website: z
|
|
391
|
+
.string()
|
|
392
|
+
.max(CHAR_LIMITS.website)
|
|
338
393
|
.optional()
|
|
339
|
-
.describe("
|
|
394
|
+
.describe("Company website URL (max 500)."),
|
|
340
395
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
341
396
|
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
342
|
-
}, async ({ name,
|
|
397
|
+
}, async ({ name, website, organization_id, dry_run }) => {
|
|
343
398
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return toolError("Provide at least one of: name, title, email, set_to_null.");
|
|
347
|
-
}
|
|
348
|
-
if (set_to_null && hasFields) {
|
|
349
|
-
return toolError("'set_to_null' is mutually exclusive with name/title/email.");
|
|
350
|
-
}
|
|
351
|
-
const current = await fetchDataRoom(organization_id);
|
|
352
|
-
const ci = currentCompanyInfo(current);
|
|
353
|
-
const existingSender = ci.sender && typeof ci.sender === "object" && !Array.isArray(ci.sender)
|
|
354
|
-
? ci.sender
|
|
355
|
-
: {};
|
|
356
|
-
let nextSender;
|
|
357
|
-
if (set_to_null) {
|
|
358
|
-
nextSender = null;
|
|
399
|
+
if (name === undefined && website === undefined) {
|
|
400
|
+
return toolError("Provide at least one of: name, website.");
|
|
359
401
|
}
|
|
360
|
-
|
|
361
|
-
|
|
402
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
403
|
+
const out = { ...ci };
|
|
362
404
|
if (name !== undefined)
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const validationError = validateOrError(senderSchema, nextSender, "sender");
|
|
369
|
-
if (validationError)
|
|
370
|
-
return validationError;
|
|
371
|
-
}
|
|
372
|
-
const patch = { sender: nextSender };
|
|
373
|
-
const merged = { ...ci, ...patch };
|
|
374
|
-
if (dry_run)
|
|
375
|
-
return dryRunResult(patch, merged);
|
|
376
|
-
const data = await putCompanyInfo(patch, organization_id);
|
|
377
|
-
return toolResult(data);
|
|
405
|
+
out.name = name;
|
|
406
|
+
if (website !== undefined)
|
|
407
|
+
out.website = website;
|
|
408
|
+
return out;
|
|
409
|
+
}, "Identity patch failed against v2 schema.");
|
|
378
410
|
}
|
|
379
411
|
catch (error) {
|
|
380
412
|
return handleToolError(error);
|
|
381
413
|
}
|
|
382
414
|
});
|
|
383
|
-
// ──
|
|
384
|
-
server.tool("
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
.
|
|
390
|
-
|
|
391
|
-
.
|
|
392
|
-
.describe("
|
|
393
|
-
|
|
394
|
-
.
|
|
395
|
-
.
|
|
396
|
-
.
|
|
415
|
+
// ── update_company_info_snapshot ───────────────────────────────────────
|
|
416
|
+
server.tool("update_company_info_snapshot", "Patch the snapshot block. Reads the current v2 payload, shallow-merges the snapshot " +
|
|
417
|
+
"fields you pass, writes the full v2 payload back. geo is merged shallowly — pass any " +
|
|
418
|
+
"subset of regions/countriesIncluded/countriesExcluded. constraints[] is REPLACED " +
|
|
419
|
+
"wholesale when passed — use update_company_info_constraint for per-item edits.", {
|
|
420
|
+
pitchOneLiner: z
|
|
421
|
+
.string()
|
|
422
|
+
.max(CHAR_LIMITS.pitchOneLiner)
|
|
423
|
+
.optional()
|
|
424
|
+
.describe("One-liner pitch (max 200)."),
|
|
425
|
+
marketSpecialty: z
|
|
426
|
+
.string()
|
|
427
|
+
.max(CHAR_LIMITS.marketSpecialty)
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("Market specialty (max 100)."),
|
|
430
|
+
stage: z
|
|
431
|
+
.enum(STAGES)
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("Company stage enum: early | growth | scaling | enterprise."),
|
|
434
|
+
salesTeamSizeFR: z
|
|
435
|
+
.number()
|
|
436
|
+
.int()
|
|
437
|
+
.min(0)
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("Sales team size in FR (non-negative integer)."),
|
|
440
|
+
geo: z
|
|
441
|
+
.object({
|
|
442
|
+
regions: z.array(z.enum(GEO_REGIONS)).optional(),
|
|
443
|
+
countriesIncluded: z.array(z.string().length(2)).optional(),
|
|
444
|
+
countriesExcluded: z.array(z.string().length(2)).optional(),
|
|
445
|
+
})
|
|
446
|
+
.optional()
|
|
447
|
+
.describe("Geo block (shallow-merged). regions enum: " +
|
|
448
|
+
GEO_REGIONS.join(", ") +
|
|
449
|
+
". countries: ISO 3166-1 alpha-2 (2 letters)."),
|
|
450
|
+
constraints: z
|
|
451
|
+
.array(constraintSchema)
|
|
452
|
+
.max(10)
|
|
453
|
+
.optional()
|
|
454
|
+
.describe("Replaces constraints[] wholesale. For per-item edits use update_company_info_constraint."),
|
|
397
455
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
398
456
|
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
399
|
-
}, async ({
|
|
457
|
+
}, async ({ pitchOneLiner, marketSpecialty, stage, salesTeamSizeFR, geo, constraints, organization_id, dry_run, }) => {
|
|
400
458
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
else if (action === "add") {
|
|
409
|
-
next = [...new Set([...existing, ...values])];
|
|
459
|
+
if (pitchOneLiner === undefined &&
|
|
460
|
+
marketSpecialty === undefined &&
|
|
461
|
+
stage === undefined &&
|
|
462
|
+
salesTeamSizeFR === undefined &&
|
|
463
|
+
geo === undefined &&
|
|
464
|
+
constraints === undefined) {
|
|
465
|
+
return toolError("Provide at least one of: pitchOneLiner, marketSpecialty, stage, salesTeamSizeFR, geo, constraints.");
|
|
410
466
|
}
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
467
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
468
|
+
const existingSnapshot = ci.snapshot && typeof ci.snapshot === "object" && !Array.isArray(ci.snapshot)
|
|
469
|
+
? ci.snapshot
|
|
470
|
+
: {};
|
|
471
|
+
const nextSnapshot = { ...existingSnapshot };
|
|
472
|
+
if (pitchOneLiner !== undefined)
|
|
473
|
+
nextSnapshot.pitchOneLiner = pitchOneLiner;
|
|
474
|
+
if (marketSpecialty !== undefined)
|
|
475
|
+
nextSnapshot.marketSpecialty = marketSpecialty;
|
|
476
|
+
if (stage !== undefined)
|
|
477
|
+
nextSnapshot.stage = stage;
|
|
478
|
+
if (salesTeamSizeFR !== undefined)
|
|
479
|
+
nextSnapshot.salesTeamSizeFR = salesTeamSizeFR;
|
|
480
|
+
if (geo !== undefined) {
|
|
481
|
+
const existingGeo = existingSnapshot.geo &&
|
|
482
|
+
typeof existingSnapshot.geo === "object" &&
|
|
483
|
+
!Array.isArray(existingSnapshot.geo)
|
|
484
|
+
? existingSnapshot.geo
|
|
485
|
+
: {};
|
|
486
|
+
nextSnapshot.geo = { ...existingGeo, ...geo };
|
|
487
|
+
}
|
|
488
|
+
if (constraints !== undefined)
|
|
489
|
+
nextSnapshot.constraints = constraints;
|
|
490
|
+
return { ...ci, snapshot: nextSnapshot };
|
|
491
|
+
}, "Snapshot patch failed against v2 schema.");
|
|
421
492
|
}
|
|
422
493
|
catch (error) {
|
|
423
494
|
return handleToolError(error);
|
|
424
495
|
}
|
|
425
496
|
});
|
|
426
|
-
// ──
|
|
427
|
-
server.tool("
|
|
428
|
-
"
|
|
429
|
-
"
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
.enum(
|
|
433
|
-
.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.
|
|
437
|
-
.
|
|
438
|
-
.optional()
|
|
439
|
-
.describe("One-liner pitch. Max 200 chars."),
|
|
440
|
-
constraints: z
|
|
441
|
-
.array(constraintSchema)
|
|
497
|
+
// ── update_company_info_constraint ─────────────────────────────────────
|
|
498
|
+
server.tool("update_company_info_constraint", "Add, replace, or remove a single business constraint in snapshot.constraints[] without " +
|
|
499
|
+
"re-sending the whole list. Constraints have no stable id — entries are addressed by " +
|
|
500
|
+
"zero-based index. Beware: indices shift after a remove. Cap is 10 constraints. " +
|
|
501
|
+
"Each constraint: {label ≤150, type ∈ regulatory|brand|legal, note? ≤150}.", {
|
|
502
|
+
action: z
|
|
503
|
+
.enum(["add", "replace", "remove"])
|
|
504
|
+
.describe("add = append; replace = overwrite at index; remove = delete at index."),
|
|
505
|
+
index: z
|
|
506
|
+
.number()
|
|
507
|
+
.int()
|
|
508
|
+
.nonnegative()
|
|
442
509
|
.optional()
|
|
443
|
-
.describe("
|
|
444
|
-
|
|
445
|
-
.record(z.unknown())
|
|
510
|
+
.describe("Zero-based index. Required for 'replace' and 'remove'."),
|
|
511
|
+
constraint: constraintSchema
|
|
446
512
|
.optional()
|
|
447
|
-
.describe("
|
|
513
|
+
.describe("Constraint payload {label ≤150, type ∈ regulatory|brand|legal, note? ≤150}. Required for 'add' and 'replace'."),
|
|
448
514
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
449
515
|
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
450
|
-
}, async ({
|
|
516
|
+
}, async ({ action, index, constraint, organization_id, dry_run }) => {
|
|
451
517
|
try {
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
constraints === undefined &&
|
|
455
|
-
(extra === undefined || Object.keys(extra).length === 0)) {
|
|
456
|
-
return toolError("Provide at least one of: stage, pitchOneLiner, constraints, extra.");
|
|
518
|
+
if ((action === "add" || action === "replace") && !constraint) {
|
|
519
|
+
return toolError(`constraint is required for action '${action}'.`);
|
|
457
520
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
521
|
+
if ((action === "replace" || action === "remove") && index === undefined) {
|
|
522
|
+
return toolError(`index is required for action '${action}'.`);
|
|
523
|
+
}
|
|
524
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
525
|
+
const existingSnapshot = ci.snapshot && typeof ci.snapshot === "object" && !Array.isArray(ci.snapshot)
|
|
526
|
+
? ci.snapshot
|
|
527
|
+
: {};
|
|
528
|
+
const list = Array.isArray(existingSnapshot.constraints)
|
|
529
|
+
? existingSnapshot.constraints.map((c) => ({
|
|
530
|
+
...c,
|
|
531
|
+
}))
|
|
532
|
+
: [];
|
|
533
|
+
if (action === "add") {
|
|
534
|
+
if (list.length >= 10) {
|
|
535
|
+
throw new Error("Constraints list is at cap (10). Remove one before adding.");
|
|
536
|
+
}
|
|
537
|
+
list.push(constraint);
|
|
538
|
+
}
|
|
539
|
+
else if (action === "replace") {
|
|
540
|
+
if (index < 0 || index >= list.length) {
|
|
541
|
+
throw new Error(`Index ${index} out of bounds (constraints list has ${list.length} entries).`);
|
|
542
|
+
}
|
|
543
|
+
list[index] = constraint;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
if (index < 0 || index >= list.length) {
|
|
547
|
+
throw new Error(`Index ${index} out of bounds (constraints list has ${list.length} entries).`);
|
|
548
|
+
}
|
|
549
|
+
list.splice(index, 1);
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
...ci,
|
|
553
|
+
snapshot: { ...existingSnapshot, constraints: list },
|
|
554
|
+
};
|
|
555
|
+
}, "Constraint patch failed against v2 schema.");
|
|
478
556
|
}
|
|
479
557
|
catch (error) {
|
|
480
558
|
return handleToolError(error);
|
|
@@ -482,19 +560,22 @@ export function registerDataRoomTools(server) {
|
|
|
482
560
|
});
|
|
483
561
|
// ── update_company_info_product ────────────────────────────────────────
|
|
484
562
|
server.tool("update_company_info_product", "Add, replace, or remove a single product in companyInfo.products[] without re-sending " +
|
|
485
|
-
"the whole array. Lookups
|
|
486
|
-
"
|
|
487
|
-
"
|
|
563
|
+
"the whole array. Lookups by product `name` (case-sensitive exact match). Cap is 5 " +
|
|
564
|
+
"products. Each product: {name ≤100, category enum, categoryDetail? ≤150, outcomeUser " +
|
|
565
|
+
"≤200, outcomeBuyer ≤200, topUseCases[≤3, ≤150 each], differentiators[≤3, ≤150 each], " +
|
|
566
|
+
"notFor ≤200, regulatory {ceMarked, ceClass nullable, reimbursementStatus enum, aoRequired}}. " +
|
|
567
|
+
"WARNING: if a productICP.productId references this product's name, removing/renaming " +
|
|
568
|
+
"will fail the cross-field refine — fix the ICP first.", {
|
|
488
569
|
action: z
|
|
489
570
|
.enum(["add", "replace", "remove"])
|
|
490
|
-
.describe("add = append (fails if name
|
|
571
|
+
.describe("add = append (fails if name exists or list at cap 5); replace = overwrite by name; remove = delete by name."),
|
|
491
572
|
name: z
|
|
492
573
|
.string()
|
|
493
574
|
.optional()
|
|
494
575
|
.describe("Lookup key for replace/remove. Required for those actions. For 'add' the name comes from the product payload."),
|
|
495
576
|
product: productSchema
|
|
496
577
|
.optional()
|
|
497
|
-
.describe("Product payload. Required for 'add' and 'replace'.
|
|
578
|
+
.describe("Product payload. Required for 'add' and 'replace'."),
|
|
498
579
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
499
580
|
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
500
581
|
}, async ({ action, name, product, organization_id, dry_run }) => {
|
|
@@ -505,105 +586,213 @@ export function registerDataRoomTools(server) {
|
|
|
505
586
|
if ((action === "replace" || action === "remove") && !name) {
|
|
506
587
|
return toolError(`name is required for action '${action}'.`);
|
|
507
588
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
589
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
590
|
+
const list = Array.isArray(ci.products)
|
|
591
|
+
? ci.products.map((p) => ({ ...p }))
|
|
592
|
+
: [];
|
|
593
|
+
if (action === "add") {
|
|
594
|
+
const newName = product.name;
|
|
595
|
+
if (list.some((p) => p.name === newName)) {
|
|
596
|
+
throw new Error(`A product with name "${newName}" already exists. Use action 'replace' instead.`);
|
|
597
|
+
}
|
|
598
|
+
if (list.length >= PRODUCT_HARD_LIMIT) {
|
|
599
|
+
throw new Error(`Products list is at cap (${PRODUCT_HARD_LIMIT}). Remove one before adding.`);
|
|
600
|
+
}
|
|
601
|
+
list.push(product);
|
|
517
602
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
603
|
+
else if (action === "replace") {
|
|
604
|
+
const idx = list.findIndex((p) => p.name === name);
|
|
605
|
+
if (idx === -1) {
|
|
606
|
+
throw new Error(`No product found with name "${name}".`);
|
|
607
|
+
}
|
|
608
|
+
list[idx] = product;
|
|
524
609
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
610
|
+
else {
|
|
611
|
+
const before = list.length;
|
|
612
|
+
const filtered = list.filter((p) => p.name !== name);
|
|
613
|
+
if (filtered.length === before) {
|
|
614
|
+
throw new Error(`No product found with name "${name}".`);
|
|
615
|
+
}
|
|
616
|
+
list.length = 0;
|
|
617
|
+
list.push(...filtered);
|
|
532
618
|
}
|
|
533
|
-
list
|
|
534
|
-
|
|
619
|
+
return { ...ci, products: list };
|
|
620
|
+
}, "Product patch failed against v2 schema.");
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
return handleToolError(error);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
// ── update_company_info_economics ──────────────────────────────────────
|
|
627
|
+
server.tool("update_company_info_economics", "Patch the economics block. Reads current economics, shallow-merges your fields, writes " +
|
|
628
|
+
"back. ticketRange and salesCycleMonths are REPLACED wholesale when passed (their " +
|
|
629
|
+
"internal refine enforces min ≤ max). triggers[] is replaced wholesale.", {
|
|
630
|
+
pricingModel: z
|
|
631
|
+
.enum(PRICING_MODELS)
|
|
632
|
+
.optional()
|
|
633
|
+
.describe("Pricing model enum: saas_per_site | saas_per_exam | capex | service | mixed."),
|
|
634
|
+
ticketRange: z
|
|
635
|
+
.object({
|
|
636
|
+
min: z.number().min(0),
|
|
637
|
+
max: z.number().min(0),
|
|
638
|
+
currency: z.enum(CURRENCIES),
|
|
639
|
+
})
|
|
640
|
+
.optional()
|
|
641
|
+
.describe("Replaces ticketRange wholesale. min ≤ max enforced. currency ∈ EUR|USD."),
|
|
642
|
+
salesCycleMonths: z
|
|
643
|
+
.object({
|
|
644
|
+
min: z.number().int().min(0),
|
|
645
|
+
max: z.number().int().min(0),
|
|
646
|
+
})
|
|
647
|
+
.optional()
|
|
648
|
+
.describe("Replaces salesCycleMonths wholesale. min ≤ max enforced."),
|
|
649
|
+
triggers: z
|
|
650
|
+
.array(z.string().max(CHAR_LIMITS.triggerItem))
|
|
651
|
+
.max(5)
|
|
652
|
+
.optional()
|
|
653
|
+
.describe("Replaces triggers[] wholesale (max 5 items, each ≤150)."),
|
|
654
|
+
defaultBaseline: z
|
|
655
|
+
.string()
|
|
656
|
+
.max(CHAR_LIMITS.defaultBaseline)
|
|
657
|
+
.optional()
|
|
658
|
+
.describe("Default baseline (max 200)."),
|
|
659
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
660
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
661
|
+
}, async ({ pricingModel, ticketRange, salesCycleMonths, triggers, defaultBaseline, organization_id, dry_run, }) => {
|
|
662
|
+
try {
|
|
663
|
+
if (pricingModel === undefined &&
|
|
664
|
+
ticketRange === undefined &&
|
|
665
|
+
salesCycleMonths === undefined &&
|
|
666
|
+
triggers === undefined &&
|
|
667
|
+
defaultBaseline === undefined) {
|
|
668
|
+
return toolError("Provide at least one of: pricingModel, ticketRange, salesCycleMonths, triggers, defaultBaseline.");
|
|
535
669
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
670
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
671
|
+
const existing = ci.economics && typeof ci.economics === "object" && !Array.isArray(ci.economics)
|
|
672
|
+
? ci.economics
|
|
673
|
+
: {};
|
|
674
|
+
const next = { ...existing };
|
|
675
|
+
if (pricingModel !== undefined)
|
|
676
|
+
next.pricingModel = pricingModel;
|
|
677
|
+
if (ticketRange !== undefined)
|
|
678
|
+
next.ticketRange = ticketRange;
|
|
679
|
+
if (salesCycleMonths !== undefined)
|
|
680
|
+
next.salesCycleMonths = salesCycleMonths;
|
|
681
|
+
if (triggers !== undefined)
|
|
682
|
+
next.triggers = triggers;
|
|
683
|
+
if (defaultBaseline !== undefined)
|
|
684
|
+
next.defaultBaseline = defaultBaseline;
|
|
685
|
+
return { ...ci, economics: next };
|
|
686
|
+
}, "Economics patch failed against v2 schema.");
|
|
542
687
|
}
|
|
543
688
|
catch (error) {
|
|
544
689
|
return handleToolError(error);
|
|
545
690
|
}
|
|
546
691
|
});
|
|
547
|
-
// ──
|
|
548
|
-
server.tool("
|
|
549
|
-
"
|
|
550
|
-
"
|
|
551
|
-
"
|
|
552
|
-
"capped at 150 chars — validated MCP-side.", {
|
|
692
|
+
// ── update_company_info_product_icp ────────────────────────────────────
|
|
693
|
+
server.tool("update_company_info_product_icp", "Add, replace, or remove a single product ICP in companyInfo.productICPs[]. Lookups by " +
|
|
694
|
+
"`productId` (must match an existing products[].name). Cap is 5 ICPs. Each ICP: " +
|
|
695
|
+
"{productId, establishmentType ≤150, technicalPrereqs ≤200, roles {champion ≤150, " +
|
|
696
|
+
"budgetDecider ≤150, coDeciders[≤5, ≤100 each]}}.", {
|
|
553
697
|
action: z
|
|
554
698
|
.enum(["add", "replace", "remove"])
|
|
555
|
-
.describe("add = append; replace = overwrite
|
|
556
|
-
|
|
557
|
-
.
|
|
558
|
-
.int()
|
|
559
|
-
.nonnegative()
|
|
699
|
+
.describe("add = append (fails if productId exists or list at cap 5); replace = overwrite by productId; remove = delete by productId."),
|
|
700
|
+
productId: z
|
|
701
|
+
.string()
|
|
560
702
|
.optional()
|
|
561
|
-
.describe("
|
|
562
|
-
|
|
703
|
+
.describe("Lookup key for replace/remove. For 'add' the productId comes from the icp payload."),
|
|
704
|
+
icp: productICPSchema
|
|
563
705
|
.optional()
|
|
564
|
-
.describe("
|
|
706
|
+
.describe("ICP payload. Required for 'add' and 'replace'."),
|
|
565
707
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
566
708
|
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
567
|
-
}, async ({ action,
|
|
709
|
+
}, async ({ action, productId, icp, organization_id, dry_run }) => {
|
|
568
710
|
try {
|
|
569
|
-
if ((action === "add" || action === "replace") && !
|
|
570
|
-
return toolError(`
|
|
711
|
+
if ((action === "add" || action === "replace") && !icp) {
|
|
712
|
+
return toolError(`icp is required for action '${action}'.`);
|
|
571
713
|
}
|
|
572
|
-
if ((action === "replace" || action === "remove") &&
|
|
573
|
-
return toolError(`
|
|
574
|
-
}
|
|
575
|
-
const current = await fetchDataRoom(organization_id);
|
|
576
|
-
const ci = currentCompanyInfo(current);
|
|
577
|
-
const existingSnapshot = ci.snapshot && typeof ci.snapshot === "object" && !Array.isArray(ci.snapshot)
|
|
578
|
-
? ci.snapshot
|
|
579
|
-
: {};
|
|
580
|
-
const list = Array.isArray(existingSnapshot.constraints)
|
|
581
|
-
? existingSnapshot.constraints.map((c) => ({
|
|
582
|
-
...c,
|
|
583
|
-
}))
|
|
584
|
-
: [];
|
|
585
|
-
if (action === "add") {
|
|
586
|
-
list.push(constraint);
|
|
714
|
+
if ((action === "replace" || action === "remove") && !productId) {
|
|
715
|
+
return toolError(`productId is required for action '${action}'.`);
|
|
587
716
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
717
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
718
|
+
const list = Array.isArray(ci.productICPs)
|
|
719
|
+
? ci.productICPs.map((i) => ({ ...i }))
|
|
720
|
+
: [];
|
|
721
|
+
if (action === "add") {
|
|
722
|
+
const newId = icp.productId;
|
|
723
|
+
if (list.some((i) => i.productId === newId)) {
|
|
724
|
+
throw new Error(`An ICP for productId "${newId}" already exists. Use 'replace' instead.`);
|
|
725
|
+
}
|
|
726
|
+
if (list.length >= PRODUCT_HARD_LIMIT) {
|
|
727
|
+
throw new Error(`ProductICPs list is at cap (${PRODUCT_HARD_LIMIT}). Remove one before adding.`);
|
|
728
|
+
}
|
|
729
|
+
list.push(icp);
|
|
591
730
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
731
|
+
else if (action === "replace") {
|
|
732
|
+
const idx = list.findIndex((i) => i.productId === productId);
|
|
733
|
+
if (idx === -1) {
|
|
734
|
+
throw new Error(`No ICP found with productId "${productId}".`);
|
|
735
|
+
}
|
|
736
|
+
list[idx] = icp;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
const before = list.length;
|
|
740
|
+
const filtered = list.filter((i) => i.productId !== productId);
|
|
741
|
+
if (filtered.length === before) {
|
|
742
|
+
throw new Error(`No ICP found with productId "${productId}".`);
|
|
743
|
+
}
|
|
744
|
+
list.length = 0;
|
|
745
|
+
list.push(...filtered);
|
|
597
746
|
}
|
|
598
|
-
|
|
747
|
+
return { ...ci, productICPs: list };
|
|
748
|
+
}, "Product ICP patch failed against v2 schema.");
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
return handleToolError(error);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
// ── update_company_info_proof_wording ──────────────────────────────────
|
|
755
|
+
server.tool("update_company_info_proof_wording", "Patch the proofWording block (keyMetrics, miniStories, forbiddenWords). Each list is " +
|
|
756
|
+
"REPLACED wholesale when passed. keyMetrics max 3 (≤150 each), miniStories max 2 (≤300 " +
|
|
757
|
+
"each), forbiddenWords unbounded (≤100 each).", {
|
|
758
|
+
keyMetrics: z
|
|
759
|
+
.array(z.string().max(CHAR_LIMITS.keyMetric))
|
|
760
|
+
.max(3)
|
|
761
|
+
.optional()
|
|
762
|
+
.describe("Replaces keyMetrics[] wholesale (max 3, each ≤150)."),
|
|
763
|
+
miniStories: z
|
|
764
|
+
.array(z.string().max(CHAR_LIMITS.miniStory))
|
|
765
|
+
.max(2)
|
|
766
|
+
.optional()
|
|
767
|
+
.describe("Replaces miniStories[] wholesale (max 2, each ≤300)."),
|
|
768
|
+
forbiddenWords: z
|
|
769
|
+
.array(z.string().max(CHAR_LIMITS.forbiddenWord))
|
|
770
|
+
.optional()
|
|
771
|
+
.describe("Replaces forbiddenWords[] wholesale (each ≤100)."),
|
|
772
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
773
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
774
|
+
}, async ({ keyMetrics, miniStories, forbiddenWords, organization_id, dry_run }) => {
|
|
775
|
+
try {
|
|
776
|
+
if (keyMetrics === undefined &&
|
|
777
|
+
miniStories === undefined &&
|
|
778
|
+
forbiddenWords === undefined) {
|
|
779
|
+
return toolError("Provide at least one of: keyMetrics, miniStories, forbiddenWords.");
|
|
599
780
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
781
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
782
|
+
const existing = ci.proofWording &&
|
|
783
|
+
typeof ci.proofWording === "object" &&
|
|
784
|
+
!Array.isArray(ci.proofWording)
|
|
785
|
+
? ci.proofWording
|
|
786
|
+
: {};
|
|
787
|
+
const next = { ...existing };
|
|
788
|
+
if (keyMetrics !== undefined)
|
|
789
|
+
next.keyMetrics = keyMetrics;
|
|
790
|
+
if (miniStories !== undefined)
|
|
791
|
+
next.miniStories = miniStories;
|
|
792
|
+
if (forbiddenWords !== undefined)
|
|
793
|
+
next.forbiddenWords = forbiddenWords;
|
|
794
|
+
return { ...ci, proofWording: next };
|
|
795
|
+
}, "Proof wording patch failed against v2 schema.");
|
|
607
796
|
}
|
|
608
797
|
catch (error) {
|
|
609
798
|
return handleToolError(error);
|
|
@@ -637,20 +826,3 @@ export function registerDataRoomTools(server) {
|
|
|
637
826
|
}
|
|
638
827
|
});
|
|
639
828
|
}
|
|
640
|
-
// ─── Backend TODO ──────────────────────────────────────────────────────────
|
|
641
|
-
// The Leadify backend currently runs the data-room companyInfo schema with
|
|
642
|
-
// .parse(), so any payload with N validation errors only surfaces the FIRST
|
|
643
|
-
// one as a 422. That forces clients (and LLMs) into a death loop of
|
|
644
|
-
// "fix one field, retry, discover next error, retry…".
|
|
645
|
-
//
|
|
646
|
-
// Fix on the backend side (apps/server most likely):
|
|
647
|
-
// const result = companyInfoSchema.safeParse(input);
|
|
648
|
-
// if (!result.success) {
|
|
649
|
-
// return res.status(422).json({
|
|
650
|
-
// error: "Validation failed",
|
|
651
|
-
// issues: result.error.issues, // ALL of them, not just the first
|
|
652
|
-
// });
|
|
653
|
-
// }
|
|
654
|
-
//
|
|
655
|
-
// Once that ships, the MCP can pass the full issue list through verbatim
|
|
656
|
-
// instead of relying on the mirror-side validation as a fallback.
|