aicrm-mcp 0.2.0 → 0.2.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 +2 -2
- package/dist/cli.js +103 -21
- package/package.json +11 -4
- package/skill/aicrm/SKILL.md +8 -4
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ Outros clientes:
|
|
|
26
26
|
| `aicrm-mcp skill [install] [--project \| --dir <pasta>]` | Imprime ou instala o SKILL.md para agentes |
|
|
27
27
|
| `aicrm-mcp` | Inicia o servidor MCP (stdio) |
|
|
28
28
|
|
|
29
|
-
## Ferramentas (
|
|
29
|
+
## Ferramentas (44)
|
|
30
30
|
|
|
31
|
-
- **Leads**: `list_workspaces`, `search_leads`, `get_lead`, `create_lead`, `update_lead`, `move_lead`, `mark_won`, `mark_lost`, `reopen_lead`, `add_note`, `bulk_update_leads`, `delete_lead`
|
|
31
|
+
- **Leads**: `list_workspaces`, `search_leads`, `get_lead`, `create_lead`, `update_lead`, `move_lead`, `mark_won`, `mark_lost`, `reopen_lead`, `handoff_lead`, `add_note`, `bulk_update_leads`, `delete_lead`
|
|
32
32
|
- **Importação**: `import_leads_csv` (`dry_run`, `columns`, `template_name`), `rename_import_template`, `delete_import_template`
|
|
33
33
|
- **Campos**: `list_fields`, `create_field`, `update_field`, `archive_field`, `set_standard_fields`
|
|
34
34
|
- **Metas**: `get_dashboard`, `get_forecast`, `get_goals`, `set_goals`, `list_commission_plans`, `save_commission_plan`, `delete_commission_plan`
|
package/dist/cli.js
CHANGED
|
@@ -314,7 +314,9 @@ var workspaceSchema = z.object({
|
|
|
314
314
|
var workspaceUpdateSchema = z.object({
|
|
315
315
|
name: z.string().trim().min(1, "Informe um nome").max(80).optional(),
|
|
316
316
|
visibility: z.enum(VISIBILITIES).optional(),
|
|
317
|
-
hiddenFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional()
|
|
317
|
+
hiddenFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional(),
|
|
318
|
+
/** Raster data URL (PNG/JPEG/WebP), or null to remove. SVG is rejected: the browser rasterizes it first. */
|
|
319
|
+
logo: z.string().regex(/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/, "Envie uma imagem PNG, JPG ou WebP").max(4e5, "Imagem muito grande (m\xE1x. ~300 KB)").nullable().optional()
|
|
318
320
|
});
|
|
319
321
|
var fieldOptions = z.array(z.string().trim().min(1).max(60)).max(100, "No m\xE1ximo 100 op\xE7\xF5es").transform((o) => [...new Set(o)]);
|
|
320
322
|
var leadFieldSchema = z.object({
|
|
@@ -344,13 +346,15 @@ var funnelSchema = z.object({
|
|
|
344
346
|
goalKpi: z.enum(GOAL_KPIS).default("revenue"),
|
|
345
347
|
goalStageIndex: z.number().int().min(0).optional(),
|
|
346
348
|
template: z.enum(Object.keys(FUNNEL_TEMPLATES)).optional(),
|
|
347
|
-
stages: z.array(stageInputSchema).min(1).max(20).optional()
|
|
349
|
+
stages: z.array(stageInputSchema).min(1).max(20).optional(),
|
|
350
|
+
nextFunnelId: id.nullish()
|
|
348
351
|
});
|
|
349
352
|
var funnelUpdateSchema = z.object({
|
|
350
353
|
name: z.string().trim().min(1).max(80).optional(),
|
|
351
354
|
goalKpi: z.enum(GOAL_KPIS).optional(),
|
|
352
355
|
goalStageId: id.nullable().optional(),
|
|
353
356
|
stages: z.array(stageInputSchema).min(1).max(20).optional(),
|
|
357
|
+
nextFunnelId: id.nullable().optional(),
|
|
354
358
|
archived: z.boolean().optional()
|
|
355
359
|
});
|
|
356
360
|
var optionalText = (max) => z.string().trim().max(max).nullish().transform((v) => v ? v : null);
|
|
@@ -375,9 +379,16 @@ var moveLeadSchema = z.object({
|
|
|
375
379
|
funnelId: id.optional(),
|
|
376
380
|
stageId: id
|
|
377
381
|
});
|
|
382
|
+
var handoffSchema = z.object({
|
|
383
|
+
funnelId: id,
|
|
384
|
+
stageId: id.optional(),
|
|
385
|
+
ownerId: id.nullish(),
|
|
386
|
+
valueCents: z.number().int().min(0).max(1e13).optional()
|
|
387
|
+
});
|
|
378
388
|
var leadStatusSchema = z.discriminatedUnion("status", [
|
|
379
|
-
|
|
380
|
-
z.object({ status: z.literal("
|
|
389
|
+
/** undoHandoff also deletes continuations nobody has touched yet (used by the "Desfazer" of a win). */
|
|
390
|
+
z.object({ status: z.literal("open"), undoHandoff: z.boolean().optional() }),
|
|
391
|
+
z.object({ status: z.literal("won"), valueCents: z.number().int().min(0).optional(), handoff: handoffSchema.optional() }),
|
|
381
392
|
z.object({ status: z.literal("lost"), lostReason: z.enum(LOST_REASONS), note: z.string().max(500).optional() })
|
|
382
393
|
]);
|
|
383
394
|
var noteSchema = z.object({ body: z.string().trim().min(1).max(5e3) });
|
|
@@ -740,6 +751,20 @@ var targetLabel = (fields, t) => {
|
|
|
740
751
|
return p.kind === "std" ? CSV_FIELD_LABEL[p.field] : fields.find((f) => f.id === p.fieldId)?.label ?? "campo removido";
|
|
741
752
|
};
|
|
742
753
|
var findUser = (c, ref) => c.users.find((u) => norm(u.email) === norm(ref)) ?? resolve(c.users, ref, "Usu\xE1rio");
|
|
754
|
+
var isNoOwner = (ref) => ["none", "ninguem", "sem responsavel"].includes(norm(ref));
|
|
755
|
+
function handoffBody(c, source, a) {
|
|
756
|
+
const funnel = pickFunnel(c, a.funnel, a.workspace ?? source.workspaceId);
|
|
757
|
+
const stage = resolve(funnel.stages, a.stage, "Etapa");
|
|
758
|
+
return {
|
|
759
|
+
funnel,
|
|
760
|
+
body: {
|
|
761
|
+
funnelId: funnel.id,
|
|
762
|
+
...stage ? { stageId: stage.id } : {},
|
|
763
|
+
...a.owner ? { ownerId: isNoOwner(a.owner) ? null : findUser(c, a.owner).id } : {},
|
|
764
|
+
...a.value != null ? { valueCents: Math.round(a.value * 100) } : {}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
743
768
|
var MONTH = /^\d{4}-\d{2}$/;
|
|
744
769
|
|
|
745
770
|
// src/tools/admin.ts
|
|
@@ -981,11 +1006,33 @@ function registerLeadTools(server) {
|
|
|
981
1006
|
return text({ reopened: leadSummary(c, lead) });
|
|
982
1007
|
})
|
|
983
1008
|
);
|
|
1009
|
+
server.registerTool(
|
|
1010
|
+
"handoff_lead",
|
|
1011
|
+
{
|
|
1012
|
+
title: "Enviar lead ganho para outro funil",
|
|
1013
|
+
description: "Continua um lead GANHO em outro funil (ex.: de Aquisi\xE7\xE3o para Relacionamento/p\xF3s-venda): cria um lead novo, aberto e ligado ao original, na etapa escolhida (padr\xE3o: a primeira). O lead ganho fica onde est\xE1 e continua contando na meta e na comiss\xE3o. O novo lead copia contato e campos, come\xE7a com valor R$ 0 (informe value se j\xE1 houver nova venda prevista) e mant\xE9m o respons\xE1vel (ou use owner). S\xF3 funciona para leads com status won; cada lead s\xF3 pode continuar uma vez em cada funil.",
|
|
1014
|
+
inputSchema: {
|
|
1015
|
+
lead_id: z4.string().uuid().describe("Lead ganho"),
|
|
1016
|
+
funnel: z4.string().describe("Funil onde o cliente continua (nome ou id)"),
|
|
1017
|
+
stage: z4.string().optional().describe("Etapa de entrada (padr\xE3o: a primeira)"),
|
|
1018
|
+
workspace: z4.string().optional().describe("Workspace do funil, se for outro (s\xF3 admins)"),
|
|
1019
|
+
owner: z4.string().optional().describe('Respons\xE1vel no novo funil (nome ou email; "none" = sem respons\xE1vel). Padr\xE3o: o mesmo do lead ganho'),
|
|
1020
|
+
value: z4.number().min(0).optional().describe("Valor em reais no novo funil (padr\xE3o: 0)")
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
withApi(async (api, a) => {
|
|
1024
|
+
const c = await ctx(api);
|
|
1025
|
+
const source = (await api.get(`/leads/${a.lead_id}`)).lead;
|
|
1026
|
+
const { body } = handoffBody(c, source, a);
|
|
1027
|
+
const r = await api.post(`/leads/${a.lead_id}/handoff`, body);
|
|
1028
|
+
return text({ won: leadSummary(c, r.source), continuation: leadSummary(c, r.lead) });
|
|
1029
|
+
})
|
|
1030
|
+
);
|
|
984
1031
|
server.registerTool(
|
|
985
1032
|
"bulk_update_leads",
|
|
986
1033
|
{
|
|
987
1034
|
title: "Atualizar leads em massa",
|
|
988
|
-
description: "Reatribui (s\xF3 admins) e/ou move at\xE9 1000 leads de uma vez. Pegue os ids com search_leads. Para mover, informe stage (e funnel, se for outro funil; sem funnel, vale o funil do primeiro lead). Leads que voc\xEA n\xE3o pode editar s\xE3o pulados.",
|
|
1035
|
+
description: "Reatribui (s\xF3 admins) e/ou move at\xE9 1000 leads de uma vez. Pegue os ids com search_leads. Para mover, informe stage (e funnel, se for outro funil; sem funnel, vale o funil do primeiro lead). Leads que voc\xEA n\xE3o pode editar s\xE3o pulados, assim como leads ganhos numa troca de funil (use handoff_lead para eles).",
|
|
989
1036
|
inputSchema: {
|
|
990
1037
|
lead_ids: z4.array(z4.string().uuid()).min(1).max(1e3),
|
|
991
1038
|
owner: z4.string().optional().describe("Nome ou email do novo respons\xE1vel"),
|
|
@@ -1036,12 +1083,15 @@ var stageInput = z5.object({
|
|
|
1036
1083
|
probability: z5.number().int().min(0).max(100).describe("Chance (%) de um lead nesta etapa virar venda"),
|
|
1037
1084
|
was: z5.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
|
|
1038
1085
|
});
|
|
1039
|
-
var describeFunnel = (f) => ({
|
|
1086
|
+
var describeFunnel = (f, ws) => ({
|
|
1040
1087
|
name: f.name,
|
|
1041
1088
|
goal_kpi: `${f.goalKpi} (${GOAL_KPI_LABEL[f.goalKpi]})`,
|
|
1042
1089
|
...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
|
|
1090
|
+
...f.nextFunnelId ? { after_won_continue_in: ws?.funnels.find((x) => x.id === f.nextFunnelId)?.name ?? f.nextFunnelId } : {},
|
|
1043
1091
|
stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
|
|
1044
1092
|
});
|
|
1093
|
+
var nextFunnelArg = z5.string().nullable().optional().describe('Funil (do mesmo workspace) onde o cliente continua depois de ganho, ex.: "Relacionamento". Sugerido no app ao marcar ganho. null ou "none" remove.');
|
|
1094
|
+
var nextFunnelId = (ws, ref) => ref === void 0 ? void 0 : ref === null || norm(ref) === "none" ? null : resolve(ws.funnels, ref, "Funil").id;
|
|
1045
1095
|
function registerStructureTools(server) {
|
|
1046
1096
|
server.registerTool(
|
|
1047
1097
|
"create_workspace",
|
|
@@ -1102,7 +1152,8 @@ function registerStructureTools(server) {
|
|
|
1102
1152
|
template: z5.enum(TEMPLATE_KEYS).optional(),
|
|
1103
1153
|
stages: z5.array(stageInput.omit({ was: true })).min(1).max(20).optional(),
|
|
1104
1154
|
goal_kpi: z5.enum(GOAL_KPIS).default("revenue"),
|
|
1105
|
-
goal_stage: z5.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)")
|
|
1155
|
+
goal_stage: z5.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)"),
|
|
1156
|
+
next_funnel: nextFunnelArg
|
|
1106
1157
|
}
|
|
1107
1158
|
},
|
|
1108
1159
|
withApi(
|
|
@@ -1116,9 +1167,10 @@ function registerStructureTools(server) {
|
|
|
1116
1167
|
name: a.name,
|
|
1117
1168
|
goalKpi: a.goal_kpi,
|
|
1118
1169
|
...a.stages ? { stages: a.stages } : { template: a.template ?? "sales" },
|
|
1119
|
-
...goalIdx >= 0 ? { goalStageIndex: goalIdx } : {}
|
|
1170
|
+
...goalIdx >= 0 ? { goalStageIndex: goalIdx } : {},
|
|
1171
|
+
...a.next_funnel ? { nextFunnelId: nextFunnelId(ws, a.next_funnel) } : {}
|
|
1120
1172
|
});
|
|
1121
|
-
return text({ workspace: ws.name, created: describeFunnel(f) });
|
|
1173
|
+
return text({ workspace: ws.name, created: describeFunnel(f, ws) });
|
|
1122
1174
|
}
|
|
1123
1175
|
)
|
|
1124
1176
|
);
|
|
@@ -1126,18 +1178,22 @@ function registerStructureTools(server) {
|
|
|
1126
1178
|
"update_funnel",
|
|
1127
1179
|
{
|
|
1128
1180
|
title: "Alterar funil",
|
|
1129
|
-
description: "Renomeia o funil, troca o KPI de meta ou redefine as etapas (s\xF3 admins). stages \xE9 a lista completa e na ordem: etapas com o mesmo nome (ou com was) s\xE3o mantidas, as novas s\xE3o criadas e as que ficarem de fora s\xE3o removidas \u2014 o que s\xF3 funciona se n\xE3o tiverem leads (mova antes com bulk_update_leads).",
|
|
1181
|
+
description: "Renomeia o funil, troca o KPI de meta ou redefine as etapas (s\xF3 admins). stages \xE9 a lista completa e na ordem: etapas com o mesmo nome (ou com was) s\xE3o mantidas, as novas s\xE3o criadas e as que ficarem de fora s\xE3o removidas \u2014 o que s\xF3 funciona se n\xE3o tiverem leads (mova antes com bulk_update_leads). next_funnel define para onde o cliente vai depois de ganho (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento).",
|
|
1130
1182
|
inputSchema: {
|
|
1131
1183
|
funnel: z5.string(),
|
|
1132
1184
|
workspace: z5.string().optional(),
|
|
1133
1185
|
name: z5.string().min(1).max(80).optional(),
|
|
1134
1186
|
goal_kpi: z5.enum(GOAL_KPIS).optional(),
|
|
1135
1187
|
goal_stage: z5.string().optional(),
|
|
1136
|
-
stages: z5.array(stageInput).min(1).max(20).optional()
|
|
1188
|
+
stages: z5.array(stageInput).min(1).max(20).optional(),
|
|
1189
|
+
next_funnel: nextFunnelArg
|
|
1137
1190
|
}
|
|
1138
1191
|
},
|
|
1139
1192
|
withApi(async (api, a) => {
|
|
1140
|
-
const
|
|
1193
|
+
const c = await ctx(api);
|
|
1194
|
+
const funnel = pickFunnel(c, a.funnel, a.workspace);
|
|
1195
|
+
const ws = c.workspaces.find((w) => w.id === funnel.workspaceId);
|
|
1196
|
+
const next = nextFunnelId(ws, a.next_funnel);
|
|
1141
1197
|
const stages = a.stages?.map((s) => {
|
|
1142
1198
|
const existing = funnel.stages.find((x) => norm(x.name) === norm(s.was ?? s.name));
|
|
1143
1199
|
return { ...existing ? { id: existing.id } : {}, name: s.name, probability: s.probability };
|
|
@@ -1145,13 +1201,14 @@ function registerStructureTools(server) {
|
|
|
1145
1201
|
let f = await api.patch(`/funnels/${funnel.id}`, {
|
|
1146
1202
|
...a.name ? { name: a.name } : {},
|
|
1147
1203
|
...a.goal_kpi ? { goalKpi: a.goal_kpi } : {},
|
|
1148
|
-
...stages ? { stages } : {}
|
|
1204
|
+
...stages ? { stages } : {},
|
|
1205
|
+
...next !== void 0 ? { nextFunnelId: next } : {}
|
|
1149
1206
|
});
|
|
1150
1207
|
if (a.goal_stage || a.goal_kpi === "stage_reached" && !f.goalStageId) {
|
|
1151
1208
|
const goal = a.goal_stage ? resolve(f.stages, a.goal_stage, "Etapa") : f.stages[f.stages.length - 1];
|
|
1152
1209
|
f = await api.patch(`/funnels/${funnel.id}`, { goalStageId: goal.id });
|
|
1153
1210
|
}
|
|
1154
|
-
return text({ updated: describeFunnel(f) });
|
|
1211
|
+
return text({ updated: describeFunnel(f, ws) });
|
|
1155
1212
|
})
|
|
1156
1213
|
);
|
|
1157
1214
|
server.registerTool(
|
|
@@ -1281,7 +1338,7 @@ async function startServer() {
|
|
|
1281
1338
|
const server = new McpServer(
|
|
1282
1339
|
{ name: "aicrm", version: "0.2.0" },
|
|
1283
1340
|
{
|
|
1284
|
-
instructions: "AICRM \xE9 um CRM de vendas. Workspaces cont\xEAm funis; funis cont\xEAm etapas e leads. Status do lead: open, won, lost. Workspaces, funis, etapas e vendedores podem ser referenciados por nome. Valores monet\xE1rios est\xE3o em reais (BRL). Use get_dashboard/get_forecast para metas e proje\xE7\xF5es estat\xEDsticas (Monte Carlo bayesiano). Cada workspace pode ter campos personalizados (ex.: Cidade, Porte) e esconder campos padr\xE3o: veja list_fields. Em create_lead/update_lead, preencha-os em custom_fields usando o nome do campo. Antes de importar um CSV desconhecido, rode import_leads_csv com dry_run=true para revisar o mapeamento de colunas. Admins tamb\xE9m gerenciam time (invite_user, update_user), estrutura (create_workspace, create_funnel, update_funnel), metas e comiss\xF5es (set_goals, save_commission_plan) e modo TV. O servidor aplica as permiss\xF5es do usu\xE1rio logado: se uma a\xE7\xE3o for negada, explique que precisa de um admin. A\xE7\xF5es destrutivas (delete_*, archive_field, revoke_*) exigem confirma\xE7\xE3o expl\xEDcita da pessoa antes."
|
|
1341
|
+
instructions: "AICRM \xE9 um CRM de vendas. Workspaces cont\xEAm funis; funis cont\xEAm etapas e leads. Status do lead: open, won, lost. Workspaces, funis, etapas e vendedores podem ser referenciados por nome. Valores monet\xE1rios est\xE3o em reais (BRL). Use get_dashboard/get_forecast para metas e proje\xE7\xF5es estat\xEDsticas (Monte Carlo bayesiano). Cada workspace pode ter campos personalizados (ex.: Cidade, Porte) e esconder campos padr\xE3o: veja list_fields. Em create_lead/update_lead, preencha-os em custom_fields usando o nome do campo. Lead ganho n\xE3o troca de funil: para levar o cliente adiante (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento), use handoff_lead ou next_funnel em mark_won, que criam um lead novo ligado ao ganho. Antes de importar um CSV desconhecido, rode import_leads_csv com dry_run=true para revisar o mapeamento de colunas. Admins tamb\xE9m gerenciam time (invite_user, update_user), estrutura (create_workspace, create_funnel, update_funnel), metas e comiss\xF5es (set_goals, save_commission_plan) e modo TV. O servidor aplica as permiss\xF5es do usu\xE1rio logado: se uma a\xE7\xE3o for negada, explique que precisa de um admin. A\xE7\xF5es destrutivas (delete_*, archive_field, revoke_*) exigem confirma\xE7\xE3o expl\xEDcita da pessoa antes."
|
|
1285
1342
|
}
|
|
1286
1343
|
);
|
|
1287
1344
|
server.registerTool(
|
|
@@ -1306,6 +1363,7 @@ async function startServer() {
|
|
|
1306
1363
|
id: f.id,
|
|
1307
1364
|
goal_kpi: f.goalKpi,
|
|
1308
1365
|
...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
|
|
1366
|
+
...f.nextFunnelId ? { after_won_continue_in: c.funnels.get(f.nextFunnelId)?.name ?? null } : {},
|
|
1309
1367
|
stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
|
|
1310
1368
|
}))
|
|
1311
1369
|
})),
|
|
@@ -1379,6 +1437,8 @@ async function startServer() {
|
|
|
1379
1437
|
source: d.lead.source,
|
|
1380
1438
|
lost_reason: d.lead.lostReason,
|
|
1381
1439
|
expected_close: d.lead.expectedCloseAt,
|
|
1440
|
+
...d.links.source ? { came_from: { lead_id: d.links.source.id, funnel: d.links.source.funnelName, won_at: d.links.source.wonAt, value: formatMoney(d.links.source.valueCents) } } : {},
|
|
1441
|
+
...d.links.continuations.length ? { continues_in: d.links.continuations.map((x) => ({ lead_id: x.id, funnel: x.funnelName, stage: x.stageName, status: x.status })) } : {},
|
|
1382
1442
|
history: d.events.slice(0, 30).map((e) => ({
|
|
1383
1443
|
at: e.createdAt,
|
|
1384
1444
|
by: e.actorName,
|
|
@@ -1386,6 +1446,7 @@ async function startServer() {
|
|
|
1386
1446
|
...e.type === "note" ? { note: e.payload?.body } : {},
|
|
1387
1447
|
...e.fromStageId || e.toStageId ? { from: d.names[e.fromStageId ?? ""], to: d.names[e.toStageId ?? ""] } : {},
|
|
1388
1448
|
...e.type === "status_changed" ? { status: e.payload } : {},
|
|
1449
|
+
...e.type === "handed_off" ? { to_funnel: d.names[String(e.payload?.toFunnelId ?? "")] } : {},
|
|
1389
1450
|
...e.type === "updated" && Array.isArray(e.payload?.fields) ? { changed: e.payload.fields.map(fieldLabel) } : {}
|
|
1390
1451
|
}))
|
|
1391
1452
|
});
|
|
@@ -1450,7 +1511,7 @@ async function startServer() {
|
|
|
1450
1511
|
},
|
|
1451
1512
|
withApi(async (api, a) => {
|
|
1452
1513
|
const c = await ctx(api);
|
|
1453
|
-
const unassign = a.owner != null &&
|
|
1514
|
+
const unassign = a.owner != null && isNoOwner(a.owner);
|
|
1454
1515
|
const owner = a.owner && !unassign ? findUser(c, a.owner) : void 0;
|
|
1455
1516
|
let fields;
|
|
1456
1517
|
if (a.custom_fields) fields = await listFields(api, (await api.get(`/leads/${a.lead_id}`)).lead.workspaceId);
|
|
@@ -1466,7 +1527,7 @@ async function startServer() {
|
|
|
1466
1527
|
"move_lead",
|
|
1467
1528
|
{
|
|
1468
1529
|
title: "Mover lead",
|
|
1469
|
-
description: "Move um lead para outra etapa \u2014 e opcionalmente para outro funil (entre workspaces: s\xF3 admins).",
|
|
1530
|
+
description: "Move um lead para outra etapa \u2014 e opcionalmente para outro funil (entre workspaces: s\xF3 admins). Lead ganho n\xE3o muda de funil (a venda precisa continuar contando onde foi fechada): para lev\xE1-lo a outro funil, use handoff_lead.",
|
|
1470
1531
|
inputSchema: { lead_id: z7.string().uuid(), stage: z7.string().describe("Nome ou id da etapa de destino"), funnel: z7.string().optional().describe("Funil de destino (padr\xE3o: o atual)") }
|
|
1471
1532
|
},
|
|
1472
1533
|
withApi(async (api, a) => {
|
|
@@ -1482,13 +1543,34 @@ async function startServer() {
|
|
|
1482
1543
|
"mark_won",
|
|
1483
1544
|
{
|
|
1484
1545
|
title: "Marcar como ganho",
|
|
1485
|
-
description: "Marca o lead como ganho. Informe value (R$) se o valor fechado for diferente do cadastrado.",
|
|
1486
|
-
inputSchema: {
|
|
1546
|
+
description: "Marca o lead como ganho. Informe value (R$) se o valor fechado for diferente do cadastrado. Para j\xE1 continuar o cliente em outro funil (ex.: Relacionamento), informe next_funnel: \xE9 criado um lead novo ligado ao ganho, na mesma opera\xE7\xE3o. Se o funil tiver um funil seguinte configurado, a resposta avisa \u2014 pergunte \xE0 pessoa antes de usar handoff_lead.",
|
|
1547
|
+
inputSchema: {
|
|
1548
|
+
lead_id: z7.string().uuid(),
|
|
1549
|
+
value: z7.number().min(0).optional(),
|
|
1550
|
+
next_funnel: z7.string().optional().describe("Funil onde o cliente continua depois do ganho"),
|
|
1551
|
+
next_stage: z7.string().optional().describe("Etapa de entrada no pr\xF3ximo funil (padr\xE3o: a primeira)"),
|
|
1552
|
+
next_owner: z7.string().optional().describe("Respons\xE1vel no pr\xF3ximo funil (padr\xE3o: o mesmo)"),
|
|
1553
|
+
next_value: z7.number().min(0).optional().describe("Valor em reais no pr\xF3ximo funil (padr\xE3o: 0)")
|
|
1554
|
+
}
|
|
1487
1555
|
},
|
|
1488
1556
|
withApi(async (api, a) => {
|
|
1489
1557
|
const c = await ctx(api);
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1558
|
+
const current = (await api.get(`/leads/${a.lead_id}`)).lead;
|
|
1559
|
+
const next = a.next_funnel ? handoffBody(c, current, { funnel: a.next_funnel, stage: a.next_stage, owner: a.next_owner, value: a.next_value }) : null;
|
|
1560
|
+
const lead = await api.post(`/leads/${a.lead_id}/status`, {
|
|
1561
|
+
status: "won",
|
|
1562
|
+
...a.value != null ? { valueCents: Math.round(a.value * 100) } : {},
|
|
1563
|
+
...next ? { handoff: next.body } : {}
|
|
1564
|
+
});
|
|
1565
|
+
if (lead.handoffLeadId) {
|
|
1566
|
+
const continuation = (await api.get(`/leads/${lead.handoffLeadId}`)).lead;
|
|
1567
|
+
return text({ won: leadSummary(c, lead), continuation: leadSummary(c, continuation) });
|
|
1568
|
+
}
|
|
1569
|
+
const suggested = c.funnels.get(c.funnels.get(lead.funnelId)?.nextFunnelId ?? "");
|
|
1570
|
+
return text({
|
|
1571
|
+
won: leadSummary(c, lead),
|
|
1572
|
+
...suggested ? { tip: `Este funil sugere continuar o cliente em "${suggested.name}". Se a pessoa quiser, use handoff_lead.` } : {}
|
|
1573
|
+
});
|
|
1492
1574
|
})
|
|
1493
1575
|
);
|
|
1494
1576
|
server.registerTool(
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicrm-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server for AICRM — operate your CRM from Claude, Opencode and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"aicrm-mcp": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skill"
|
|
12
|
+
],
|
|
8
13
|
"scripts": {
|
|
9
14
|
"build": "tsup",
|
|
10
15
|
"dev": "tsx src/cli.ts",
|
|
@@ -23,5 +28,7 @@
|
|
|
23
28
|
"tsx": "^4.23.15",
|
|
24
29
|
"typescript": "^5.9.3"
|
|
25
30
|
},
|
|
26
|
-
"engines": {
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
}
|
|
27
34
|
}
|
package/skill/aicrm/SKILL.md
CHANGED
|
@@ -102,8 +102,9 @@ Como ler as respostas de erro:
|
|
|
102
102
|
5. **Campos personalizados** vão em `custom_fields` pelo nome: `{ "Cidade": "Recife", "Porte": "Média" }`. Em `select`, use uma das opções (veja `list_fields`); `null` limpa o campo.
|
|
103
103
|
6. **Ações destrutivas exigem confirmação explícita da pessoa** antes: `delete_lead`, `delete_workspace`, `delete_funnel`, `archive_field`, `delete_import_template`, `delete_commission_plan`, `revoke_tv_link`, `revoke_api_token`, e `update_user` com `active: false`. Diga o que será afetado (ex.: quantos leads) e espere o "sim".
|
|
104
104
|
7. **Para tirar um lead do funil sem apagar**, prefira `mark_lost` com o motivo certo.
|
|
105
|
-
8. **
|
|
106
|
-
9. **
|
|
105
|
+
8. **Lead ganho não muda de funil.** A venda precisa continuar contando na meta e na comissão de onde foi fechada. Para levar o cliente adiante (ex.: Aquisição → Relacionamento), use `handoff_lead` (ou `next_funnel` no `mark_won`): isso cria um lead novo, aberto e ligado ao ganho, no outro funil.
|
|
106
|
+
9. **Responda em português simples.** A pessoa pode não ter experiência com CRM nem estatística: traduza números ("~70% de chance de bater a meta"), destaque o que fazer em seguida, evite jargão e ids.
|
|
107
|
+
10. **Volume:** `search_leads` devolve até 200 por chamada (padrão 25); `bulk_update_leads` aceita até 1000 ids; o CSV aceita até 10.000 linhas.
|
|
107
108
|
|
|
108
109
|
## 5. Ferramentas
|
|
109
110
|
|
|
@@ -115,8 +116,9 @@ Como ler as respostas de erro:
|
|
|
115
116
|
| `get_lead` | Todos os campos (inclusive personalizados) e o histórico. |
|
|
116
117
|
| `create_lead` | Criar (nome obrigatório; demais opcionais, inclusive `custom_fields`). |
|
|
117
118
|
| `update_lead` | Alterar só o que for informado; `owner: "none"` tira o responsável; `""` limpa um campo padrão. |
|
|
118
|
-
| `move_lead` | Mudar de etapa (e de funil). |
|
|
119
|
-
| `mark_won` / `mark_lost` / `reopen_lead` | Fechar como ganho (com valor final), perdido (motivo: `price`, `timing`, `competitor`, `no_response`, `no_fit`, `other`) ou reabrir. |
|
|
119
|
+
| `move_lead` | Mudar de etapa (e de funil, só para leads abertos ou perdidos). |
|
|
120
|
+
| `mark_won` / `mark_lost` / `reopen_lead` | Fechar como ganho (com valor final; `next_funnel` já continua o cliente em outro funil), perdido (motivo: `price`, `timing`, `competitor`, `no_response`, `no_fit`, `other`) ou reabrir. |
|
|
121
|
+
| `handoff_lead` | Continuar um lead **ganho** em outro funil (pós-venda, relacionamento): cria um lead novo ligado ao original, na 1ª etapa (ou `stage`), valor R$ 0 (ou `value`), mesmo responsável (ou `owner`). O `get_lead` mostra o vínculo em `came_from` / `continues_in`. |
|
|
120
122
|
| `add_note` | Registrar conversa, ligação, contexto. |
|
|
121
123
|
| `bulk_update_leads` | Mover e/ou reatribuir vários leads de uma vez (ids do `search_leads`). |
|
|
122
124
|
| `delete_lead` | Apagar com histórico (admin, destrutivo). |
|
|
@@ -176,6 +178,8 @@ Como ler as respostas de erro:
|
|
|
176
178
|
|
|
177
179
|
### Fechar negócio
|
|
178
180
|
- Ganho: `mark_won` (com `value` se o valor final mudou). Perdido: `mark_lost` com o motivo mais próximo e uma `note` curta. Errou? `reopen_lead`.
|
|
181
|
+
- Depois do ganho: se o funil tiver um funil seguinte (`after_won_continue_in` no `list_workspaces`, ou o `tip` na resposta do `mark_won`), pergunte se o cliente deve seguir para lá e use `handoff_lead`. Se a pessoa já pediu ("fechou, manda pro Relacionamento"), faça tudo de uma vez com `mark_won` + `next_funnel`.
|
|
182
|
+
- Para configurar o caminho padrão (admin): `update_funnel` com `next_funnel: "Relacionamento"`.
|
|
179
183
|
|
|
180
184
|
### Redistribuir carteira (férias, saída, rodízio)
|
|
181
185
|
1. `search_leads` com `owner` e `status: "open"` (aumente `limit` se precisar).
|