@adrata/adrata-mcp 1.0.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.
Files changed (41) hide show
  1. package/README.md +548 -0
  2. package/access/auth.js +289 -0
  3. package/access/oauth.js +1059 -0
  4. package/access/resource-metadata.js +167 -0
  5. package/access/tiers.js +422 -0
  6. package/analytics.js +634 -0
  7. package/api-bridge.js +499 -0
  8. package/governance/money.js +141 -0
  9. package/output-formatter.js +589 -0
  10. package/package.json +68 -0
  11. package/resources.js +246 -0
  12. package/security.js +690 -0
  13. package/server.js +2139 -0
  14. package/server.json +55 -0
  15. package/skills/backlog-triage/SKILL.md +115 -0
  16. package/skills/board-review/SKILL.md +96 -0
  17. package/skills/incident-to-card/SKILL.md +126 -0
  18. package/skills/log-outreach.md +62 -0
  19. package/skills/ship-the-card/SKILL.md +155 -0
  20. package/tool-annotations.js +269 -0
  21. package/tools/billing.js +149 -0
  22. package/tools/email-tools.js +652 -0
  23. package/tools/enterprise-tools.js +651 -0
  24. package/tools/free-search.js +160 -0
  25. package/tools/memory.js +440 -0
  26. package/tools/morning-brief.js +551 -0
  27. package/tools/paper-tools.js +563 -0
  28. package/tools/scheduling.js +322 -0
  29. package/tools/work-board-tools.js +758 -0
  30. package/toolsets/communications.js +276 -0
  31. package/toolsets/crm.js +495 -0
  32. package/toolsets/extensibility.js +1131 -0
  33. package/toolsets/infrastructure.js +757 -0
  34. package/toolsets/intelligence.js +232 -0
  35. package/toolsets/knowledge.js +154 -0
  36. package/toolsets/matrix.js +217 -0
  37. package/toolsets/outreach.js +432 -0
  38. package/toolsets/prospecting.js +314 -0
  39. package/toolsets/revenue/always-loaded.js +341 -0
  40. package/toolsets/revenue/sloan-tools.js +81 -0
  41. package/transport-http.js +505 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * CRM & Data Toolset (6 tools, Enterprise tier)
3
+ *
4
+ * manage_company, manage_person, manage_opportunity,
5
+ * manage_activity, manage_buyer_group, get_action_history
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import {
10
+ md, mdError, table, formatEntityTimeline, formatDate,
11
+ } from '../output-formatter.js';
12
+
13
+ export function register(server, api, AUTH) {
14
+
15
+ // -----------------------------------------------------------------------
16
+ // manage_company
17
+ // -----------------------------------------------------------------------
18
+ server.tool(
19
+ 'manage_company',
20
+ 'Full CRUD for companies: create, read, update, delete, enrich, and view activity timeline. One tool for all company operations.',
21
+ {
22
+ action: z.enum(['create', 'get', 'update', 'delete', 'enrich', 'timeline', 'search']).describe('Operation'),
23
+ id: z.string().optional().describe('Company ID (for get/update/delete/enrich/timeline)'),
24
+ name: z.string().optional().describe('Company name'),
25
+ domain: z.string().optional().describe('Company domain'),
26
+ industry: z.string().optional().describe('Industry'),
27
+ website: z.string().optional().describe('Website URL'),
28
+ status: z.string().optional().describe('ACTIVE, PROSPECT, CUSTOMER, CHURNED'),
29
+ customFields: z.record(z.unknown()).optional().describe('Custom fields (JSONB merged)'),
30
+ query: z.string().optional().describe('Search query (for search action)'),
31
+ },
32
+ async (args) => {
33
+ try {
34
+ switch (args.action) {
35
+ case 'create': {
36
+ const body = {};
37
+ if (args.name) body.name = args.name;
38
+ if (args.domain) body.domain = args.domain;
39
+ if (args.industry) body.industry = args.industry;
40
+ if (args.website) body.website = args.website;
41
+ if (args.status) body.status = args.status;
42
+ if (args.customFields) body.customFields = args.customFields;
43
+ const result = await api('POST', '/api/v1/companies', { body });
44
+ const c = result?.data || result;
45
+ return md(`## Company Created\n\n- **Name:** ${c.name || args.name}\n- **ID:** ${c.id || '\u2014'}\n- **Status:** ${c.status || 'PROSPECT'}\n`);
46
+ }
47
+ case 'get': {
48
+ const c = await api('GET', `/api/v1/companies/${args.id}`);
49
+ const data = c?.data || c;
50
+ let text = `## ${data.name || 'Company'}\n\n`;
51
+ text += `- **ID:** ${data.id}\n- **Industry:** ${data.industry || '\u2014'}\n`;
52
+ text += `- **Size:** ${data.employeeCount || data.size || '\u2014'}\n`;
53
+ text += `- **Status:** ${data.status || '\u2014'}\n- **Website:** ${data.website || data.domain || '\u2014'}\n`;
54
+ if (data.customFields && Object.keys(data.customFields).length > 0) {
55
+ text += '\n### Custom Fields\n';
56
+ Object.entries(data.customFields).forEach(([k, v]) => { text += `- **${k}:** ${v}\n`; });
57
+ }
58
+ return md(text);
59
+ }
60
+ case 'update': {
61
+ const body = {};
62
+ if (args.name) body.name = args.name;
63
+ if (args.industry) body.industry = args.industry;
64
+ if (args.website) body.website = args.website;
65
+ if (args.status) body.status = args.status;
66
+ if (args.customFields) body.customFields = args.customFields;
67
+ await api('PATCH', `/api/v1/companies/${args.id}`, { body });
68
+ return md(`## Company Updated\n\nID: ${args.id}\nFields updated: ${Object.keys(body).join(', ')}\n`);
69
+ }
70
+ case 'delete': {
71
+ await api('DELETE', `/api/v1/companies/${args.id}`);
72
+ return md(`## Company Deleted\n\nID: ${args.id}\n`);
73
+ }
74
+ case 'enrich': {
75
+ await api('POST', `/api/v1/companies/${args.id}/enrich`);
76
+ return md(`## Enrichment Triggered\n\nCompany ID: ${args.id}\nData will be available shortly.\n`);
77
+ }
78
+ case 'timeline': {
79
+ const [company, actions] = await Promise.all([
80
+ api('GET', `/api/v1/companies/${args.id}`).then(r => r?.data || r),
81
+ api('GET', `/api/v1/companies/${args.id}/actions`, { params: { limit: 25, page: 1 } }),
82
+ ]);
83
+ return md(formatEntityTimeline(company, actions));
84
+ }
85
+ case 'search': {
86
+ const result = await api('GET', '/api/v1/companies', { params: { search: args.query || args.name, limit: 25, page: 1 } });
87
+ const items = result?.data || [];
88
+ if (items.length === 0) return md('## Company Search\n\nNo companies found.\n');
89
+ let text = `## ${items.length} Companies Found\n\n`;
90
+ const rows = items.map(c => [c.name || '\u2014', c.industry || '\u2014', c.status || '\u2014', c.domain || '\u2014']);
91
+ text += table(['Name', 'Industry', 'Status', 'Domain'], rows);
92
+ return md(text);
93
+ }
94
+ default:
95
+ return mdError(`Unknown action: ${args.action}`);
96
+ }
97
+ } catch (err) {
98
+ return mdError('Company operation failed', err.message);
99
+ }
100
+ }
101
+ );
102
+
103
+ // -----------------------------------------------------------------------
104
+ // manage_person
105
+ // -----------------------------------------------------------------------
106
+ server.tool(
107
+ 'manage_person',
108
+ 'Full CRUD for people/contacts: create, read, update, delete, enrich, and view communication history.',
109
+ {
110
+ action: z.enum(['create', 'get', 'update', 'delete', 'enrich', 'timeline', 'search']).describe('Operation'),
111
+ id: z.string().optional().describe('Person ID'),
112
+ name: z.string().optional(),
113
+ firstName: z.string().optional(),
114
+ lastName: z.string().optional(),
115
+ email: z.string().optional(),
116
+ phone: z.string().optional(),
117
+ title: z.string().optional(),
118
+ companyId: z.string().optional(),
119
+ status: z.string().optional().describe('LEAD, PROSPECT, CUSTOMER'),
120
+ linkedinUrl: z.string().optional(),
121
+ customFields: z.record(z.unknown()).optional(),
122
+ query: z.string().optional().describe('Search query'),
123
+ },
124
+ async (args) => {
125
+ try {
126
+ switch (args.action) {
127
+ case 'create': {
128
+ const body = {};
129
+ ['name', 'firstName', 'lastName', 'email', 'phone', 'title', 'companyId', 'status', 'linkedinUrl', 'customFields'].forEach(k => {
130
+ if (args[k] !== undefined) body[k] = args[k];
131
+ });
132
+ const result = await api('POST', '/api/v1/people', { body });
133
+ const p = result?.data || result;
134
+ return md(`## Person Created\n\n- **Name:** ${p.name || args.name || `${args.firstName} ${args.lastName}`}\n- **ID:** ${p.id || '\u2014'}\n- **Email:** ${p.email || args.email || '\u2014'}\n`);
135
+ }
136
+ case 'get': {
137
+ const p = await api('GET', `/api/v1/people/${args.id}`);
138
+ const data = p?.data || p;
139
+ let text = `## ${data.name || `${data.firstName} ${data.lastName}`}\n\n`;
140
+ text += `- **Title:** ${data.title || '\u2014'}\n- **Company:** ${data.companyName || '\u2014'}\n`;
141
+ text += `- **Email:** ${data.email || '\u2014'}\n- **Phone:** ${data.phone || '\u2014'}\n`;
142
+ text += `- **Status:** ${data.status || '\u2014'}\n- **LinkedIn:** ${data.linkedinUrl || '\u2014'}\n`;
143
+ return md(text);
144
+ }
145
+ case 'update': {
146
+ const body = {};
147
+ ['name', 'firstName', 'lastName', 'email', 'phone', 'title', 'status', 'linkedinUrl', 'customFields'].forEach(k => {
148
+ if (args[k] !== undefined) body[k] = args[k];
149
+ });
150
+ await api('PATCH', `/api/v1/people/${args.id}`, { body });
151
+ return md(`## Person Updated\n\nID: ${args.id}\nFields: ${Object.keys(body).join(', ')}\n`);
152
+ }
153
+ case 'delete': {
154
+ await api('DELETE', `/api/v1/people/${args.id}`);
155
+ return md(`## Person Deleted\n\nID: ${args.id}\n`);
156
+ }
157
+ case 'enrich': {
158
+ await api('POST', `/api/v1/people/${args.id}/enrich`);
159
+ return md(`## Enrichment Triggered\n\nPerson ID: ${args.id}\n`);
160
+ }
161
+ case 'timeline': {
162
+ const [person, actions] = await Promise.all([
163
+ api('GET', `/api/v1/people/${args.id}`).then(r => r?.data || r),
164
+ api('GET', '/api/v1/actions', { params: { personId: args.id, limit: 25, page: 1 } }),
165
+ ]);
166
+ return md(formatEntityTimeline(person, actions));
167
+ }
168
+ case 'search': {
169
+ const result = await api('GET', '/api/v1/people', { params: { search: args.query || args.name || args.email, limit: 25, page: 1 } });
170
+ const items = result?.data || [];
171
+ if (items.length === 0) return md('## Person Search\n\nNo people found.\n');
172
+ let text = `## ${items.length} People Found\n\n`;
173
+ const rows = items.map(p => [p.name || '\u2014', p.title || '\u2014', p.companyName || '\u2014', p.email || '\u2014']);
174
+ text += table(['Name', 'Title', 'Company', 'Email'], rows);
175
+ return md(text);
176
+ }
177
+ default:
178
+ return mdError(`Unknown action: ${args.action}`);
179
+ }
180
+ } catch (err) {
181
+ return mdError('Person operation failed', err.message);
182
+ }
183
+ }
184
+ );
185
+
186
+ // -----------------------------------------------------------------------
187
+ // manage_opportunity
188
+ // -----------------------------------------------------------------------
189
+ server.tool(
190
+ 'manage_opportunity',
191
+ 'Full CRUD for deals/opportunities: create, read, update, delete, view stage history and stakeholders.',
192
+ {
193
+ action: z.enum(['create', 'get', 'update', 'delete', 'search']).describe('Operation'),
194
+ id: z.string().optional().describe('Opportunity ID'),
195
+ name: z.string().optional(),
196
+ companyId: z.string().optional(),
197
+ amount: z.number().optional(),
198
+ stage: z.string().optional().describe('Discovery, Qualification, Proposal, Negotiation, Closed Won, Closed Lost'),
199
+ probability: z.number().optional(),
200
+ closeDate: z.string().optional(),
201
+ ownerId: z.string().optional(),
202
+ customFields: z.record(z.unknown()).optional(),
203
+ query: z.string().optional(),
204
+ },
205
+ async (args) => {
206
+ try {
207
+ switch (args.action) {
208
+ case 'create': {
209
+ const body = {};
210
+ ['name', 'companyId', 'amount', 'stage', 'probability', 'closeDate', 'ownerId', 'customFields'].forEach(k => {
211
+ if (args[k] !== undefined) body[k] = args[k];
212
+ });
213
+ const result = await api('POST', '/api/v1/opportunities', { body });
214
+ const o = result?.data || result;
215
+ return md(`## Opportunity Created\n\n- **Name:** ${o.name || args.name}\n- **ID:** ${o.id || '\u2014'}\n- **Amount:** $${(o.amount || args.amount || 0).toLocaleString()}\n- **Stage:** ${o.stage || args.stage || 'Discovery'}\n`);
216
+ }
217
+ case 'get': {
218
+ const o = await api('GET', `/api/v1/opportunities/${args.id}`);
219
+ const data = o?.data || o;
220
+ let text = `## ${data.name || 'Opportunity'}\n\n`;
221
+ text += `- **Amount:** $${(data.amount || 0).toLocaleString()}\n`;
222
+ text += `- **Stage:** ${data.stage || '\u2014'}\n`;
223
+ text += `- **Probability:** ${data.probability || 0}%\n`;
224
+ text += `- **Close Date:** ${formatDate(data.closeDate || data.expectedCloseDate)}\n`;
225
+ text += `- **Company:** ${data.companyId || '\u2014'}\n`;
226
+ return md(text);
227
+ }
228
+ case 'update': {
229
+ const body = {};
230
+ ['name', 'amount', 'stage', 'probability', 'closeDate', 'customFields'].forEach(k => {
231
+ if (args[k] !== undefined) body[k] = args[k];
232
+ });
233
+ await api('PATCH', `/api/v1/opportunities/${args.id}`, { body });
234
+ return md(`## Opportunity Updated\n\nID: ${args.id}\nFields: ${Object.keys(body).join(', ')}\n`);
235
+ }
236
+ case 'delete': {
237
+ await api('DELETE', `/api/v1/opportunities/${args.id}`);
238
+ return md(`## Opportunity Deleted\n\nID: ${args.id}\n`);
239
+ }
240
+ case 'search': {
241
+ const result = await api('GET', '/api/v1/opportunities', { params: { search: args.query || args.name, limit: 25, page: 1 } });
242
+ const items = result?.data || [];
243
+ if (items.length === 0) return md('## Opportunity Search\n\nNo opportunities found.\n');
244
+ let text = `## ${items.length} Opportunities\n\n`;
245
+ const rows = items.map(o => [
246
+ o.name || '\u2014',
247
+ `$${(o.amount || 0).toLocaleString()}`,
248
+ o.stage || '\u2014',
249
+ `${o.probability || 0}%`,
250
+ formatDate(o.closeDate),
251
+ ]);
252
+ text += table(['Name', 'Amount', 'Stage', 'Prob', 'Close'], rows);
253
+ return md(text);
254
+ }
255
+ default:
256
+ return mdError(`Unknown action: ${args.action}`);
257
+ }
258
+ } catch (err) {
259
+ return mdError('Opportunity operation failed', err.message);
260
+ }
261
+ }
262
+ );
263
+
264
+ // -----------------------------------------------------------------------
265
+ // manage_activity
266
+ // -----------------------------------------------------------------------
267
+ server.tool(
268
+ 'manage_activity',
269
+ 'Create, update, complete, or list activities (actions, notes, calls, meetings). Auto-logs to CRM timeline.',
270
+ {
271
+ action: z.enum(['create', 'get', 'update', 'complete', 'list', 'delete']).describe('Operation'),
272
+ id: z.string().optional().describe('Action ID'),
273
+ title: z.string().optional(),
274
+ type: z.string().optional().describe('email, call, meeting, note, task'),
275
+ personId: z.string().optional(),
276
+ companyId: z.string().optional(),
277
+ status: z.string().optional().describe('PLANNED, IN_PROGRESS, COMPLETED'),
278
+ priority: z.string().optional().describe('LOW, NORMAL, HIGH, URGENT'),
279
+ dueDate: z.string().optional(),
280
+ outcome: z.string().optional(),
281
+ metadata: z.record(z.unknown()).optional(),
282
+ },
283
+ async (args) => {
284
+ try {
285
+ switch (args.action) {
286
+ case 'create': {
287
+ const body = {};
288
+ ['title', 'type', 'personId', 'companyId', 'status', 'priority', 'dueDate', 'outcome', 'metadata'].forEach(k => {
289
+ if (args[k] !== undefined) body[k] = args[k];
290
+ });
291
+ const result = await api('POST', '/api/v1/actions', { body });
292
+ const a = result?.data || result;
293
+ return md(`## Activity Created\n\n- **Title:** ${a.title || args.title}\n- **Type:** ${a.type || args.type || '\u2014'}\n- **ID:** ${a.id || '\u2014'}\n`);
294
+ }
295
+ case 'get': {
296
+ const a = await api('GET', `/api/v1/actions/${args.id}`);
297
+ const data = a?.data || a;
298
+ let text = `## ${data.title || 'Activity'}\n\n`;
299
+ text += `- **Type:** ${data.type || '\u2014'}\n- **Status:** ${data.status || '\u2014'}\n`;
300
+ text += `- **Priority:** ${data.priority || '\u2014'}\n- **Due:** ${formatDate(data.dueDate)}\n`;
301
+ if (data.outcome) text += `- **Outcome:** ${data.outcome}\n`;
302
+ return md(text);
303
+ }
304
+ case 'update': {
305
+ const body = {};
306
+ ['title', 'type', 'status', 'priority', 'dueDate', 'outcome', 'metadata'].forEach(k => {
307
+ if (args[k] !== undefined) body[k] = args[k];
308
+ });
309
+ await api('PUT', `/api/v1/actions/${args.id}`, { body });
310
+ return md(`## Activity Updated\n\nID: ${args.id}\n`);
311
+ }
312
+ case 'complete': {
313
+ if (args.outcome) await api('PUT', `/api/v1/actions/${args.id}`, { body: { outcome: args.outcome } });
314
+ await api('POST', `/api/v1/actions/${args.id}/complete`);
315
+ return md(`## Activity Completed\n\nID: ${args.id}${args.outcome ? `\nOutcome: ${args.outcome}` : ''}\n`);
316
+ }
317
+ case 'list': {
318
+ const params = { limit: 25, page: 1 };
319
+ if (args.personId) params.personId = args.personId;
320
+ if (args.companyId) params.companyId = args.companyId;
321
+ if (args.type) params.type = args.type;
322
+ if (args.status) params.status = args.status;
323
+ const result = await api('GET', '/api/v1/actions', { params });
324
+ const items = result?.data || [];
325
+ if (items.length === 0) return md('## Activities\n\nNo activities found.\n');
326
+ let text = `## ${items.length} Activities\n\n`;
327
+ const rows = items.map(a => [
328
+ a.type || '\u2014',
329
+ a.title || '\u2014',
330
+ a.status || '\u2014',
331
+ formatDate(a.dueDate || a.createdAt),
332
+ ]);
333
+ text += table(['Type', 'Title', 'Status', 'Date'], rows);
334
+ return md(text);
335
+ }
336
+ case 'delete': {
337
+ await api('DELETE', `/api/v1/actions/${args.id}`);
338
+ return md(`## Activity Deleted\n\nID: ${args.id}\n`);
339
+ }
340
+ default:
341
+ return mdError(`Unknown action: ${args.action}`);
342
+ }
343
+ } catch (err) {
344
+ return mdError('Activity operation failed', err.message);
345
+ }
346
+ }
347
+ );
348
+
349
+ // -----------------------------------------------------------------------
350
+ // manage_buyer_group
351
+ // -----------------------------------------------------------------------
352
+ server.tool(
353
+ 'manage_buyer_group',
354
+ 'Full CRUD for buyer groups (decision-making units): create, read, list, update and delete groups, and add/update/remove/list members with roles (Champion, Decision Maker, Influencer, Blocker, End User). Deleting a group cascades to its member seats, so a room never has to be emptied before it can be removed. Use bulk_delete to clear many rooms in one call.',
355
+ {
356
+ action: z.enum([
357
+ 'create', 'get', 'list', 'update', 'delete', 'bulk_delete',
358
+ 'add_member', 'update_member', 'remove_member', 'list_members',
359
+ ]).describe('Operation'),
360
+ id: z.string().optional().describe('Buyer group ID'),
361
+ ids: z.array(z.string()).optional().describe('Buyer group IDs (bulk_delete, max 50)'),
362
+ companyId: z.string().optional().describe('Company ID'),
363
+ name: z.string().optional().describe('Group name'),
364
+ personId: z.string().optional().describe('Person ID (add_member/update_member)'),
365
+ memberId: z.string().optional().describe('Buyer group member ID (update_member/remove_member)'),
366
+ role: z.string().optional().describe('Champion, Decision Maker, Influencer, Blocker, End User'),
367
+ influenceScore: z.number().optional().describe('Member influence score (update_member)'),
368
+ },
369
+ async (args) => {
370
+ try {
371
+ switch (args.action) {
372
+ case 'create': {
373
+ const result = await api('POST', '/api/v1/buyer-group', { body: { name: args.name, companyId: args.companyId } });
374
+ const bg = result?.data || result;
375
+ return md(`## Buyer Group Created\n\n- **Name:** ${bg.name || args.name}\n- **ID:** ${bg.id || '\u2014'}\n- **Company:** ${args.companyId}\n`);
376
+ }
377
+ case 'get': {
378
+ const bg = await api('GET', `/api/v1/buyer-group/${args.id}`);
379
+ const data = bg?.data || bg;
380
+ let text = `## Buyer Group: ${data.name || '\u2014'}\n\n`;
381
+ text += `- **ID:** ${data.id}\n- **Company:** ${data.companyId || '\u2014'}\n`;
382
+ return md(text);
383
+ }
384
+ case 'list': {
385
+ const result = await api('GET', '/api/v1/buyer-group', { params: { companyId: args.companyId, limit: 25, page: 1 } });
386
+ const items = result?.data || [];
387
+ if (items.length === 0) return md('## Buyer Groups\n\nNo buyer groups found.\n');
388
+ let text = `## ${items.length} Buyer Groups\n\n`;
389
+ const rows = items.map(bg => [bg.name || '\u2014', bg.id, bg.companyId || '\u2014']);
390
+ text += table(['Name', 'ID', 'Company'], rows);
391
+ return md(text);
392
+ }
393
+ case 'update': {
394
+ const patch = {};
395
+ if (args.name) patch.name = args.name;
396
+ if (args.companyId) patch.companyId = args.companyId;
397
+ if (Object.keys(patch).length === 0) {
398
+ return mdError('Nothing to update', 'Provide name and/or companyId.');
399
+ }
400
+ await api('PUT', `/api/v1/buyer-group/${args.id}`, { body: patch });
401
+ return md(`## Buyer Group Updated\n\nID: ${args.id}\nFields updated: ${Object.keys(patch).join(', ')}\n`);
402
+ }
403
+ case 'delete': {
404
+ await api('DELETE', `/api/v1/buyer-group/${args.id}`);
405
+ return md(`## Buyer Group Deleted\n\nID: ${args.id}\n\nThe room's member seats were removed with it. The underlying people and company records are untouched.\n`);
406
+ }
407
+ case 'bulk_delete': {
408
+ const ids = args.ids || [];
409
+ if (ids.length === 0) return mdError('bulk_delete requires ids', 'Pass an array of buyer group IDs (max 50).');
410
+ if (ids.length > 50) return mdError('Too many IDs', 'Maximum 50 buyer groups per batch.');
411
+ const result = await api('DELETE', '/api/v1/buyer-group/bulk', { body: { ids } });
412
+ const summary = result?.summary || {};
413
+ return md(`## Buyer Groups Deleted\n\n- **Requested:** ${summary.total ?? ids.length}\n- **Deleted:** ${summary.succeeded ?? '\u2014'}\n- **Failed:** ${summary.failed ?? '\u2014'}\n\nEach deleted room took its member seats with it.\n`);
414
+ }
415
+ case 'add_member': {
416
+ await api('POST', `/api/v1/buyer-group/${args.id}/members`, { body: { personId: args.personId, role: args.role } });
417
+ return md(`## Member Added\n\n- **Group:** ${args.id}\n- **Person:** ${args.personId}\n- **Role:** ${args.role || '\u2014'}\n`);
418
+ }
419
+ case 'update_member': {
420
+ if (!args.memberId) return mdError('update_member requires memberId', 'Use list_members to find the member ID.');
421
+ const patch = {};
422
+ if (args.role) patch.role = args.role;
423
+ if (args.personId) patch.personId = args.personId;
424
+ if (args.influenceScore !== undefined) patch.influenceScore = args.influenceScore;
425
+ await api('PUT', `/api/v1/buyer-group/${args.id}/members/${args.memberId}`, { body: patch });
426
+ return md(`## Member Updated\n\n- **Group:** ${args.id}\n- **Member:** ${args.memberId}\n- **Fields:** ${Object.keys(patch).join(', ') || '\u2014'}\n`);
427
+ }
428
+ case 'remove_member': {
429
+ if (!args.memberId) return mdError('remove_member requires memberId', 'Use list_members to find the member ID.');
430
+ await api('DELETE', `/api/v1/buyer-group/${args.id}/members/${args.memberId}`);
431
+ return md(`## Member Removed\n\n- **Group:** ${args.id}\n- **Member:** ${args.memberId}\n\nThe person record itself was not deleted.\n`);
432
+ }
433
+ case 'list_members': {
434
+ const result = await api('GET', `/api/v1/buyer-group/${args.id}/members`);
435
+ const members = result?.data || [];
436
+ if (members.length === 0) return md('## Buyer Group Members\n\nNo members in this group yet.\n');
437
+ let text = `## Buyer Group Members (${members.length})\n\n`;
438
+ const rows = members.map(m => [m.name || m.personId || '\u2014', m.role || '\u2014', m.title || '\u2014']);
439
+ text += table(['Name', 'Role', 'Title'], rows);
440
+ return md(text);
441
+ }
442
+ default:
443
+ return mdError(`Unknown action: ${args.action}`);
444
+ }
445
+ } catch (err) {
446
+ return mdError('Buyer group operation failed', err.message);
447
+ }
448
+ }
449
+ );
450
+
451
+ // -----------------------------------------------------------------------
452
+ // get_action_history
453
+ // -----------------------------------------------------------------------
454
+ server.tool(
455
+ 'get_action_history',
456
+ 'Full activity timeline for any entity: emails, calls, meetings, notes, stage changes. Shows the complete history of interactions with a person, company, or deal.',
457
+ {
458
+ entityType: z.enum(['person', 'company', 'opportunity']).describe('Entity type'),
459
+ entityId: z.string().describe('Entity ID'),
460
+ limit: z.number().optional().describe('Max items (default 25)'),
461
+ },
462
+ async (args) => {
463
+ try {
464
+ const limit = args.limit || 25;
465
+ let entity;
466
+ let actions;
467
+
468
+ switch (args.entityType) {
469
+ case 'person':
470
+ [entity, actions] = await Promise.all([
471
+ api('GET', `/api/v1/people/${args.entityId}`).then(r => r?.data || r),
472
+ api('GET', '/api/v1/actions', { params: { personId: args.entityId, limit, page: 1 } }),
473
+ ]);
474
+ break;
475
+ case 'company':
476
+ [entity, actions] = await Promise.all([
477
+ api('GET', `/api/v1/companies/${args.entityId}`).then(r => r?.data || r),
478
+ api('GET', `/api/v1/companies/${args.entityId}/actions`, { params: { limit, page: 1 } }),
479
+ ]);
480
+ break;
481
+ case 'opportunity':
482
+ [entity, actions] = await Promise.all([
483
+ api('GET', `/api/v1/opportunities/${args.entityId}`).then(r => r?.data || r),
484
+ api('GET', '/api/v1/actions', { params: { companyId: args.entityId, limit, page: 1 } }),
485
+ ]);
486
+ break;
487
+ }
488
+
489
+ return md(formatEntityTimeline(entity, actions));
490
+ } catch (err) {
491
+ return mdError('Action history failed', err.message);
492
+ }
493
+ }
494
+ );
495
+ }