@adrata/adrata-mcp 1.0.43 → 1.0.44

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adrata/adrata-mcp",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
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",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node server.js",
12
- "test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js security.cap-contract.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js toolsets/prospecting.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/launches/work-launch-tools.test.js tools/work-hub/audit.test.js tools/work-hub/field-changes.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/provisioning/onboarding-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
12
+ "test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js security.cap-contract.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js toolsets/prospecting.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/launches/work-launch-tools.test.js tools/work-hub/audit.test.js tools/work-hub/field-changes.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/provisioning/onboarding-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js tools/free-search.test.js"
13
13
  },
14
14
  "keywords": [
15
15
  "mcp",
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';
@@ -1123,17 +1123,30 @@ server.tool('move_pipeline_card',
1123
1123
  }
1124
1124
  });
1125
1125
 
1126
- // ===== FREE TIER: Claude-powered search (zero cost to Adrata) =====
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
- 'Search for a company using Claude\'s training data. Returns a structured profile with industry, key people, estimated size, competitors, and sales angles. Free tier — no API key needed. For verified real-time data, upgrade to Pro.',
1130
- { name: z.string().describe('Company name to research (e.g. "Stripe", "Snowflake", "Datadog")') },
1131
- async (a) => findCompany(a.name));
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
- 'Search for a professional using Claude\'s training data. Returns a structured profile with title, company, background, expertise, and sales context. Free tier no API key needed. For verified contact info and buyer signals, upgrade to Pro.',
1135
- { name: z.string().describe('Full name of the person to research (e.g. "Patrick Collison", "Jensen Huang")') },
1136
- async (a) => findPerson(a.name));
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.43",
6
+ "version": "1.0.44",
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.43",
18
+ "version": "1.0.44",
19
19
  "transport": {
20
20
  "type": "stdio"
21
21
  },
@@ -1,11 +1,20 @@
1
1
  /**
2
- * Free Tier Claude-Powered Search
2
+ * find_company / find_person
3
3
  *
4
- * These tools return structured prompts that guide Claude (the LLM running the
5
- * MCP client) to search its own training data for company and person profiles.
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
- * Cost to Adrata: $0 — Claude's own knowledge does the work.
8
- * No API key needed, no account needed, unlimited usage.
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
+ }