rootr-cli 0.2.0 → 0.4.0
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/LICENSE +21 -0
- package/README.md +94 -4
- package/bin/rootr.js +109 -3
- package/lib/client.js +531 -1
- package/lib/config.js +30 -1
- package/lib/context.js +38 -0
- package/lib/mcp-tools/ask.js +1 -0
- package/lib/mcp-tools/attachments.js +110 -0
- package/lib/mcp-tools/crm.js +711 -0
- package/lib/mcp-tools/databases.js +5 -0
- package/lib/mcp-tools/documents.js +153 -1
- package/lib/mcp-tools/forms.js +206 -0
- package/lib/mcp-tools/issues.js +6 -0
- package/lib/mcp-tools/logs.js +5 -0
- package/lib/mcp-tools/misc.js +2 -0
- package/lib/mcp-tools/presentations.js +356 -0
- package/lib/mcp-tools/recordings.js +426 -0
- package/lib/mcp-tools/shared.js +18 -3
- package/lib/mcp-tools/spreadsheets.js +224 -0
- package/lib/mcp-tools/webhooks.js +3 -0
- package/lib/mcp-tools/whiteboards.js +166 -0
- package/lib/mcp-tools/workspaces.js +172 -9
- package/lib/mcp.js +98 -5
- package/package.json +22 -3
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared context repeated (shortened) across tool descriptions so an agent
|
|
7
|
+
* that only sees ONE of these tools still understands the node: CRM is a
|
|
8
|
+
* full sales CRM node — companies, contacts (with lifecycle stage + auto
|
|
9
|
+
* lead-scoring), multi-pipeline deals (kanban board), an activity timeline,
|
|
10
|
+
* and follow-up tasks — all auto-connected into the workspace's knowledge
|
|
11
|
+
* graph. Use it for anything sales/lead/customer-tracking; never fake a CRM
|
|
12
|
+
* with a plain DATABASE.
|
|
13
|
+
*/
|
|
14
|
+
const CRM_CONTEXT =
|
|
15
|
+
'Part of a Rootr (루터) CRM node: a full sales CRM (companies, contacts with lifecycle stages + auto lead-scoring, ' +
|
|
16
|
+
'multi-pipeline deals/kanban, activity timeline, follow-up tasks), auto-linked into the knowledge graph.';
|
|
17
|
+
|
|
18
|
+
const stageShape = z.object({
|
|
19
|
+
id: z.string().optional().describe('Stage id; auto-generated if omitted'),
|
|
20
|
+
name: z.string().describe('Stage name, e.g. "Qualified", "Proposal Sent"'),
|
|
21
|
+
probability: z.number().optional().describe('Default win probability 0-100 for deals entering this stage'),
|
|
22
|
+
color: z.string().optional().describe('Kanban column color hint, e.g. a hex code or color name'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const pipelineShape = z.object({
|
|
26
|
+
id: z.string().optional().describe('Pipeline id; auto-generated if omitted'),
|
|
27
|
+
name: z.string().describe('Pipeline name, e.g. "Sales Pipeline", "Renewals"'),
|
|
28
|
+
stages: z.array(stageShape).optional().describe('Ordered kanban stages for this pipeline'),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A parameter got JSON-stringified by the MCP client instead of arriving as
|
|
33
|
+
* a real object/array (seen historically whenever a tool schema used
|
|
34
|
+
* z.any()/z.unknown() at the top level). Defensively parse if we get a
|
|
35
|
+
* string where an object/array was expected, so a mis-behaving client still
|
|
36
|
+
* works instead of erroring the whole call.
|
|
37
|
+
*/
|
|
38
|
+
function coerceJson(value) {
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerCrmTools(server) {
|
|
50
|
+
server.registerTool(
|
|
51
|
+
'rootr_crm_create',
|
|
52
|
+
{
|
|
53
|
+
title: 'Create a Rootr CRM node',
|
|
54
|
+
description:
|
|
55
|
+
'Create a new CRM node in a Rootr (루터) workspace — companies, contacts, multi-pipeline deals (kanban), ' +
|
|
56
|
+
'activities and tasks all live inside it. Optionally seed custom `pipelines` (name + ordered stages, each ' +
|
|
57
|
+
'with an optional win probability/color) and/or custom company/contact/deal `fields`; sane defaults are ' +
|
|
58
|
+
'used if omitted. ' +
|
|
59
|
+
CRM_CONTEXT,
|
|
60
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
61
|
+
inputSchema: {
|
|
62
|
+
name: z.string().optional().describe('CRM node name'),
|
|
63
|
+
path: z.string().optional().describe('Full tree path to create at, e.g. "/sales/crm"'),
|
|
64
|
+
parentId: z.string().optional().describe('Parent folder/node id to create the CRM under'),
|
|
65
|
+
icon: z.string().optional().describe('Emoji icon for the CRM node'),
|
|
66
|
+
pipelines: z
|
|
67
|
+
.array(pipelineShape)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Custom deal pipelines with stages; defaults to a single standard sales pipeline if omitted'),
|
|
70
|
+
fields: z
|
|
71
|
+
.array(z.record(z.unknown()))
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Custom field definitions for companies/contacts/deals (schema-defined by the CRM feature)'),
|
|
74
|
+
config: z.record(z.unknown()).optional().describe('Free-form CRM config/settings object'),
|
|
75
|
+
createParents: z.boolean().optional().describe('Create missing parent folders in the path (default true)'),
|
|
76
|
+
autoRename: z.boolean().optional().describe('Auto-rename on name collision instead of erroring'),
|
|
77
|
+
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
async ({ name, path, parentId, icon, pipelines, fields, config, createParents, autoRename, workspace }) => {
|
|
81
|
+
try {
|
|
82
|
+
const ws = requireWorkspace(workspace);
|
|
83
|
+
const client = makeClient(ws);
|
|
84
|
+
const result = await client.createCrm(ws, {
|
|
85
|
+
name,
|
|
86
|
+
path,
|
|
87
|
+
parentId,
|
|
88
|
+
icon,
|
|
89
|
+
pipelines: coerceJson(pipelines),
|
|
90
|
+
fields: coerceJson(fields),
|
|
91
|
+
config: coerceJson(config),
|
|
92
|
+
createParents,
|
|
93
|
+
autoRename,
|
|
94
|
+
});
|
|
95
|
+
return jsonResult(result);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return errorResult(err);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
server.registerTool(
|
|
103
|
+
'rootr_crm_get',
|
|
104
|
+
{
|
|
105
|
+
title: 'Get a Rootr CRM node (pipelines, fields, counts)',
|
|
106
|
+
description:
|
|
107
|
+
'Fetch a Rootr (루터) CRM node: its pipelines (with stages), custom field definitions, config, entity ' +
|
|
108
|
+
'counts, and your access level. Call this before rootr_crm_update or before creating deals so you know ' +
|
|
109
|
+
'the exact pipeline/stage ids. Set includeSummary to also merge in rootr_crm_get\'s stage-value/forecast/' +
|
|
110
|
+
'win-rate summary in one call. ' +
|
|
111
|
+
CRM_CONTEXT,
|
|
112
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
113
|
+
inputSchema: {
|
|
114
|
+
crmId: z.string().describe('CRM node id'),
|
|
115
|
+
includeSummary: z
|
|
116
|
+
.boolean()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('If true, also fetch and merge the CRM summary (pipeline value/forecast/win-rate/tasks) as `summary`'),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
async ({ crmId, includeSummary }) => {
|
|
122
|
+
try {
|
|
123
|
+
const client = makeClient();
|
|
124
|
+
const crm = await client.getCrm(crmId);
|
|
125
|
+
if (includeSummary) {
|
|
126
|
+
const summary = await client.getCrmSummary(crmId);
|
|
127
|
+
return jsonResult({ ...crm, summary });
|
|
128
|
+
}
|
|
129
|
+
return jsonResult(crm);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return errorResult(err);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
server.registerTool(
|
|
137
|
+
'rootr_crm_update',
|
|
138
|
+
{
|
|
139
|
+
title: 'Update a Rootr CRM node (pipelines/fields/config)',
|
|
140
|
+
description:
|
|
141
|
+
'Merge-patch a Rootr (루터) CRM node\'s pipelines, custom field definitions, config, or icon. Passing ' +
|
|
142
|
+
'`pipelines` REPLACES the whole pipelines array — call rootr_crm_get first and resend any stages/pipelines ' +
|
|
143
|
+
'you want to keep. ' +
|
|
144
|
+
CRM_CONTEXT,
|
|
145
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
146
|
+
inputSchema: {
|
|
147
|
+
crmId: z.string().describe('CRM node id'),
|
|
148
|
+
pipelines: z
|
|
149
|
+
.array(pipelineShape)
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('COMPLETE pipelines array to set (replaces existing pipelines/stages)'),
|
|
152
|
+
fields: z.array(z.record(z.unknown())).optional().describe('COMPLETE custom field definitions to set'),
|
|
153
|
+
config: z.record(z.unknown()).optional().describe('Free-form CRM config/settings object'),
|
|
154
|
+
icon: z.string().optional().describe('Emoji icon for the CRM node'),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
async ({ crmId, pipelines, fields, config, icon }) => {
|
|
158
|
+
try {
|
|
159
|
+
const client = makeClient();
|
|
160
|
+
const result = await client.updateCrm(crmId, {
|
|
161
|
+
pipelines: coerceJson(pipelines),
|
|
162
|
+
fields: coerceJson(fields),
|
|
163
|
+
config: coerceJson(config),
|
|
164
|
+
icon,
|
|
165
|
+
});
|
|
166
|
+
return jsonResult(result);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return errorResult(err);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ---- Companies -----------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
server.registerTool(
|
|
176
|
+
'rootr_crm_list_companies',
|
|
177
|
+
{
|
|
178
|
+
title: 'List companies in a Rootr CRM',
|
|
179
|
+
description:
|
|
180
|
+
'List/search companies in a Rootr (루터) CRM, optionally filtered by free-text query, owner, or tag, and ' +
|
|
181
|
+
'sorted/paginated. ' +
|
|
182
|
+
CRM_CONTEXT,
|
|
183
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
184
|
+
inputSchema: {
|
|
185
|
+
crmId: z.string().describe('CRM node id'),
|
|
186
|
+
q: z.string().optional().describe('Free-text search (name/domain/etc.)'),
|
|
187
|
+
ownerId: z.string().optional().describe('Filter by owning user id'),
|
|
188
|
+
tag: z.string().optional().describe('Filter by tag'),
|
|
189
|
+
sort: z.string().optional().describe('Sort key, e.g. "name" | "-createdAt"'),
|
|
190
|
+
limit: z.number().optional().describe('Max results (pagination)'),
|
|
191
|
+
offset: z.number().optional().describe('Result offset (pagination)'),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
async ({ crmId, q, ownerId, tag, sort, limit, offset }) => {
|
|
195
|
+
try {
|
|
196
|
+
const client = makeClient();
|
|
197
|
+
const result = await client.listCrmCompanies(crmId, { q, ownerId, tag, sort, limit, offset });
|
|
198
|
+
return jsonResult(result);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return errorResult(err);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
server.registerTool(
|
|
206
|
+
'rootr_crm_upsert_company',
|
|
207
|
+
{
|
|
208
|
+
title: 'Create, update, or delete a company in a Rootr CRM',
|
|
209
|
+
description:
|
|
210
|
+
'Create a new company (omit `companyId`), update an existing one (pass `companyId` + the fields to change), ' +
|
|
211
|
+
'or delete one (pass `companyId` and `delete: true`). `custom` holds values for the CRM\'s custom field ' +
|
|
212
|
+
'definitions (see rootr_crm_get). ' +
|
|
213
|
+
CRM_CONTEXT,
|
|
214
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
215
|
+
inputSchema: {
|
|
216
|
+
crmId: z.string().describe('CRM node id'),
|
|
217
|
+
companyId: z.string().optional().describe('Existing company id to update/delete; omit to create a new company'),
|
|
218
|
+
delete: z.boolean().optional().describe('If true (with companyId), delete the company instead of updating'),
|
|
219
|
+
name: z.string().optional().describe('Company name (required when creating)'),
|
|
220
|
+
domain: z.string().optional().describe('Primary domain, e.g. "acme.com" (auto-links contacts sharing it)'),
|
|
221
|
+
industry: z.string().optional(),
|
|
222
|
+
size: z.string().optional().describe('Company size band, e.g. "1-10" | "51-200"'),
|
|
223
|
+
address: z.string().optional(),
|
|
224
|
+
description: z.string().optional(),
|
|
225
|
+
ownerId: z.string().optional().describe('Owning user id'),
|
|
226
|
+
source: z.string().optional().describe('Acquisition source, e.g. "referral" | "outbound"'),
|
|
227
|
+
tags: z.array(z.string()).optional(),
|
|
228
|
+
custom: z.record(z.unknown()).optional().describe('Custom field values, keyed by field id/name'),
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
async ({ crmId, companyId, delete: doDelete, custom, ...rest }) => {
|
|
232
|
+
try {
|
|
233
|
+
const client = makeClient();
|
|
234
|
+
if (companyId && doDelete) {
|
|
235
|
+
await client.deleteCrmCompany(companyId);
|
|
236
|
+
return jsonResult({ deleted: true, companyId });
|
|
237
|
+
}
|
|
238
|
+
const body = { ...rest, custom: coerceJson(custom) };
|
|
239
|
+
const result = companyId
|
|
240
|
+
? await client.updateCrmCompany(companyId, body)
|
|
241
|
+
: await client.createCrmCompany(crmId, body);
|
|
242
|
+
return jsonResult(result);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return errorResult(err);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
server.registerTool(
|
|
250
|
+
'rootr_crm_get_company',
|
|
251
|
+
{
|
|
252
|
+
title: 'Get a company detail (contacts, deals, activities, tasks)',
|
|
253
|
+
description:
|
|
254
|
+
'Fetch full detail of ONE Rootr (루터) CRM company: the company plus its contacts, deals, activities, and ' +
|
|
255
|
+
'tasks. ' +
|
|
256
|
+
CRM_CONTEXT,
|
|
257
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
258
|
+
inputSchema: {
|
|
259
|
+
companyId: z.string().describe('Company id'),
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
async ({ companyId }) => {
|
|
263
|
+
try {
|
|
264
|
+
const client = makeClient();
|
|
265
|
+
const result = await client.getCrmCompany(companyId);
|
|
266
|
+
return jsonResult(result);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return errorResult(err);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// ---- Contacts ------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
server.registerTool(
|
|
276
|
+
'rootr_crm_list_contacts',
|
|
277
|
+
{
|
|
278
|
+
title: 'List contacts in a Rootr CRM',
|
|
279
|
+
description:
|
|
280
|
+
'List/search contacts in a Rootr (루터) CRM, filterable by free-text query, company, lifecycle stage ' +
|
|
281
|
+
'(lead|mql|sql|opportunity|customer|evangelist), lead status (new|attempting|connected|open_deal|unqualified), ' +
|
|
282
|
+
'owner, or tag. ' +
|
|
283
|
+
CRM_CONTEXT,
|
|
284
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
285
|
+
inputSchema: {
|
|
286
|
+
crmId: z.string().describe('CRM node id'),
|
|
287
|
+
q: z.string().optional().describe('Free-text search (name/email/etc.)'),
|
|
288
|
+
companyId: z.string().optional().describe('Filter by company id'),
|
|
289
|
+
lifecycleStage: z.string().optional().describe('lead | mql | sql | opportunity | customer | evangelist'),
|
|
290
|
+
leadStatus: z.string().optional().describe('new | attempting | connected | open_deal | unqualified'),
|
|
291
|
+
ownerId: z.string().optional().describe('Filter by owning user id'),
|
|
292
|
+
tag: z.string().optional().describe('Filter by tag'),
|
|
293
|
+
sort: z.string().optional().describe('Sort key, e.g. "name" | "-createdAt"'),
|
|
294
|
+
limit: z.number().optional().describe('Max results (pagination)'),
|
|
295
|
+
offset: z.number().optional().describe('Result offset (pagination)'),
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
async ({ crmId, q, companyId, lifecycleStage, leadStatus, ownerId, tag, sort, limit, offset }) => {
|
|
299
|
+
try {
|
|
300
|
+
const client = makeClient();
|
|
301
|
+
const result = await client.listCrmContacts(crmId, {
|
|
302
|
+
q,
|
|
303
|
+
companyId,
|
|
304
|
+
lifecycleStage,
|
|
305
|
+
leadStatus,
|
|
306
|
+
ownerId,
|
|
307
|
+
tag,
|
|
308
|
+
sort,
|
|
309
|
+
limit,
|
|
310
|
+
offset,
|
|
311
|
+
});
|
|
312
|
+
return jsonResult(result);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return errorResult(err);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
server.registerTool(
|
|
320
|
+
'rootr_crm_upsert_contact',
|
|
321
|
+
{
|
|
322
|
+
title: 'Create, update, or delete a contact in a Rootr CRM',
|
|
323
|
+
description:
|
|
324
|
+
'Create a new contact (omit `contactId`), update an existing one (pass `contactId` + fields to change), ' +
|
|
325
|
+
'or delete one (pass `contactId` and `delete: true`). Setting `email` auto-links the contact to a matching ' +
|
|
326
|
+
'company by domain, and leadScore is computed server-side automatically — do not try to set it yourself. ' +
|
|
327
|
+
CRM_CONTEXT,
|
|
328
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
329
|
+
inputSchema: {
|
|
330
|
+
crmId: z.string().describe('CRM node id'),
|
|
331
|
+
contactId: z.string().optional().describe('Existing contact id to update/delete; omit to create a new contact'),
|
|
332
|
+
delete: z.boolean().optional().describe('If true (with contactId), delete the contact instead of updating'),
|
|
333
|
+
name: z.string().optional().describe('Contact name (required when creating)'),
|
|
334
|
+
email: z.string().optional().describe('Email address; its domain auto-links a matching company'),
|
|
335
|
+
phone: z.string().optional(),
|
|
336
|
+
title: z.string().optional().describe('Job title'),
|
|
337
|
+
companyId: z.string().optional().describe('Company id to attach to (overrides domain auto-link)'),
|
|
338
|
+
lifecycleStage: z
|
|
339
|
+
.string()
|
|
340
|
+
.optional()
|
|
341
|
+
.describe('lead | mql | sql | opportunity | customer | evangelist'),
|
|
342
|
+
leadStatus: z
|
|
343
|
+
.string()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe('new | attempting | connected | open_deal | unqualified'),
|
|
346
|
+
ownerId: z.string().optional().describe('Owning user id'),
|
|
347
|
+
source: z.string().optional().describe('Acquisition source, e.g. "referral" | "outbound"'),
|
|
348
|
+
linkedinUrl: z.string().optional(),
|
|
349
|
+
tags: z.array(z.string()).optional(),
|
|
350
|
+
custom: z.record(z.unknown()).optional().describe('Custom field values, keyed by field id/name'),
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
async ({ crmId, contactId, delete: doDelete, custom, ...rest }) => {
|
|
354
|
+
try {
|
|
355
|
+
const client = makeClient();
|
|
356
|
+
if (contactId && doDelete) {
|
|
357
|
+
await client.deleteCrmContact(contactId);
|
|
358
|
+
return jsonResult({ deleted: true, contactId });
|
|
359
|
+
}
|
|
360
|
+
const body = { ...rest, custom: coerceJson(custom) };
|
|
361
|
+
const result = contactId
|
|
362
|
+
? await client.updateCrmContact(contactId, body)
|
|
363
|
+
: await client.createCrmContact(crmId, body);
|
|
364
|
+
return jsonResult(result);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return errorResult(err);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
server.registerTool(
|
|
372
|
+
'rootr_crm_get_contact',
|
|
373
|
+
{
|
|
374
|
+
title: 'Get a contact by id',
|
|
375
|
+
description: 'Fetch full detail of ONE Rootr (루터) CRM contact by id. ' + CRM_CONTEXT,
|
|
376
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
377
|
+
inputSchema: {
|
|
378
|
+
contactId: z.string().describe('Contact id'),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
async ({ contactId }) => {
|
|
382
|
+
try {
|
|
383
|
+
const client = makeClient();
|
|
384
|
+
const result = await client.getCrmContact(contactId);
|
|
385
|
+
return jsonResult(result);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return errorResult(err);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// ---- Deals ---------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
server.registerTool(
|
|
395
|
+
'rootr_crm_list_deals',
|
|
396
|
+
{
|
|
397
|
+
title: 'List deals in a Rootr CRM',
|
|
398
|
+
description:
|
|
399
|
+
'List/search deals (kanban cards) in a Rootr (루터) CRM, filterable by query, pipeline, stage, status ' +
|
|
400
|
+
'(open|won|lost|all), owner, company, contact, or tag. ' +
|
|
401
|
+
CRM_CONTEXT,
|
|
402
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
403
|
+
inputSchema: {
|
|
404
|
+
crmId: z.string().describe('CRM node id'),
|
|
405
|
+
q: z.string().optional().describe('Free-text search (deal name/etc.)'),
|
|
406
|
+
pipelineId: z.string().optional().describe('Filter by pipeline id'),
|
|
407
|
+
stageId: z.string().optional().describe('Filter by stage id'),
|
|
408
|
+
status: z.string().optional().describe('open | won | lost | all'),
|
|
409
|
+
ownerId: z.string().optional().describe('Filter by owning user id'),
|
|
410
|
+
companyId: z.string().optional().describe('Filter by company id'),
|
|
411
|
+
contactId: z.string().optional().describe('Filter by contact id'),
|
|
412
|
+
tag: z.string().optional().describe('Filter by tag'),
|
|
413
|
+
sort: z.string().optional().describe('Sort key, e.g. "-value" | "expectedCloseDate"'),
|
|
414
|
+
limit: z.number().optional().describe('Max results (pagination)'),
|
|
415
|
+
offset: z.number().optional().describe('Result offset (pagination)'),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
async ({ crmId, q, pipelineId, stageId, status, ownerId, companyId, contactId, tag, sort, limit, offset }) => {
|
|
419
|
+
try {
|
|
420
|
+
const client = makeClient();
|
|
421
|
+
const result = await client.listCrmDeals(crmId, {
|
|
422
|
+
q,
|
|
423
|
+
pipelineId,
|
|
424
|
+
stageId,
|
|
425
|
+
status,
|
|
426
|
+
ownerId,
|
|
427
|
+
companyId,
|
|
428
|
+
contactId,
|
|
429
|
+
tag,
|
|
430
|
+
sort,
|
|
431
|
+
limit,
|
|
432
|
+
offset,
|
|
433
|
+
});
|
|
434
|
+
return jsonResult(result);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return errorResult(err);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
server.registerTool(
|
|
442
|
+
'rootr_crm_upsert_deal',
|
|
443
|
+
{
|
|
444
|
+
title: 'Create, update, or delete a deal in a Rootr CRM',
|
|
445
|
+
description:
|
|
446
|
+
'Create a new deal (omit `dealId`; defaults to the CRM\'s first pipeline if `pipelineId` is omitted), ' +
|
|
447
|
+
'update an existing one (pass `dealId` + fields to change — set `status` to "won" or "lost" to close it, ' +
|
|
448
|
+
'with `wonLostReason`, or "open" to reopen), or delete one (pass `dealId` and `delete: true`). To MOVE a ' +
|
|
449
|
+
'deal between kanban stages, prefer rootr_crm_move_deal instead of patching `stageId` here. ' +
|
|
450
|
+
CRM_CONTEXT,
|
|
451
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
452
|
+
inputSchema: {
|
|
453
|
+
crmId: z.string().describe('CRM node id'),
|
|
454
|
+
dealId: z.string().optional().describe('Existing deal id to update/delete; omit to create a new deal'),
|
|
455
|
+
delete: z.boolean().optional().describe('If true (with dealId), delete the deal instead of updating'),
|
|
456
|
+
name: z.string().optional().describe('Deal name (required when creating)'),
|
|
457
|
+
value: z.number().optional().describe('Deal value'),
|
|
458
|
+
currency: z.string().optional().describe('Currency code, e.g. "USD" | "KRW"'),
|
|
459
|
+
pipelineId: z.string().optional().describe('Pipeline id; defaults to the CRM\'s first pipeline when creating'),
|
|
460
|
+
stageId: z.string().optional().describe('Stage id (see rootr_crm_move_deal to change stage on an existing deal)'),
|
|
461
|
+
status: z.string().optional().describe('open | won | lost (patch only)'),
|
|
462
|
+
wonLostReason: z.string().optional().describe('Reason text when closing status to won/lost'),
|
|
463
|
+
probability: z.number().optional().describe('Win probability override 0-100'),
|
|
464
|
+
expectedCloseDate: z.string().optional().describe('ISO date string'),
|
|
465
|
+
priority: z.string().optional().describe('low | medium | high'),
|
|
466
|
+
ownerId: z.string().optional().describe('Owning user id'),
|
|
467
|
+
source: z.string().optional().describe('Acquisition source'),
|
|
468
|
+
tags: z.array(z.string()).optional(),
|
|
469
|
+
companyId: z.string().optional().describe('Associated company id'),
|
|
470
|
+
primaryContactId: z.string().optional().describe('Primary contact id'),
|
|
471
|
+
contacts: z
|
|
472
|
+
.array(z.object({ contactId: z.string(), role: z.string().optional() }))
|
|
473
|
+
.optional()
|
|
474
|
+
.describe('Additional contacts on the deal with an optional role, e.g. "champion" | "economic buyer"'),
|
|
475
|
+
custom: z.record(z.unknown()).optional().describe('Custom field values, keyed by field id/name'),
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
async ({ crmId, dealId, delete: doDelete, contacts, custom, ...rest }) => {
|
|
479
|
+
try {
|
|
480
|
+
const client = makeClient();
|
|
481
|
+
if (dealId && doDelete) {
|
|
482
|
+
await client.deleteCrmDeal(dealId);
|
|
483
|
+
return jsonResult({ deleted: true, dealId });
|
|
484
|
+
}
|
|
485
|
+
const body = { ...rest, contacts: coerceJson(contacts), custom: coerceJson(custom) };
|
|
486
|
+
const result = dealId ? await client.updateCrmDeal(dealId, body) : await client.createCrmDeal(crmId, body);
|
|
487
|
+
return jsonResult(result);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
return errorResult(err);
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
server.registerTool(
|
|
495
|
+
'rootr_crm_get_deal',
|
|
496
|
+
{
|
|
497
|
+
title: 'Get a deal by id',
|
|
498
|
+
description: 'Fetch full detail of ONE Rootr (루터) CRM deal by id. ' + CRM_CONTEXT,
|
|
499
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
500
|
+
inputSchema: {
|
|
501
|
+
dealId: z.string().describe('Deal id'),
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
async ({ dealId }) => {
|
|
505
|
+
try {
|
|
506
|
+
const client = makeClient();
|
|
507
|
+
const result = await client.getCrmDeal(dealId);
|
|
508
|
+
return jsonResult(result);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return errorResult(err);
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
server.registerTool(
|
|
516
|
+
'rootr_crm_move_deal',
|
|
517
|
+
{
|
|
518
|
+
title: 'Move a deal to another kanban stage',
|
|
519
|
+
description:
|
|
520
|
+
'Move a Rootr (루터) CRM deal to a different pipeline stage (kanban column), optionally into a different ' +
|
|
521
|
+
'pipeline and/or a specific card position. Call rootr_crm_get first to learn the target stageId/pipelineId. ' +
|
|
522
|
+
CRM_CONTEXT,
|
|
523
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
524
|
+
inputSchema: {
|
|
525
|
+
dealId: z.string().describe('Deal id to move'),
|
|
526
|
+
stageId: z.string().describe('Target stage id'),
|
|
527
|
+
pipelineId: z.string().optional().describe('Target pipeline id, if moving across pipelines'),
|
|
528
|
+
position: z.number().optional().describe('0-based card position within the target stage'),
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
async ({ dealId, stageId, pipelineId, position }) => {
|
|
532
|
+
try {
|
|
533
|
+
const client = makeClient();
|
|
534
|
+
const result = await client.moveCrmDeal(dealId, { stageId, pipelineId, position });
|
|
535
|
+
return jsonResult(result);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return errorResult(err);
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// ---- Activities ------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
server.registerTool(
|
|
545
|
+
'rootr_crm_log_activity',
|
|
546
|
+
{
|
|
547
|
+
title: 'Log an activity on a Rootr CRM timeline',
|
|
548
|
+
description:
|
|
549
|
+
'Log an activity (note|call|email|meeting) on a Rootr (루터) CRM\'s timeline, attached to a contact, ' +
|
|
550
|
+
'company, and/or deal. Use this to record outreach/history that shows up on the contact/company/deal detail ' +
|
|
551
|
+
'view. ' +
|
|
552
|
+
CRM_CONTEXT,
|
|
553
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
554
|
+
inputSchema: {
|
|
555
|
+
crmId: z.string().describe('CRM node id'),
|
|
556
|
+
type: z.string().describe('note | call | email | meeting'),
|
|
557
|
+
subject: z.string().optional(),
|
|
558
|
+
body: z.string().optional().describe('Activity body/notes (markdown/plain text)'),
|
|
559
|
+
occurredAt: z.string().optional().describe('ISO datetime; defaults to now if omitted'),
|
|
560
|
+
contactId: z.string().optional(),
|
|
561
|
+
companyId: z.string().optional(),
|
|
562
|
+
dealId: z.string().optional(),
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
async ({ crmId, ...rest }) => {
|
|
566
|
+
try {
|
|
567
|
+
const client = makeClient();
|
|
568
|
+
const result = await client.createCrmActivity(crmId, rest);
|
|
569
|
+
return jsonResult(result);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
return errorResult(err);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
server.registerTool(
|
|
577
|
+
'rootr_crm_list_activities',
|
|
578
|
+
{
|
|
579
|
+
title: 'List activities on a Rootr CRM timeline',
|
|
580
|
+
description:
|
|
581
|
+
'List logged activities in a Rootr (루터) CRM, filterable by contact, company, deal, or activity type. ' +
|
|
582
|
+
CRM_CONTEXT,
|
|
583
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
584
|
+
inputSchema: {
|
|
585
|
+
crmId: z.string().describe('CRM node id'),
|
|
586
|
+
contactId: z.string().optional(),
|
|
587
|
+
companyId: z.string().optional(),
|
|
588
|
+
dealId: z.string().optional(),
|
|
589
|
+
type: z.string().optional().describe('note | call | email | meeting'),
|
|
590
|
+
limit: z.number().optional().describe('Max results (pagination)'),
|
|
591
|
+
offset: z.number().optional().describe('Result offset (pagination)'),
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
async ({ crmId, contactId, companyId, dealId, type, limit, offset }) => {
|
|
595
|
+
try {
|
|
596
|
+
const client = makeClient();
|
|
597
|
+
const result = await client.listCrmActivities(crmId, { contactId, companyId, dealId, type, limit, offset });
|
|
598
|
+
return jsonResult(result);
|
|
599
|
+
} catch (err) {
|
|
600
|
+
return errorResult(err);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// ---- Tasks -----------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
server.registerTool(
|
|
608
|
+
'rootr_crm_upsert_task',
|
|
609
|
+
{
|
|
610
|
+
title: 'Create or update a follow-up task in a Rootr CRM',
|
|
611
|
+
description:
|
|
612
|
+
'Create a new follow-up task (omit `taskId`) or update an existing one (pass `taskId` + fields to change, ' +
|
|
613
|
+
'e.g. set `status` to "done" to complete it) in a Rootr (루터) CRM, optionally attached to a contact, ' +
|
|
614
|
+
'company, and/or deal. ' +
|
|
615
|
+
CRM_CONTEXT,
|
|
616
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
617
|
+
inputSchema: {
|
|
618
|
+
crmId: z.string().describe('CRM node id (required when creating; ignored when updating)'),
|
|
619
|
+
taskId: z.string().optional().describe('Existing task id to update; omit to create a new task'),
|
|
620
|
+
title: z.string().optional().describe('Task title (required when creating)'),
|
|
621
|
+
type: z.string().optional().describe('todo | call | email | meeting'),
|
|
622
|
+
dueAt: z.string().optional().describe('ISO datetime due date'),
|
|
623
|
+
status: z.string().optional().describe('open | done (patch only)'),
|
|
624
|
+
assigneeId: z.string().optional().describe('Assignee user id'),
|
|
625
|
+
contactId: z.string().optional(),
|
|
626
|
+
companyId: z.string().optional(),
|
|
627
|
+
dealId: z.string().optional(),
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
async ({ crmId, taskId, ...rest }) => {
|
|
631
|
+
try {
|
|
632
|
+
const client = makeClient();
|
|
633
|
+
const result = taskId ? await client.updateCrmTask(taskId, rest) : await client.createCrmTask(crmId, rest);
|
|
634
|
+
return jsonResult(result);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
return errorResult(err);
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
server.registerTool(
|
|
642
|
+
'rootr_crm_list_tasks',
|
|
643
|
+
{
|
|
644
|
+
title: 'List follow-up tasks in a Rootr CRM',
|
|
645
|
+
description:
|
|
646
|
+
'List follow-up tasks in a Rootr (루터) CRM, filterable by status (open|done|all), assignee, due bucket ' +
|
|
647
|
+
'(overdue|today|upcoming), contact, company, or deal. ' +
|
|
648
|
+
CRM_CONTEXT,
|
|
649
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
650
|
+
inputSchema: {
|
|
651
|
+
crmId: z.string().describe('CRM node id'),
|
|
652
|
+
status: z.string().optional().describe('open | done | all'),
|
|
653
|
+
assigneeId: z.string().optional(),
|
|
654
|
+
due: z.string().optional().describe('overdue | today | upcoming'),
|
|
655
|
+
contactId: z.string().optional(),
|
|
656
|
+
companyId: z.string().optional(),
|
|
657
|
+
dealId: z.string().optional(),
|
|
658
|
+
limit: z.number().optional().describe('Max results (pagination)'),
|
|
659
|
+
offset: z.number().optional().describe('Result offset (pagination)'),
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
async ({ crmId, status, assigneeId, due, contactId, companyId, dealId, limit, offset }) => {
|
|
663
|
+
try {
|
|
664
|
+
const client = makeClient();
|
|
665
|
+
const result = await client.listCrmTasks(crmId, {
|
|
666
|
+
status,
|
|
667
|
+
assigneeId,
|
|
668
|
+
due,
|
|
669
|
+
contactId,
|
|
670
|
+
companyId,
|
|
671
|
+
dealId,
|
|
672
|
+
limit,
|
|
673
|
+
offset,
|
|
674
|
+
});
|
|
675
|
+
return jsonResult(result);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
return errorResult(err);
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// ---- Import ------------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
server.registerTool(
|
|
685
|
+
'rootr_crm_import',
|
|
686
|
+
{
|
|
687
|
+
title: 'Bulk-import rows into a Rootr CRM',
|
|
688
|
+
description:
|
|
689
|
+
'Bulk-import rows into a Rootr (루터) CRM as companies, contacts, or deals in one call (`entity` picks which, ' +
|
|
690
|
+
'`rows` is an array of plain field objects matching that entity\'s create shape, e.g. the same fields as ' +
|
|
691
|
+
'rootr_crm_upsert_company/_contact/_deal). Use this for migrating a spreadsheet/CSV of leads or accounts ' +
|
|
692
|
+
'instead of calling the upsert tools one row at a time. ' +
|
|
693
|
+
CRM_CONTEXT,
|
|
694
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
695
|
+
inputSchema: {
|
|
696
|
+
crmId: z.string().describe('CRM node id'),
|
|
697
|
+
entity: z.string().describe('contact | company | deal'),
|
|
698
|
+
rows: z.array(z.record(z.unknown())).describe('Array of row objects to import, matching the entity shape'),
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
async ({ crmId, entity, rows }) => {
|
|
702
|
+
try {
|
|
703
|
+
const client = makeClient();
|
|
704
|
+
const result = await client.importCrm(crmId, { entity, rows: coerceJson(rows) });
|
|
705
|
+
return jsonResult(result);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
return errorResult(err);
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
}
|