@agifyai/leadify-mcp 1.4.2 → 1.5.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 +133 -25
- package/dist/tools/dataroom.js +764 -18
- package/dist/types.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
# Leadify MCP Server
|
|
2
2
|
|
|
3
|
-
Serveur MCP (Model Context Protocol) pour l'API Leadify. Expose
|
|
3
|
+
Serveur MCP (Model Context Protocol) pour l'API Leadify. Expose les endpoints REST de Leadify sous forme de tools utilisables depuis Claude Desktop, Claude Code, Cursor ou tout client compatible MCP.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Package npm : [`@agifyai/leadify-mcp`](https://www.npmjs.com/package/@agifyai/leadify-mcp)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Pour les utilisateurs
|
|
10
|
+
|
|
11
|
+
Aucun clone, aucun build. `npx` télécharge la dernière version à chaque démarrage de session MCP.
|
|
12
|
+
|
|
13
|
+
### Pré-requis
|
|
14
|
+
|
|
15
|
+
- [Node.js](https://nodejs.org/) ≥ 18 (`node --version` pour vérifier)
|
|
16
|
+
- Une clé API Leadify (demander à l'équipe ou la générer dans l'app)
|
|
17
|
+
|
|
18
|
+
### Claude Code
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
claude mcp add leadify -e LEADIFY_API_KEY=votre-clé-api -- npx -y @agifyai/leadify-mcp@latest
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> Le `--` est nécessaire pour que `claude mcp add` ne tente pas d'interpréter le `-y` de `npx` comme une de ses propres options.
|
|
25
|
+
|
|
26
|
+
Vérifier que c'est bien branché :
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
claude mcp list
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Tu dois voir `leadify` dans la liste. Dans une session Claude Code, demande "appelle test_api_key" pour valider.
|
|
8
33
|
|
|
9
34
|
### Claude Desktop
|
|
10
35
|
|
|
11
36
|
Ouvrir le fichier de configuration :
|
|
37
|
+
|
|
12
38
|
- **macOS** : `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
13
39
|
- **Windows** : `%APPDATA%\Claude\claude_desktop_config.json`
|
|
14
40
|
|
|
15
|
-
Ajouter une entrée `
|
|
41
|
+
Ajouter une entrée `leadify` dans `mcpServers` :
|
|
16
42
|
|
|
17
43
|
```json
|
|
18
44
|
{
|
|
@@ -28,21 +54,32 @@ Ajouter une entrée `"leadify"` dans `"mcpServers"` :
|
|
|
28
54
|
}
|
|
29
55
|
```
|
|
30
56
|
|
|
31
|
-
Redémarrer Claude Desktop. Les tools apparaissent (icône marteau).
|
|
57
|
+
Redémarrer Claude Desktop. Les tools Leadify apparaissent (icône marteau dans la zone de saisie).
|
|
32
58
|
|
|
33
|
-
###
|
|
59
|
+
### Mise à jour automatique
|
|
60
|
+
|
|
61
|
+
Le tag `@latest` force `npx` à vérifier la dernière version publiée à chaque lancement de session. Quand un nouveau tool est mergé sur `main` et publié, il est dispo dès la **session suivante** — sans `git pull`, sans rebuild, sans rien.
|
|
62
|
+
|
|
63
|
+
Pour forcer un rafraîchissement immédiat sans attendre le cache npm :
|
|
34
64
|
|
|
35
65
|
```bash
|
|
36
|
-
|
|
66
|
+
npm cache clean --force
|
|
37
67
|
```
|
|
38
68
|
|
|
39
|
-
###
|
|
69
|
+
### Dépannage
|
|
40
70
|
|
|
41
|
-
|
|
71
|
+
| Symptôme | Cause / solution |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `LEADIFY_API_KEY environment variable is required` | La clé n'est pas passée. Vérifier le `-e` (Claude Code) ou le bloc `env` (Claude Desktop). |
|
|
74
|
+
| `401 Unauthorized` sur tous les tools | Clé API invalide ou révoquée. Tester avec `test_api_key`. |
|
|
75
|
+
| Le serveur ne démarre pas | Vérifier que `npx -y @agifyai/leadify-mcp@latest` tourne en standalone. Si erreur réseau, vérifier l'accès à `registry.npmjs.org`. |
|
|
76
|
+
| Tool ajouté côté équipe mais pas visible chez moi | Quitter complètement le client (Claude Desktop : ⌘Q ; Claude Code : ferme la session) et relancer. |
|
|
42
77
|
|
|
43
|
-
|
|
78
|
+
---
|
|
44
79
|
|
|
45
|
-
|
|
80
|
+
## 🛠️ Pour les contributeurs
|
|
81
|
+
|
|
82
|
+
### Setup local
|
|
46
83
|
|
|
47
84
|
```bash
|
|
48
85
|
git clone git@github.com:AgifyAI/mcp_leadify.git
|
|
@@ -51,20 +88,91 @@ npm install
|
|
|
51
88
|
npm run build
|
|
52
89
|
```
|
|
53
90
|
|
|
54
|
-
|
|
91
|
+
Brancher Claude Code sur ta build locale (en plus de la version npm si tu veux comparer) :
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add leadify-dev -e LEADIFY_API_KEY=votre-clé-api -- node /chemin/absolu/vers/mcp_leadify/dist/index.js
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Mode watch :
|
|
55
98
|
|
|
56
99
|
```bash
|
|
57
|
-
|
|
100
|
+
npm run dev
|
|
58
101
|
```
|
|
59
102
|
|
|
103
|
+
### Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
src/
|
|
107
|
+
├── index.ts # entrée stdio (shebang + transport)
|
|
108
|
+
├── server.ts # création du McpServer + register* de chaque module
|
|
109
|
+
├── client.ts # LeadifyClient (singleton, lit LEADIFY_API_KEY)
|
|
110
|
+
├── types.ts # toolResult, handleToolError, LeadifyApiError
|
|
111
|
+
└── tools/
|
|
112
|
+
├── auth.ts
|
|
113
|
+
├── leads.ts
|
|
114
|
+
├── campaigns.ts
|
|
115
|
+
├── dataroom.ts
|
|
116
|
+
├── personas.ts
|
|
117
|
+
└── ... # un fichier = un domaine fonctionnel
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Un tool = un appel à `server.tool(name, description, zodSchema, async handler)`. Voir `src/tools/auth.ts` pour le plus simple.
|
|
121
|
+
|
|
122
|
+
### Ajouter un tool
|
|
123
|
+
|
|
124
|
+
1. Coder le tool dans le fichier de domaine pertinent (`src/tools/<domaine>.ts`), ou créer un nouveau fichier.
|
|
125
|
+
2. Si nouveau fichier : exporter `registerXxxTools(server)` et l'appeler depuis `src/server.ts`.
|
|
126
|
+
3. Vérifier que ça compile :
|
|
127
|
+
```bash
|
|
128
|
+
npm run build
|
|
129
|
+
```
|
|
130
|
+
4. Tester en local (cf. setup ci-dessus).
|
|
131
|
+
5. Bumper la version et publier (cf. section suivante).
|
|
132
|
+
|
|
60
133
|
### Publier une nouvelle version
|
|
61
134
|
|
|
135
|
+
Le publish est automatique via GitHub Actions sur push `main`. Le workflow ne fait rien si la version dans `package.json` n'a pas bougé — il faut donc bumper avant de push.
|
|
136
|
+
|
|
62
137
|
```bash
|
|
63
|
-
npm version patch
|
|
138
|
+
npm version patch # 1.4.2 → 1.4.3 (bug fix, ajout de tool)
|
|
139
|
+
npm version minor # 1.4.2 → 1.5.0 (changement non-breaking notable)
|
|
140
|
+
npm version major # 1.4.2 → 2.0.0 (breaking : tool renommé / supprimé / signature changée)
|
|
64
141
|
git push && git push --tags
|
|
65
142
|
```
|
|
66
143
|
|
|
67
|
-
|
|
144
|
+
`npm version` crée un commit + un tag git automatiquement.
|
|
145
|
+
|
|
146
|
+
Vérifier la publication :
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm view @agifyai/leadify-mcp version
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Et le run du workflow : https://github.com/AgifyAI/mcp_leadify/actions
|
|
153
|
+
|
|
154
|
+
### Comment marche la CI
|
|
155
|
+
|
|
156
|
+
`.github/workflows/publish.yml` se déclenche sur push `main` :
|
|
157
|
+
|
|
158
|
+
1. Checkout + install Node 20.
|
|
159
|
+
2. `npm ci` pour installer les deps.
|
|
160
|
+
3. Check si la version actuelle de `package.json` est déjà publiée sur npm. Si oui, skip silencieusement (on évite les doublons et on permet de push des commits non-version sur `main`).
|
|
161
|
+
4. Sinon, upgrade npm vers ≥ 11.5.1 (requis pour Trusted Publishing) puis `npm publish`.
|
|
162
|
+
|
|
163
|
+
L'authentification npm passe par **Trusted Publishing (OIDC)** : pas de token stocké, GitHub Actions s'authentifie directement auprès de npm via la permission `id-token: write` du workflow. La trust relation est configurée sur la [page npm du package](https://www.npmjs.com/package/@agifyai/leadify-mcp/access) (Trusted Publisher : `AgifyAI/mcp_leadify` / `publish.yml`).
|
|
164
|
+
|
|
165
|
+
Conséquences pratiques :
|
|
166
|
+
- Pas de secret `NPM_TOKEN` à rotater.
|
|
167
|
+
- Le workflow ne peut publier que depuis ce repo + ce fichier de workflow exact. Renommer `publish.yml` casse la trust → mettre à jour côté npm si besoin.
|
|
168
|
+
|
|
169
|
+
### Conventions
|
|
170
|
+
|
|
171
|
+
- **Versionning** : suivre semver. Ajout de tool = `patch` (rétrocompatible). Renommage / suppression / signature breaking = `major`.
|
|
172
|
+
- **Description des tools** : verbeuse et précise — c'est ce que le modèle lit pour décider d'utiliser le tool. Voir les tools `outreach_*` pour des exemples détaillés.
|
|
173
|
+
- **Erreurs** : toujours wrapper le handler dans `try / catch` et retourner `handleToolError(error)` en cas d'échec — ça normalise les erreurs API en réponse MCP propre.
|
|
174
|
+
|
|
175
|
+
---
|
|
68
176
|
|
|
69
177
|
## Tools disponibles
|
|
70
178
|
|
|
@@ -89,8 +197,16 @@ GitHub Actions build et publie sur npm automatiquement (workflow `.github/workfl
|
|
|
89
197
|
| `export_campaign` | Exporter les statistiques complètes d'une campagne en CSV. |
|
|
90
198
|
| `add_signal` | Ajouter un signal de business intelligence à un lead (INFO, CRITICAL, GOLDEN). |
|
|
91
199
|
| `add_activity` | Journaliser une interaction prospect (LinkedIn, email, call) dans le feed du lead. |
|
|
92
|
-
| `get_data_room` | Récupérer la data room complète : infos
|
|
93
|
-
| `
|
|
200
|
+
| `get_data_room` | Récupérer la data room complète : infos société (v2), documents, personas. |
|
|
201
|
+
| `describe_company_info_schema` | Lire le schéma companyInfo v2 mirroré côté MCP (enums, max-lengths, sections, patch tool par section). À appeler avant la première update. |
|
|
202
|
+
| `update_data_room` | Update WHOLESALE (escape hatch) — remplace l'intégralité de companyInfo. Requis pour le bootstrap initial. Préférer les tools granulaires pour l'incrémental. |
|
|
203
|
+
| `update_company_info_identity` | Patch name + website. Reads + merges + PUTs le full v2 payload. |
|
|
204
|
+
| `update_company_info_snapshot` | Patch du bloc snapshot (pitchOneLiner, marketSpecialty, stage, salesTeamSizeFR, geo, constraints). geo merge shallow. |
|
|
205
|
+
| `update_company_info_constraint` | Add / replace / remove une seule contrainte dans snapshot.constraints[]. label + type enum + note≤150. Cap 10. |
|
|
206
|
+
| `update_company_info_product` | Add / replace / remove un seul produit dans products[]. category enum strict + regulatory sub-block. Cap 5. |
|
|
207
|
+
| `update_company_info_economics` | Patch du bloc economics (pricingModel, ticketRange, salesCycleMonths, triggers, defaultBaseline). |
|
|
208
|
+
| `update_company_info_product_icp` | Add / replace / remove un seul ICP dans productICPs[]. Lookup par productId (must match a products[].name). Cap 5. |
|
|
209
|
+
| `update_company_info_proof_wording` | Patch du bloc proofWording (keyMetrics, miniStories, forbiddenWords). |
|
|
94
210
|
| `add_data_room_document` | Ajouter un document textuel à la data room avec catégorie. |
|
|
95
211
|
| `upsert_persona` | Créer ou mettre à jour un persona (ciblage, messaging, outils actifs). |
|
|
96
212
|
| `get_persona` | Récupérer un persona par son ID. |
|
|
@@ -107,11 +223,3 @@ GitHub Actions build et publie sur npm automatiquement (workflow `.github/workfl
|
|
|
107
223
|
| `update_outreach_case_study` | Ajouter / remplacer / supprimer un case study par index, sans re-envoyer la liste. |
|
|
108
224
|
| `update_outreach_urls` | Patch booking_url et/ou website_url uniquement. |
|
|
109
225
|
| `set_outreach_connection_request` | Toggle du flag connectionRequestEnabled (LinkedIn invite vs cold DM). |
|
|
110
|
-
|
|
111
|
-
## Développement
|
|
112
|
-
|
|
113
|
-
Mode watch pour le développement :
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
npm run dev
|
|
117
|
-
```
|
package/dist/tools/dataroom.js
CHANGED
|
@@ -1,6 +1,158 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getClient } from "../client.js";
|
|
3
|
-
import { toolResult, handleToolError } from "../types.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) ───────────────────────────────────────────
|
|
4
156
|
const DOCUMENT_CATEGORIES = [
|
|
5
157
|
"PRODUCT_CATALOG",
|
|
6
158
|
"CONGRESS_LIST",
|
|
@@ -10,48 +162,642 @@ const DOCUMENT_CATEGORIES = [
|
|
|
10
162
|
"CAMPAIGN_BRIEF",
|
|
11
163
|
"OTHER",
|
|
12
164
|
];
|
|
165
|
+
// ─── Common descriptions ───────────────────────────────────────────────────
|
|
13
166
|
const ORG_ID_DESC = "Optional Clerk organization ID to target a specific org's data room. " +
|
|
14
167
|
"Defaults to the caller's own org. Requires admin access for writes.";
|
|
168
|
+
const DRY_RUN_DESC = "If true, validate the merged payload + return the preview WITHOUT writing. " +
|
|
169
|
+
"Use to dry-fit a change before persisting.";
|
|
170
|
+
async function fetchDataRoom(organizationId) {
|
|
171
|
+
const params = organizationId
|
|
172
|
+
? new URLSearchParams({ organizationId })
|
|
173
|
+
: undefined;
|
|
174
|
+
const data = (await getClient().get("/api/data-room", params));
|
|
175
|
+
return data ?? {};
|
|
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
|
+
}
|
|
183
|
+
async function putCompanyInfo(companyInfo, organizationId) {
|
|
184
|
+
const body = { companyInfo };
|
|
185
|
+
if (organizationId)
|
|
186
|
+
body.organizationId = organizationId;
|
|
187
|
+
return getClient().put("/api/data-room", body);
|
|
188
|
+
}
|
|
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 });
|
|
202
|
+
}
|
|
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) {
|
|
215
|
+
return toolResult({
|
|
216
|
+
ok: true,
|
|
217
|
+
dry_run: true,
|
|
218
|
+
preview_company_info: merged,
|
|
219
|
+
note: "No write performed. Re-call without dry_run to persist.",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
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);
|
|
238
|
+
}
|
|
239
|
+
// ─── Tool registrations ────────────────────────────────────────────────────
|
|
15
240
|
export function registerDataRoomTools(server) {
|
|
16
241
|
// ── get_data_room ──────────────────────────────────────────────────────
|
|
17
|
-
server.tool("get_data_room", "Retrieve the organization's full data room: company info, all documents, and
|
|
18
|
-
"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 " +
|
|
19
244
|
"understand the company's context before writing campaigns or messages.", {
|
|
20
245
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
21
246
|
}, async ({ organization_id }) => {
|
|
22
247
|
try {
|
|
23
|
-
const
|
|
24
|
-
? new URLSearchParams({ organizationId: organization_id })
|
|
25
|
-
: undefined;
|
|
26
|
-
const data = await getClient().get("/api/data-room", params);
|
|
248
|
+
const data = await fetchDataRoom(organization_id);
|
|
27
249
|
return toolResult(data);
|
|
28
250
|
}
|
|
29
251
|
catch (error) {
|
|
30
252
|
return handleToolError(error);
|
|
31
253
|
}
|
|
32
254
|
});
|
|
255
|
+
// ── describe_company_info_schema ───────────────────────────────────────
|
|
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 () => {
|
|
260
|
+
try {
|
|
261
|
+
return toolResult({
|
|
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",
|
|
301
|
+
},
|
|
302
|
+
products: {
|
|
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}}.",
|
|
304
|
+
patch_tool: "update_company_info_product",
|
|
305
|
+
},
|
|
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",
|
|
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).",
|
|
332
|
+
},
|
|
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.",
|
|
337
|
+
],
|
|
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
|
+
},
|
|
342
|
+
notes: [
|
|
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.",
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return handleToolError(error);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
33
352
|
// ── update_data_room ───────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.", {
|
|
38
361
|
company_info: z
|
|
39
362
|
.record(z.unknown())
|
|
40
|
-
.describe("
|
|
41
|
-
"size, website, products, valueProposition, competitors, targetRegions, sender."),
|
|
363
|
+
.describe("Full v2 companyInfo payload (replaces the existing one). See describe_company_info_schema for the contract."),
|
|
42
364
|
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
43
|
-
|
|
365
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
366
|
+
}, async ({ company_info, organization_id, dry_run }) => {
|
|
44
367
|
try {
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
371
|
+
if (dry_run) {
|
|
372
|
+
return dryRunResult(company_info);
|
|
373
|
+
}
|
|
374
|
+
const data = await putCompanyInfo(company_info, organization_id);
|
|
49
375
|
return toolResult(data);
|
|
50
376
|
}
|
|
51
377
|
catch (error) {
|
|
52
378
|
return handleToolError(error);
|
|
53
379
|
}
|
|
54
380
|
});
|
|
381
|
+
// ── update_company_info_identity ───────────────────────────────────────
|
|
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)
|
|
393
|
+
.optional()
|
|
394
|
+
.describe("Company website URL (max 500)."),
|
|
395
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
396
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
397
|
+
}, async ({ name, website, organization_id, dry_run }) => {
|
|
398
|
+
try {
|
|
399
|
+
if (name === undefined && website === undefined) {
|
|
400
|
+
return toolError("Provide at least one of: name, website.");
|
|
401
|
+
}
|
|
402
|
+
return await mergeAndPut(organization_id, dry_run, (ci) => {
|
|
403
|
+
const out = { ...ci };
|
|
404
|
+
if (name !== undefined)
|
|
405
|
+
out.name = name;
|
|
406
|
+
if (website !== undefined)
|
|
407
|
+
out.website = website;
|
|
408
|
+
return out;
|
|
409
|
+
}, "Identity patch failed against v2 schema.");
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
return handleToolError(error);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
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."),
|
|
455
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
456
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
457
|
+
}, async ({ pitchOneLiner, marketSpecialty, stage, salesTeamSizeFR, geo, constraints, organization_id, dry_run, }) => {
|
|
458
|
+
try {
|
|
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.");
|
|
466
|
+
}
|
|
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.");
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
return handleToolError(error);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
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()
|
|
509
|
+
.optional()
|
|
510
|
+
.describe("Zero-based index. Required for 'replace' and 'remove'."),
|
|
511
|
+
constraint: constraintSchema
|
|
512
|
+
.optional()
|
|
513
|
+
.describe("Constraint payload {label ≤150, type ∈ regulatory|brand|legal, note? ≤150}. Required for 'add' and 'replace'."),
|
|
514
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
515
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
516
|
+
}, async ({ action, index, constraint, organization_id, dry_run }) => {
|
|
517
|
+
try {
|
|
518
|
+
if ((action === "add" || action === "replace") && !constraint) {
|
|
519
|
+
return toolError(`constraint is required for action '${action}'.`);
|
|
520
|
+
}
|
|
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.");
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
return handleToolError(error);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
// ── update_company_info_product ────────────────────────────────────────
|
|
562
|
+
server.tool("update_company_info_product", "Add, replace, or remove a single product in companyInfo.products[] without re-sending " +
|
|
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.", {
|
|
569
|
+
action: z
|
|
570
|
+
.enum(["add", "replace", "remove"])
|
|
571
|
+
.describe("add = append (fails if name exists or list at cap 5); replace = overwrite by name; remove = delete by name."),
|
|
572
|
+
name: z
|
|
573
|
+
.string()
|
|
574
|
+
.optional()
|
|
575
|
+
.describe("Lookup key for replace/remove. Required for those actions. For 'add' the name comes from the product payload."),
|
|
576
|
+
product: productSchema
|
|
577
|
+
.optional()
|
|
578
|
+
.describe("Product payload. Required for 'add' and 'replace'."),
|
|
579
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
580
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
581
|
+
}, async ({ action, name, product, organization_id, dry_run }) => {
|
|
582
|
+
try {
|
|
583
|
+
if ((action === "add" || action === "replace") && !product) {
|
|
584
|
+
return toolError(`product is required for action '${action}'.`);
|
|
585
|
+
}
|
|
586
|
+
if ((action === "replace" || action === "remove") && !name) {
|
|
587
|
+
return toolError(`name is required for action '${action}'.`);
|
|
588
|
+
}
|
|
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);
|
|
602
|
+
}
|
|
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;
|
|
609
|
+
}
|
|
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);
|
|
618
|
+
}
|
|
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.");
|
|
669
|
+
}
|
|
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.");
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
return handleToolError(error);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
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]}}.", {
|
|
697
|
+
action: z
|
|
698
|
+
.enum(["add", "replace", "remove"])
|
|
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()
|
|
702
|
+
.optional()
|
|
703
|
+
.describe("Lookup key for replace/remove. For 'add' the productId comes from the icp payload."),
|
|
704
|
+
icp: productICPSchema
|
|
705
|
+
.optional()
|
|
706
|
+
.describe("ICP payload. Required for 'add' and 'replace'."),
|
|
707
|
+
organization_id: z.string().optional().describe(ORG_ID_DESC),
|
|
708
|
+
dry_run: z.boolean().optional().describe(DRY_RUN_DESC),
|
|
709
|
+
}, async ({ action, productId, icp, organization_id, dry_run }) => {
|
|
710
|
+
try {
|
|
711
|
+
if ((action === "add" || action === "replace") && !icp) {
|
|
712
|
+
return toolError(`icp is required for action '${action}'.`);
|
|
713
|
+
}
|
|
714
|
+
if ((action === "replace" || action === "remove") && !productId) {
|
|
715
|
+
return toolError(`productId is required for action '${action}'.`);
|
|
716
|
+
}
|
|
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);
|
|
730
|
+
}
|
|
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);
|
|
746
|
+
}
|
|
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.");
|
|
780
|
+
}
|
|
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.");
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
return handleToolError(error);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
55
801
|
// ── add_data_room_document ─────────────────────────────────────────────
|
|
56
802
|
server.tool("add_data_room_document", "Add a textual/markdown document to the data room under a predefined category. " +
|
|
57
803
|
"Use this to store product catalogs, congress lists, target lists, competitor briefs, " +
|
package/dist/types.js
CHANGED
|
@@ -29,6 +29,17 @@ export function toolError(message, details) {
|
|
|
29
29
|
}
|
|
30
30
|
export function handleToolError(error) {
|
|
31
31
|
if (error instanceof LeadifyApiError) {
|
|
32
|
+
// If the backend returned a cumulative ZodIssue list (formatZodError now
|
|
33
|
+
// emits `issues[]` on every 422), surface the count up-front in the
|
|
34
|
+
// message so the caller can fix every field in a single retry instead
|
|
35
|
+
// of looping one-error-at-a-time on the legacy `error` field.
|
|
36
|
+
const body = error.responseBody;
|
|
37
|
+
if (body && typeof body === "object" && !Array.isArray(body) && "issues" in body) {
|
|
38
|
+
const issues = body.issues;
|
|
39
|
+
if (Array.isArray(issues) && issues.length > 1) {
|
|
40
|
+
return toolError(`Validation failed: ${issues.length} issues. See details.response.issues[] for every path that needs fixing.`, { statusCode: error.statusCode, response: body });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
32
43
|
return toolError(error.message, {
|
|
33
44
|
statusCode: error.statusCode,
|
|
34
45
|
response: error.responseBody,
|