@adrata/adrata-mcp 1.0.43 → 1.0.46
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/api-bridge.js +24 -0
- package/package.json +1 -1
- package/server.js +28 -15
- package/server.json +2 -2
- package/tools/free-search.js +131 -9
package/api-bridge.js
CHANGED
|
@@ -24,6 +24,17 @@ const BLOCKED_SURFACES = {
|
|
|
24
24
|
webhooks: ['/api/v1/webhooks'],
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// Families the bridge may READ but must never WRITE, whatever the caller
|
|
28
|
+
// asserts. `/api/v1/ai-crm-tools` mounts the governed dispatcher (`/execute`),
|
|
29
|
+
// whose live writes need a server-issued single-use confirmation token that a
|
|
30
|
+
// machine principal cannot mint. It used to also mount ungated REST twins of
|
|
31
|
+
// the CRM write tools (`/bulk/delete`, `/company/delete`, ...), and a
|
|
32
|
+
// model-supplied `approved:true` here was the only thing between a
|
|
33
|
+
// prompt-injected agent and a 100-record soft delete. A client boolean is not
|
|
34
|
+
// authority for a destructive or bulk operation, so writes on this family are
|
|
35
|
+
// refused outright: use adrata_ai_tool_execute, which speaks the token flow.
|
|
36
|
+
const WRITE_BLOCKED_PREFIXES = ['/api/v1/ai-crm-tools'];
|
|
37
|
+
|
|
27
38
|
const ALLOWED_PREFIXES = [
|
|
28
39
|
'/api/v1/actions',
|
|
29
40
|
'/api/v1/action-columns',
|
|
@@ -395,6 +406,18 @@ export function validateApiBridgeRequest({
|
|
|
395
406
|
}
|
|
396
407
|
|
|
397
408
|
const isWrite = WRITE_METHODS.has(normalizedMethod);
|
|
409
|
+
if (
|
|
410
|
+
isWrite
|
|
411
|
+
&& WRITE_BLOCKED_PREFIXES.some((prefix) =>
|
|
412
|
+
matchesPathPrefix(toScopeClassificationPath(normalizedPath), prefix))
|
|
413
|
+
) {
|
|
414
|
+
// Checked before the dry-run branch so a preview cannot report
|
|
415
|
+
// `wouldSend: true` for a write this bridge will never send.
|
|
416
|
+
throw new Error(
|
|
417
|
+
`writes to ${normalizedPath} are not available through adrata_api_request; `
|
|
418
|
+
+ 'use adrata_ai_tool_execute, whose live writes require a server-issued confirmation token',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
398
421
|
if (isWrite && dryRun !== false) {
|
|
399
422
|
// A dry-run used to answer `wouldSend: true` unconditionally, which is the
|
|
400
423
|
// single most misleading thing this tool could say: it reported success for
|
|
@@ -588,6 +611,7 @@ export function apiBridgeCatalog() {
|
|
|
588
611
|
aiToolDispatcher:
|
|
589
612
|
'Use adrata_ai_tool_catalog and adrata_ai_tool_execute for the same Channels, external catalogs, ICP ranking, and batch import tools Adrata chat uses.',
|
|
590
613
|
blocked: BLOCKED_PREFIXES,
|
|
614
|
+
writeBlocked: WRITE_BLOCKED_PREFIXES,
|
|
591
615
|
blockedSurfaces: BLOCKED_SURFACES,
|
|
592
616
|
allowed: ALLOWED_PREFIXES,
|
|
593
617
|
examples: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrata/adrata-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. About 275 tools registered at startup for companies, people, deals, actions, buyer groups, warm intros, webhooks and intelligence, plus 65 more behind eight named toolsets you load with enable_toolset.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
package/server.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
getValidAgentToken,
|
|
41
41
|
} from './access/auth.js';
|
|
42
42
|
import { TIERS } from './access/tiers.js';
|
|
43
|
-
import { findCompany, findPerson } from './tools/free-search.js';
|
|
43
|
+
import { findCompany, findPerson, findCompanyInWorkspace, findPersonInWorkspace } from './tools/free-search.js';
|
|
44
44
|
import { applySecurityLayer } from './security.js';
|
|
45
45
|
import { describeEdgeBlock } from './http/edge-block.js';
|
|
46
46
|
import { createRateLimitRegistry } from './http/rate-limit.js';
|
|
@@ -634,15 +634,15 @@ server.tool('describe_capability',
|
|
|
634
634
|
async (args) => ok({ capability: await resolveExactCapability(args.ref) }));
|
|
635
635
|
|
|
636
636
|
server.tool('run_capability',
|
|
637
|
-
'Run one exact capability through the governed dispatcher. Defaults to dry-run. Live writes remain subject to server-bound approval, reason,
|
|
637
|
+
'Run one exact capability through the governed dispatcher. Defaults to dry-run. Reads, including paid provider reads and enrichment, run directly. Live writes remain subject to server-bound approval, reason, policy, and idempotency controls. Product profiles refuse references outside their namespace.',
|
|
638
638
|
{
|
|
639
639
|
ref: z.string().describe('Exact /product/domain/action reference returned by search/describe.'),
|
|
640
640
|
arguments: z.record(z.unknown()).optional(),
|
|
641
641
|
dryRun: z.boolean().optional().describe('Defaults true. Set false only after reviewing the exact schema and preview.'),
|
|
642
642
|
reason: z.string().optional(),
|
|
643
643
|
confirmationToken: z.string().optional().describe('Single-use server-bound token when the capability requires one.'),
|
|
644
|
-
confirmSpend: z.boolean().optional(),
|
|
645
|
-
idempotencyKey: z.string().optional().describe('Required by governed live writes
|
|
644
|
+
confirmSpend: z.boolean().optional().describe('Deprecated no-op: paid reads no longer need a spend acknowledgment.'),
|
|
645
|
+
idempotencyKey: z.string().optional().describe('Required by governed live writes. Reuse the same key on retry.'),
|
|
646
646
|
},
|
|
647
647
|
async (args) => {
|
|
648
648
|
const capability = await resolveExactCapability(args.ref);
|
|
@@ -741,16 +741,16 @@ server.tool('adrata_ai_tool_catalog',
|
|
|
741
741
|
async () => ok(await api('GET', '/api/v1/ai-crm-tools/catalog')));
|
|
742
742
|
|
|
743
743
|
server.tool('adrata_ai_tool_execute',
|
|
744
|
-
'Execute the same governed AI CRM ToolDispatcher path used by Adrata chat.
|
|
744
|
+
'Execute the same governed AI CRM ToolDispatcher path used by Adrata chat. Reads run directly, including paid provider reads and enrichment. Writes use a two-step, server-bound confirmationToken flow; a client approved boolean can never authorize a write. A non-interactive machine/OAuth principal (every MCP client) can preview writes but cannot complete one.',
|
|
745
745
|
{
|
|
746
746
|
toolName: z.string().describe('AI CRM tool name from adrata_ai_tool_catalog, e.g. list_channels or rank_companies_by_icp'),
|
|
747
747
|
arguments: z.record(z.unknown()).optional(),
|
|
748
748
|
dryRun: z.boolean().optional().describe('Defaults to true for write tools. Set false only after explicit approval.'),
|
|
749
749
|
approved: z.boolean().optional().describe('Deprecated compatibility field; never authorizes a live write.'),
|
|
750
|
-
reason: z.string().optional().describe('Required audit reason
|
|
750
|
+
reason: z.string().optional().describe('Required audit reason when requesting a live-write confirmation token.'),
|
|
751
751
|
confirmationToken: z.string().optional().describe('Single-use server token returned by the first live-write request; bound to the user, workspace, tool, and stored arguments.'),
|
|
752
|
-
confirmSpend: z.boolean().optional().describe('
|
|
753
|
-
idempotencyKey: z.string().optional().describe('
|
|
752
|
+
confirmSpend: z.boolean().optional().describe('Deprecated no-op: paid reads no longer need a spend acknowledgment. Never authorizes a workspace write.'),
|
|
753
|
+
idempotencyKey: z.string().optional().describe('Optional. Reuse the same key on retry.'),
|
|
754
754
|
},
|
|
755
755
|
async (args) => {
|
|
756
756
|
try {
|
|
@@ -1123,17 +1123,30 @@ server.tool('move_pipeline_card',
|
|
|
1123
1123
|
}
|
|
1124
1124
|
});
|
|
1125
1125
|
|
|
1126
|
-
// =====
|
|
1126
|
+
// ===== find_company / find_person =====
|
|
1127
|
+
//
|
|
1128
|
+
// A seat with a credential gets its WORKSPACE RECORD (found / not found / could
|
|
1129
|
+
// not check). Only a server with no credential at all falls back to the
|
|
1130
|
+
// training-data profile prompt, which carries no upsell. See tools/free-search.js.
|
|
1131
|
+
|
|
1132
|
+
function hasWorkspaceCredential() {
|
|
1133
|
+
const auth = currentAuth();
|
|
1134
|
+
return Boolean(auth?.token || auth?.apiKey);
|
|
1135
|
+
}
|
|
1127
1136
|
|
|
1128
1137
|
server.tool('find_company',
|
|
1129
|
-
'
|
|
1130
|
-
{ name: z.string().describe('Company name
|
|
1131
|
-
async (a) =>
|
|
1138
|
+
'Find a company. Connected to a workspace, returns that workspace\'s company record (or says plainly that none matches, or that the workspace could not be checked). With no workspace connected, returns an unverified profile from Claude\'s training data.',
|
|
1139
|
+
{ name: z.string().describe('Company name or domain (e.g. "Stripe", "stripe.com")') },
|
|
1140
|
+
async (a) => (hasWorkspaceCredential()
|
|
1141
|
+
? findCompanyInWorkspace(a.name, (query) => api('GET', '/api/v1/companies', { params: { search: query, limit: 25, page: 1 } }))
|
|
1142
|
+
: findCompany(a.name)));
|
|
1132
1143
|
|
|
1133
1144
|
server.tool('find_person',
|
|
1134
|
-
'
|
|
1135
|
-
{ name: z.string().describe('Full name of the person
|
|
1136
|
-
async (a) =>
|
|
1145
|
+
'Find a person. Connected to a workspace, returns that workspace\'s person record (or says plainly that none matches, or that the workspace could not be checked). With no workspace connected, returns an unverified profile from Claude\'s training data.',
|
|
1146
|
+
{ name: z.string().describe('Full name or email of the person (e.g. "Patrick Collison")') },
|
|
1147
|
+
async (a) => (hasWorkspaceCredential()
|
|
1148
|
+
? findPersonInWorkspace(a.name, (query) => api('GET', '/api/v1/people', { params: { search: query, limit: 25, page: 1 } }))
|
|
1149
|
+
: findPerson(a.name)));
|
|
1137
1150
|
|
|
1138
1151
|
// ===== GOVERNED WRITES (shared) =====
|
|
1139
1152
|
//
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "com.adrata/adrata-mcp",
|
|
4
4
|
"description": "Adrata revenue-intelligence MCP server: companies, people, opportunities, actions, buyer groups, enrichment, email, and workspace operations for AI agents.",
|
|
5
5
|
"status": "active",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.46",
|
|
7
7
|
"websiteUrl": "https://adrata.com/developers",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/adrata/adrata",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
17
|
"identifier": "@adrata/adrata-mcp",
|
|
18
|
-
"version": "1.0.
|
|
18
|
+
"version": "1.0.46",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
},
|
package/tools/free-search.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* find_company / find_person
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Connected to a workspace, these return the WORKSPACE RECORD — the thing a
|
|
5
|
+
* seller asking "find Acme" means. Measured on Colin's day-one audit
|
|
6
|
+
* (2026-09-12): a connected seat got back a raw model prompt ending "Upgrade to
|
|
7
|
+
* Adrata Pro for enriched intelligence" instead of the record sitting in its own
|
|
8
|
+
* CRM. Owner ruling 2026-09-11: no seat sees credits, prices, quotas or upsells.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
10
|
+
* Three states, never two:
|
|
11
|
+
* - found: the workspace record(s), labelled as such;
|
|
12
|
+
* - not found: said plainly — the workspace was checked and has no match;
|
|
13
|
+
* - could not look: said plainly — nothing was ruled out.
|
|
14
|
+
*
|
|
15
|
+
* With no credential at all there is no workspace to check, so the tool falls
|
|
16
|
+
* back to a structured training-data profile prompt, labelled unverified, with
|
|
17
|
+
* no upsell in it.
|
|
9
18
|
*/
|
|
10
19
|
|
|
11
20
|
// ---------------------------------------------------------------------------
|
|
@@ -56,8 +65,7 @@ IMPORTANT: Respond with ONLY a valid JSON object matching this exact schema. No
|
|
|
56
65
|
"competitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
|
57
66
|
"recent_highlights": "Notable recent developments, funding, acquisitions, or strategic moves from training data",
|
|
58
67
|
"ideal_sales_angles": "2-3 sentence suggestion for how a seller might approach this company",
|
|
59
|
-
"data_freshness": "Based on training data through early 2025. Some details may have changed."
|
|
60
|
-
"upgrade_prompt": "Want verified contacts, real-time headcount, funding rounds, hiring signals, and intent data? Upgrade to Adrata Pro for enriched intelligence."
|
|
68
|
+
"data_freshness": "Based on training data through early 2025. Some details may have changed."
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
Rules:
|
|
@@ -84,8 +92,7 @@ IMPORTANT: Respond with ONLY a valid JSON object matching this exact schema. No
|
|
|
84
92
|
"notable_achievements": "Key accomplishments, board seats, speaking engagements, publications",
|
|
85
93
|
"linkedin_likely": "Likely LinkedIn URL pattern (e.g. linkedin.com/in/firstname-lastname) — note: unverified",
|
|
86
94
|
"sales_context": "2-3 sentence suggestion for how a seller might approach this person — what they likely care about",
|
|
87
|
-
"data_freshness": "Based on training data through early 2025. Current role may have changed."
|
|
88
|
-
"upgrade_prompt": "Want verified email, phone, social profiles, recent activity, and buyer signals? Upgrade to Adrata Pro for enriched contact intelligence."
|
|
95
|
+
"data_freshness": "Based on training data through early 2025. Current role may have changed."
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
Rules:
|
|
@@ -158,3 +165,118 @@ export function findPerson(name) {
|
|
|
158
165
|
cacheSet(cacheKey, result);
|
|
159
166
|
return result;
|
|
160
167
|
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Workspace-record lookup (connected seats)
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const WORKSPACE_MATCH_LIMIT = 5;
|
|
174
|
+
|
|
175
|
+
/** Pull the record array out of the API's list envelope, whatever its shape. */
|
|
176
|
+
export function recordsFromListResponse(response, collection) {
|
|
177
|
+
const candidates = [
|
|
178
|
+
response,
|
|
179
|
+
response?.data,
|
|
180
|
+
response?.data?.[collection],
|
|
181
|
+
response?.data?.items,
|
|
182
|
+
response?.data?.data,
|
|
183
|
+
response?.[collection],
|
|
184
|
+
response?.items,
|
|
185
|
+
];
|
|
186
|
+
for (const candidate of candidates) {
|
|
187
|
+
if (Array.isArray(candidate)) return candidate;
|
|
188
|
+
}
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalize(value) {
|
|
193
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function recordName(record, kind) {
|
|
197
|
+
if (kind === 'person') {
|
|
198
|
+
return (
|
|
199
|
+
record?.fullName ||
|
|
200
|
+
record?.name ||
|
|
201
|
+
[record?.firstName, record?.lastName].filter(Boolean).join(' ')
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return record?.name;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function exactMatches(records, query, kind) {
|
|
208
|
+
const wanted = normalize(query);
|
|
209
|
+
return records.filter((record) => {
|
|
210
|
+
if (normalize(recordName(record, kind)) === wanted) return true;
|
|
211
|
+
if (kind === 'company') {
|
|
212
|
+
return [record?.domain, record?.website].some((d) => normalize(d).replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '') === wanted);
|
|
213
|
+
}
|
|
214
|
+
return normalize(record?.email) === wanted;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function textResult(payload, isError = false) {
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
221
|
+
...(isError ? { isError: true } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Look the name up in the connected workspace and return what is there.
|
|
227
|
+
*
|
|
228
|
+
* `lookup(name)` performs the authenticated list read and resolves to the raw
|
|
229
|
+
* API response; it is injected so this stays testable without a server.
|
|
230
|
+
*/
|
|
231
|
+
async function findInWorkspace(kind, name, lookup) {
|
|
232
|
+
const label = kind === 'company' ? 'company' : 'person';
|
|
233
|
+
const query = String(name ?? '').trim();
|
|
234
|
+
let response;
|
|
235
|
+
try {
|
|
236
|
+
response = await lookup(query);
|
|
237
|
+
} catch {
|
|
238
|
+
// The honest third state. Never an empty result, never "not found".
|
|
239
|
+
return textResult(
|
|
240
|
+
{
|
|
241
|
+
source: 'workspace',
|
|
242
|
+
status: 'could_not_check',
|
|
243
|
+
query,
|
|
244
|
+
message: `Couldn't check the workspace for a ${label} named "${query}" right now, so nothing was ruled out. Try again in a moment.`,
|
|
245
|
+
},
|
|
246
|
+
true,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const records = recordsFromListResponse(response, kind === 'company' ? 'companies' : 'people');
|
|
251
|
+
if (records.length === 0) {
|
|
252
|
+
return textResult({
|
|
253
|
+
source: 'workspace',
|
|
254
|
+
status: 'not_found',
|
|
255
|
+
query,
|
|
256
|
+
message: `No ${label} matching "${query}" is in this workspace.`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const exact = exactMatches(records, query, kind);
|
|
261
|
+
const matches = (exact.length > 0 ? exact : records).slice(0, WORKSPACE_MATCH_LIMIT);
|
|
262
|
+
return textResult({
|
|
263
|
+
source: 'workspace',
|
|
264
|
+
status: exact.length > 0 ? 'found' : 'similar_found',
|
|
265
|
+
query,
|
|
266
|
+
message:
|
|
267
|
+
exact.length > 0
|
|
268
|
+
? `Found ${exact.length === 1 ? 'the' : exact.length} workspace ${label} record${exact.length === 1 ? '' : 's'} for "${query}".`
|
|
269
|
+
: `No exact match for "${query}"; these workspace ${label} records are the closest.`,
|
|
270
|
+
[kind === 'company' ? 'companies' : 'people']: matches,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** find_company for a seat with a credential: the workspace record, three-state. */
|
|
275
|
+
export function findCompanyInWorkspace(name, lookup) {
|
|
276
|
+
return findInWorkspace('company', name, lookup);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** find_person for a seat with a credential: the workspace record, three-state. */
|
|
280
|
+
export function findPersonInWorkspace(name, lookup) {
|
|
281
|
+
return findInWorkspace('person', name, lookup);
|
|
282
|
+
}
|