@adrata/adrata-mcp 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/access/auth.js +193 -4
- package/access/oauth.js +82 -8
- package/access/tiers.js +33 -0
- package/api-bridge.js +68 -5
- package/governance/governed-args.js +261 -0
- package/package.json +5 -4
- package/product-profile.js +43 -0
- package/resources.js +22 -10
- package/security.js +34 -6
- package/server.js +282 -58
- package/server.json +0 -6
- package/skills/board-review/SKILL.md +10 -2
- package/skills/incident-to-card/SKILL.md +23 -15
- package/skills/ship-the-card/SKILL.md +5 -4
- package/tool-annotations.js +57 -7
- package/tools/billing.js +5 -1
- package/tools/roadmap-tools.js +192 -0
- package/tools/work-board-tools.js +286 -16
- package/tools/work-hub/audit.js +162 -0
- package/toolsets/crm.js +72 -10
- package/toolsets/prospecting.js +118 -12
- package/toolsets/spaces.js +628 -0
package/toolsets/crm.js
CHANGED
|
@@ -9,6 +9,23 @@ import { z } from 'zod';
|
|
|
9
9
|
import {
|
|
10
10
|
md, mdError, table, formatEntityTimeline, formatDate,
|
|
11
11
|
} from '../output-formatter.js';
|
|
12
|
+
import {
|
|
13
|
+
governedWriteArgs, governedWriteNote, isLiveWrite, runGovernedMarkdownWrite,
|
|
14
|
+
enrichCostLine, COMPANY_ENRICH_COST_SENTENCE, PERSON_ENRICH_COST_SENTENCE,
|
|
15
|
+
} from '../governance/governed-args.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `manage_*` tools are composites: one tool, one `action` argument, reads
|
|
19
|
+
* and writes side by side. Only the write actions are governed — a `get`,
|
|
20
|
+
* `list`, `search` or `timeline` must keep working exactly as before, with no
|
|
21
|
+
* new required arguments and no preview step.
|
|
22
|
+
*
|
|
23
|
+
* This is the list of actions that mutate. Everything not in it is a read.
|
|
24
|
+
*/
|
|
25
|
+
const WRITE_ACTIONS = new Set([
|
|
26
|
+
'create', 'update', 'delete', 'enrich', 'complete',
|
|
27
|
+
'bulk_delete', 'add_member', 'update_member', 'remove_member',
|
|
28
|
+
]);
|
|
12
29
|
|
|
13
30
|
export function register(server, api, AUTH) {
|
|
14
31
|
|
|
@@ -17,7 +34,10 @@ export function register(server, api, AUTH) {
|
|
|
17
34
|
// -----------------------------------------------------------------------
|
|
18
35
|
server.tool(
|
|
19
36
|
'manage_company',
|
|
20
|
-
'Full CRUD for companies: create, read, update, delete, enrich, and view activity timeline. One tool for all company operations.'
|
|
37
|
+
'Full CRUD for companies: create, read, update, delete, enrich, and view activity timeline. One tool for all company operations.'
|
|
38
|
+
+ ' Reads (get/search/timeline) run directly. create/update/delete/enrich are governed writes.'
|
|
39
|
+
+ COMPANY_ENRICH_COST_SENTENCE
|
|
40
|
+
+ governedWriteNote('write:companies'),
|
|
21
41
|
{
|
|
22
42
|
action: z.enum(['create', 'get', 'update', 'delete', 'enrich', 'timeline', 'search']).describe('Operation'),
|
|
23
43
|
id: z.string().optional().describe('Company ID (for get/update/delete/enrich/timeline)'),
|
|
@@ -28,6 +48,7 @@ export function register(server, api, AUTH) {
|
|
|
28
48
|
status: z.string().optional().describe('ACTIVE, PROSPECT, CUSTOMER, CHURNED'),
|
|
29
49
|
customFields: z.record(z.unknown()).optional().describe('Custom fields (JSONB merged)'),
|
|
30
50
|
query: z.string().optional().describe('Search query (for search action)'),
|
|
51
|
+
...governedWriteArgs(z),
|
|
31
52
|
},
|
|
32
53
|
async (args) => {
|
|
33
54
|
try {
|
|
@@ -40,9 +61,20 @@ export function register(server, api, AUTH) {
|
|
|
40
61
|
if (args.website) body.website = args.website;
|
|
41
62
|
if (args.status) body.status = args.status;
|
|
42
63
|
if (args.customFields) body.customFields = args.customFields;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
return runGovernedMarkdownWrite(
|
|
65
|
+
api, args,
|
|
66
|
+
{
|
|
67
|
+
method: 'POST',
|
|
68
|
+
path: '/api/v1/companies',
|
|
69
|
+
body,
|
|
70
|
+
heading: 'Create Company',
|
|
71
|
+
preview: { entity: 'company', operation: 'create', fields: Object.keys(body) },
|
|
72
|
+
},
|
|
73
|
+
(result) => {
|
|
74
|
+
const c = result?.data || result || {};
|
|
75
|
+
return `## Company Created\n\n- **Name:** ${c.name || args.name}\n- **ID:** ${c.id || '\u2014'}\n- **Status:** ${c.status || 'PROSPECT'}\n`;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
46
78
|
}
|
|
47
79
|
case 'get': {
|
|
48
80
|
const c = await api('GET', `/api/v1/companies/${args.id}`);
|
|
@@ -64,16 +96,46 @@ export function register(server, api, AUTH) {
|
|
|
64
96
|
if (args.website) body.website = args.website;
|
|
65
97
|
if (args.status) body.status = args.status;
|
|
66
98
|
if (args.customFields) body.customFields = args.customFields;
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
return runGovernedMarkdownWrite(
|
|
100
|
+
api, args,
|
|
101
|
+
{
|
|
102
|
+
method: 'PATCH',
|
|
103
|
+
path: `/api/v1/companies/${args.id}`,
|
|
104
|
+
body,
|
|
105
|
+
heading: 'Update Company',
|
|
106
|
+
preview: { entity: 'company', entityId: args.id, operation: 'update', fields: Object.keys(body) },
|
|
107
|
+
},
|
|
108
|
+
() => `## Company Updated\n\nID: ${args.id}\nFields updated: ${Object.keys(body).join(', ')}\n`,
|
|
109
|
+
);
|
|
69
110
|
}
|
|
70
111
|
case 'delete': {
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
return runGovernedMarkdownWrite(
|
|
113
|
+
api, args,
|
|
114
|
+
{
|
|
115
|
+
method: 'DELETE',
|
|
116
|
+
path: `/api/v1/companies/${args.id}`,
|
|
117
|
+
heading: 'Delete Company',
|
|
118
|
+
preview: { entity: 'company', entityId: args.id, operation: 'soft-delete' },
|
|
119
|
+
},
|
|
120
|
+
() => `## Company Deleted\n\nID: ${args.id}\n`,
|
|
121
|
+
);
|
|
73
122
|
}
|
|
74
123
|
case 'enrich': {
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
return runGovernedMarkdownWrite(
|
|
125
|
+
api, args,
|
|
126
|
+
{
|
|
127
|
+
method: 'POST',
|
|
128
|
+
path: `/api/v1/companies/${args.id}/enrich`,
|
|
129
|
+
heading: 'Enrich Company',
|
|
130
|
+
preview: {
|
|
131
|
+
entity: 'company',
|
|
132
|
+
entityId: args.id,
|
|
133
|
+
operation: 'enrich',
|
|
134
|
+
cost: enrichCostLine('company'),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
() => `## Enrichment Triggered\n\nCompany ID: ${args.id}\nCost: ${enrichCostLine('company')}\nData will be available shortly.\n`,
|
|
138
|
+
);
|
|
77
139
|
}
|
|
78
140
|
case 'timeline': {
|
|
79
141
|
const [company, actions] = await Promise.all([
|
package/toolsets/prospecting.js
CHANGED
|
@@ -12,6 +12,89 @@ import {
|
|
|
12
12
|
formatQualification, formatCompanyResearch, formatPersonResearch,
|
|
13
13
|
formatSpeedrunList, formatNextContacts, table, signalEmoji, formatDate, daysSince,
|
|
14
14
|
} from '../output-formatter.js';
|
|
15
|
+
import {
|
|
16
|
+
governedWriteArgs, governedWriteNote, isLiveWrite, missingLiveWriteFields,
|
|
17
|
+
previewMarkdown, enrichCostLine,
|
|
18
|
+
COMPANY_ENRICH_COST_SENTENCE, PERSON_ENRICH_COST_SENTENCE,
|
|
19
|
+
} from '../governance/governed-args.js';
|
|
20
|
+
import { governedWrite } from '../api-bridge.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run the enrichment leg of a research tool through the governed-write
|
|
24
|
+
* contract and render the outcome as a Markdown section.
|
|
25
|
+
*
|
|
26
|
+
* `research_company` / `research_person` are read tools with one write inside
|
|
27
|
+
* them: a POST to `/enrich`, which spends real Coresignal credits. They used to
|
|
28
|
+
* fire that POST on every call with no preview, no audit reason and no
|
|
29
|
+
* idempotency key — while the IDENTICAL endpoint reached through
|
|
30
|
+
* `enrich_company` / `enrich_person` in server.js previewed and refused.
|
|
31
|
+
*
|
|
32
|
+
* So the reads still run and still produce the report. The enrich is withheld
|
|
33
|
+
* by default and reported as withheld, with the credit cost named, rather than
|
|
34
|
+
* being silently skipped — a research report that quietly lost its enrichment
|
|
35
|
+
* would be worse than one that spent the credits.
|
|
36
|
+
*
|
|
37
|
+
* @returns `{ enrichment, section }` — `enrichment` is the live result (or an
|
|
38
|
+
* empty object when nothing was executed) so the report renders
|
|
39
|
+
* identically either way.
|
|
40
|
+
*/
|
|
41
|
+
async function governedEnrich(api, args, { method, path, kind, subjectId }) {
|
|
42
|
+
if (isLiveWrite(args)) {
|
|
43
|
+
const missing = missingLiveWriteFields(args);
|
|
44
|
+
if (missing.length > 0) {
|
|
45
|
+
return {
|
|
46
|
+
enrichment: {},
|
|
47
|
+
section:
|
|
48
|
+
`\n---\n\n## Enrichment refused — nothing was spent\n\n` +
|
|
49
|
+
`A live enrichment write is missing: ${missing.join(', ')}.\n\n` +
|
|
50
|
+
`- **Would have called:** ${method} ${path}\n` +
|
|
51
|
+
`- **Cost if executed:** ${enrichCostLine(kind)}\n\n` +
|
|
52
|
+
`Re-call with \`dryRun:false\` plus ${missing.join(', ')}.\n`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const outcome = await governedWrite(api, args, {
|
|
59
|
+
method,
|
|
60
|
+
path,
|
|
61
|
+
preview: {
|
|
62
|
+
entity: kind,
|
|
63
|
+
entityId: subjectId,
|
|
64
|
+
operation: 'enrich',
|
|
65
|
+
cost: enrichCostLine(kind),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (outcome.dryRun) {
|
|
70
|
+
return {
|
|
71
|
+
enrichment: {},
|
|
72
|
+
section:
|
|
73
|
+
`\n---\n\n${previewMarkdown('Enrichment', outcome.preview)}` +
|
|
74
|
+
`\nThe research above was assembled from reads only. No vendor credits were spent.\n`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
enrichment: outcome.result || {},
|
|
80
|
+
section:
|
|
81
|
+
`\n---\n\n## Enrichment executed\n\n` +
|
|
82
|
+
`- **Call:** ${method} ${path}\n` +
|
|
83
|
+
`- **Cost:** ${enrichCostLine(kind)}\n` +
|
|
84
|
+
`- **Reason:** ${args.reason}\n` +
|
|
85
|
+
`- **Idempotency key:** ${args.idempotencyKey}\n`,
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
enrichment: {},
|
|
90
|
+
section:
|
|
91
|
+
`\n---\n\n## Enrichment not performed\n\n` +
|
|
92
|
+
`- **Attempted:** ${method} ${path}\n` +
|
|
93
|
+
`- **Error:** ${err.message}\n` +
|
|
94
|
+
`\nThe research above was assembled from reads only.\n`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
15
98
|
|
|
16
99
|
export function register(server, api, AUTH) {
|
|
17
100
|
|
|
@@ -63,10 +146,14 @@ export function register(server, api, AUTH) {
|
|
|
63
146
|
// -----------------------------------------------------------------------
|
|
64
147
|
server.tool(
|
|
65
148
|
'research_company',
|
|
66
|
-
'Deep research report on a company: firmographics, tech stack, funding, news, competitors, intent signals. Orchestrates 5+ API calls into a structured report.'
|
|
149
|
+
'Deep research report on a company: firmographics, tech stack, funding, news, competitors, intent signals. Orchestrates 5+ API calls into a structured report.'
|
|
150
|
+
+ ' The reads always run. The enrichment leg is a governed write:'
|
|
151
|
+
+ COMPANY_ENRICH_COST_SENTENCE
|
|
152
|
+
+ governedWriteNote('write:companies'),
|
|
67
153
|
{
|
|
68
154
|
companyId: z.string().optional().describe('Company ID'),
|
|
69
155
|
name: z.string().optional().describe('Company name to search'),
|
|
156
|
+
...governedWriteArgs(z),
|
|
70
157
|
},
|
|
71
158
|
async (args) => {
|
|
72
159
|
try {
|
|
@@ -86,14 +173,22 @@ export function register(server, api, AUTH) {
|
|
|
86
173
|
return mdError('Provide companyId or name');
|
|
87
174
|
}
|
|
88
175
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
176
|
+
// Reads run unconditionally. The enrich POST is a governed write and
|
|
177
|
+
// previews by default, so a research call costs zero vendor credits
|
|
178
|
+
// unless the caller explicitly authorised the spend.
|
|
179
|
+
const [signals, competitors] = await Promise.all([
|
|
92
180
|
api('GET', '/api/v1/intent-signals', { params: { companyId } }).catch(() => ({ data: [] })),
|
|
93
181
|
api('GET', '/api/v1/competitors', { params: { companyId } }).catch(() => ({ data: [] })),
|
|
94
182
|
]);
|
|
95
183
|
|
|
96
|
-
|
|
184
|
+
const { enrichment, section } = await governedEnrich(api, args, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
path: `/api/v1/companies/${companyId}/enrich`,
|
|
187
|
+
kind: 'company',
|
|
188
|
+
subjectId: companyId,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return md(formatCompanyResearch(company, enrichment, signals, competitors) + section);
|
|
97
192
|
} catch (err) {
|
|
98
193
|
return mdError('Research failed', err.message);
|
|
99
194
|
}
|
|
@@ -105,11 +200,15 @@ export function register(server, api, AUTH) {
|
|
|
105
200
|
// -----------------------------------------------------------------------
|
|
106
201
|
server.tool(
|
|
107
202
|
'research_person',
|
|
108
|
-
'Deep research on a person: professional profile, communication preferences, intro paths, recent activity. Orchestrates 3+ API calls.'
|
|
203
|
+
'Deep research on a person: professional profile, communication preferences, intro paths, recent activity. Orchestrates 3+ API calls.'
|
|
204
|
+
+ ' The reads always run. The enrichment leg is a governed write:'
|
|
205
|
+
+ PERSON_ENRICH_COST_SENTENCE
|
|
206
|
+
+ governedWriteNote('write:people'),
|
|
109
207
|
{
|
|
110
208
|
personId: z.string().optional().describe('Person ID'),
|
|
111
209
|
name: z.string().optional().describe('Person name to search'),
|
|
112
210
|
email: z.string().optional().describe('Email to search by'),
|
|
211
|
+
...governedWriteArgs(z),
|
|
113
212
|
},
|
|
114
213
|
async (args) => {
|
|
115
214
|
try {
|
|
@@ -132,13 +231,20 @@ export function register(server, api, AUTH) {
|
|
|
132
231
|
return mdError('Provide personId, name, or email');
|
|
133
232
|
}
|
|
134
233
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
234
|
+
// Reads run unconditionally; the enrich POST is governed and previews
|
|
235
|
+
// by default. A Coresignal collect is 20 credits per person, so an
|
|
236
|
+
// ungoverned research call was a per-invocation vendor charge.
|
|
237
|
+
const actions = await api('GET', '/api/v1/actions', { params: { personId, limit: 10, page: 1 } })
|
|
238
|
+
.catch(() => ({ data: [] }));
|
|
239
|
+
|
|
240
|
+
const { enrichment, section } = await governedEnrich(api, args, {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
path: `/api/v1/people/${personId}/enrich`,
|
|
243
|
+
kind: 'person',
|
|
244
|
+
subjectId: personId,
|
|
245
|
+
});
|
|
140
246
|
|
|
141
|
-
return md(formatPersonResearch(person, enrichment, actions));
|
|
247
|
+
return md(formatPersonResearch(person, enrichment, actions) + section);
|
|
142
248
|
} catch (err) {
|
|
143
249
|
return mdError('Research failed', err.message);
|
|
144
250
|
}
|