@adrata/adrata-mcp 1.0.2 → 1.0.6
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 +29 -1
- package/access/auth.js +49 -1
- package/access/oauth.js +355 -26
- package/access/resource-metadata.js +9 -1
- package/access/tiers.js +17 -0
- package/analytics.js +141 -20
- package/api-bridge.js +16 -0
- package/package.json +2 -2
- package/server.js +109 -16
- package/server.json +14 -2
- package/skills/qa-the-card/SKILL.md +179 -0
- package/skills/ship-the-card/SKILL.md +31 -5
- package/tool-annotations.js +15 -1
- package/tools/source-control/connection-tools.js +31 -0
- package/tools/work-board-tools.js +1174 -30
- package/tools/work-hub/audit.js +17 -7
- package/toolsets/revenue/competitive-coverage.js +164 -0
- package/transport-http.js +30 -9
package/access/tiers.js
CHANGED
|
@@ -218,22 +218,39 @@ export const TOOL_TIERS = {
|
|
|
218
218
|
// the tool — only exists on an OAuth token.
|
|
219
219
|
list_my_work_items: TIERS.ENTERPRISE,
|
|
220
220
|
list_work_boards: TIERS.ENTERPRISE,
|
|
221
|
+
list_source_control_connections: TIERS.ENTERPRISE,
|
|
222
|
+
get_source_control_connection_events: TIERS.ENTERPRISE,
|
|
221
223
|
get_work_item_delivery_evidence: TIERS.ENTERPRISE,
|
|
222
224
|
audit_work_hub: TIERS.ENTERPRISE,
|
|
223
225
|
list_work_item_acceptance_criteria: TIERS.ENTERPRISE,
|
|
226
|
+
list_work_item_qa_evidence: TIERS.ENTERPRISE,
|
|
224
227
|
get_work_board: TIERS.ENTERPRISE,
|
|
225
228
|
get_work_item: TIERS.ENTERPRISE,
|
|
229
|
+
get_work_item_worker_lease: TIERS.ENTERPRISE,
|
|
230
|
+
get_work_item_worker_activity: TIERS.ENTERPRISE,
|
|
231
|
+
claim_work_item_qa_pass: TIERS.ENTERPRISE,
|
|
232
|
+
claim_next_work_item_qa_pass: TIERS.ENTERPRISE,
|
|
233
|
+
heartbeat_work_item_qa_pass: TIERS.ENTERPRISE,
|
|
234
|
+
release_work_item_qa_pass: TIERS.ENTERPRISE,
|
|
235
|
+
record_work_item_qa_failure_and_release: TIERS.ENTERPRISE,
|
|
236
|
+
requeue_work_item_qa_after_fix: TIERS.ENTERPRISE,
|
|
226
237
|
get_work_item_history: TIERS.ENTERPRISE,
|
|
227
238
|
get_work_item_comments: TIERS.ENTERPRISE,
|
|
228
239
|
get_work_board_rollup: TIERS.ENTERPRISE,
|
|
229
240
|
list_work_board_rollups: TIERS.ENTERPRISE,
|
|
230
241
|
set_work_board_archived: TIERS.ENTERPRISE,
|
|
231
242
|
set_work_board_column_wip_limit: TIERS.ENTERPRISE,
|
|
243
|
+
attach_work_item_qa_evidence: TIERS.ENTERPRISE,
|
|
244
|
+
verify_work_item_qa_evidence_playback: TIERS.ENTERPRISE,
|
|
232
245
|
move_work_item: TIERS.ENTERPRISE,
|
|
246
|
+
transfer_work_item_between_boards: TIERS.ENTERPRISE,
|
|
233
247
|
set_work_item_tag: TIERS.ENTERPRISE,
|
|
234
248
|
set_work_item_kind: TIERS.ENTERPRISE,
|
|
235
249
|
create_work_item: TIERS.ENTERPRISE,
|
|
236
250
|
add_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
251
|
+
satisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
252
|
+
record_work_item_criterion_engineering_proof: TIERS.ENTERPRISE,
|
|
253
|
+
unsatisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
237
254
|
comment_on_work_item: TIERS.ENTERPRISE,
|
|
238
255
|
flag_work_item: TIERS.ENTERPRISE,
|
|
239
256
|
// The containers above the cards, and the "add this to the roadmap" verb.
|
package/analytics.js
CHANGED
|
@@ -119,7 +119,6 @@ function ensureDir() {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function readLocalAnalytics() {
|
|
122
|
-
ensureDir();
|
|
123
122
|
if (!existsSync(ANALYTICS_FILE)) {
|
|
124
123
|
return { events: [], sessions: [] };
|
|
125
124
|
}
|
|
@@ -278,6 +277,69 @@ export function trackConversion(apiFn, auth, fromTier, toTier, trigger) {
|
|
|
278
277
|
/** @type {{ startTime: number, toolsUsed: Set<string>, invocationCount: number } | null} */
|
|
279
278
|
let currentSession = null;
|
|
280
279
|
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Analytics wrapper coverage
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// The analytics counter is a monkey-patch on `server.tool`, so it can only see
|
|
284
|
+
// tools registered AFTER it is installed. That makes its own coverage a fact
|
|
285
|
+
// worth reporting: a zero from an instrumented surface and a zero from an
|
|
286
|
+
// uninstrumented one are different answers, and until 2026-08-29 they were
|
|
287
|
+
// rendered identically. `countedToolNames` is what the wrapper actually
|
|
288
|
+
// wrapped; `get_mcp_analytics` compares it against what the server registered.
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Wrapper state belongs to one McpServer instance. Hosted HTTP creates a fresh
|
|
292
|
+
* server for every protocol session, so a module-global installed flag would
|
|
293
|
+
* wrap the first client and silently skip every client after it.
|
|
294
|
+
*
|
|
295
|
+
* @type {WeakMap<object, { countedToolNames: Set<string> }>}
|
|
296
|
+
*/
|
|
297
|
+
let analyticsWrapperStates = new WeakMap();
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* How much of the registered tool surface the invocation counter can see.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} [server] - McpServer instance, for the registered-tool total
|
|
303
|
+
* @returns {{installed: boolean, tools_counted: number, tools_registered: number|null, complete: boolean|null, uncounted_note: string|undefined}}
|
|
304
|
+
*/
|
|
305
|
+
export function getAnalyticsCoverage(server) {
|
|
306
|
+
const registered = registeredToolCount(server);
|
|
307
|
+
const wrapperState = server && typeof server === 'object'
|
|
308
|
+
? analyticsWrapperStates.get(server)
|
|
309
|
+
: undefined;
|
|
310
|
+
const counted = wrapperState?.countedToolNames.size ?? 0;
|
|
311
|
+
const complete = registered === null ? null : counted >= registered;
|
|
312
|
+
return {
|
|
313
|
+
installed: wrapperState !== undefined,
|
|
314
|
+
tools_counted: counted,
|
|
315
|
+
tools_registered: registered,
|
|
316
|
+
complete,
|
|
317
|
+
uncounted_note:
|
|
318
|
+
complete === false
|
|
319
|
+
? `${registered - counted} of ${registered} registered tools were registered before the `
|
|
320
|
+
+ 'analytics wrapper was installed, so their invocations are NOT counted. '
|
|
321
|
+
+ 'Treat tool_invocations as a lower bound, not a measurement.'
|
|
322
|
+
: undefined,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Best-effort read of the SDK's registered-tool map. Returns null if unavailable. */
|
|
327
|
+
function registeredToolCount(server) {
|
|
328
|
+
try {
|
|
329
|
+
const registry = server?._registeredTools;
|
|
330
|
+
if (registry && typeof registry === 'object') return Object.keys(registry).length;
|
|
331
|
+
} catch {
|
|
332
|
+
// The SDK internal is not a contract. An unknown total is reported as
|
|
333
|
+
// null, never as a number we did not measure.
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Test-only: forget that the wrapper was installed. */
|
|
339
|
+
export function __resetAnalyticsWrapperForTests() {
|
|
340
|
+
analyticsWrapperStates = new WeakMap();
|
|
341
|
+
}
|
|
342
|
+
|
|
281
343
|
/**
|
|
282
344
|
* Start a new analytics session.
|
|
283
345
|
*
|
|
@@ -388,24 +450,43 @@ const SEARCH_TOOLS = {
|
|
|
388
450
|
* invocation. This builds on top of the existing event logging in memory.js
|
|
389
451
|
* by adding richer analytics (search classification, tier gates, sessions).
|
|
390
452
|
*
|
|
391
|
-
* Must be applied AFTER the event logging wrapper from memory.js
|
|
453
|
+
* Must be applied AFTER the event logging wrapper from memory.js, and BEFORE
|
|
454
|
+
* the first `server.tool(...)` call. Monkey-patching `server.tool` can only
|
|
455
|
+
* ever reach FUTURE registrations, so installing it late does not degrade the
|
|
456
|
+
* count — it silently zeroes it. Measured 2026-08-29: installed from
|
|
457
|
+
* `registerAnalytics` ~1975 lines after the first registration, it covered 27
|
|
458
|
+
* of 254 tools and `get_mcp_analytics` reported `tool_invocations.total: 0`
|
|
459
|
+
* with no indication that 89% of the surface was never instrumented.
|
|
460
|
+
*
|
|
461
|
+
* Idempotent: calling it twice installs one wrapper, so `registerAnalytics`
|
|
462
|
+
* remains safe to call after server.js has already installed it early.
|
|
463
|
+
*
|
|
464
|
+
* `auth` may be an AUTH object or a zero-argument getter. Prefer the getter:
|
|
465
|
+
* server.js reassigns AUTH on `connect_workspace`, and an object captured at
|
|
466
|
+
* install time would report the pre-connection tier forever.
|
|
392
467
|
*
|
|
393
468
|
* @param {object} server - McpServer instance
|
|
394
469
|
* @param {Function} apiFn - The api() helper
|
|
395
|
-
* @param {object} auth - AUTH context
|
|
470
|
+
* @param {object|Function} auth - AUTH context, or a getter returning it
|
|
396
471
|
* @param {Function} checkToolAccessFn - checkToolAccess from auth.js
|
|
397
472
|
*/
|
|
398
473
|
export function wrapWithAnalytics(server, apiFn, auth, checkToolAccessFn) {
|
|
399
474
|
if (!TELEMETRY_ENABLED) return;
|
|
475
|
+
if (analyticsWrapperStates.has(server)) return;
|
|
476
|
+
const wrapperState = { countedToolNames: new Set() };
|
|
477
|
+
analyticsWrapperStates.set(server, wrapperState);
|
|
400
478
|
|
|
479
|
+
const resolveAuth = () => (typeof auth === 'function' ? auth() : auth);
|
|
401
480
|
const _analyticsWrappedTool = server.tool.bind(server);
|
|
402
481
|
|
|
403
482
|
server.tool = function analyticsTool(name, ...rest) {
|
|
404
483
|
const handler = rest[rest.length - 1];
|
|
484
|
+
wrapperState.countedToolNames.add(name);
|
|
405
485
|
rest[rest.length - 1] = async function analyticsHandler(...handlerArgs) {
|
|
406
486
|
const start = Date.now();
|
|
407
487
|
let success = true;
|
|
408
488
|
let errorMsg = null;
|
|
489
|
+
const auth = resolveAuth();
|
|
409
490
|
|
|
410
491
|
// Track tier gate if access was denied (check without modifying flow)
|
|
411
492
|
const access = checkToolAccessFn(name, auth);
|
|
@@ -574,16 +655,18 @@ function parsePeriod(period) {
|
|
|
574
655
|
* @param {object} opts - { z, api, auth, ok, checkToolAccess }
|
|
575
656
|
*/
|
|
576
657
|
export function registerAnalytics(server, { z, api, auth, ok, checkToolAccess }) {
|
|
658
|
+
const resolveAuth = () => (typeof auth === 'function' ? auth() : auth);
|
|
659
|
+
|
|
577
660
|
// Wire up the analytics wrapper on all future tool registrations
|
|
578
661
|
wrapWithAnalytics(server, api, auth, checkToolAccess);
|
|
579
662
|
|
|
580
663
|
// Start a session
|
|
581
|
-
startSession(api,
|
|
664
|
+
startSession(api, resolveAuth());
|
|
582
665
|
|
|
583
666
|
// Graceful shutdown: end session on process exit
|
|
584
|
-
process.on('beforeExit', () => endSession(api,
|
|
585
|
-
process.on('SIGINT', () => { endSession(api,
|
|
586
|
-
process.on('SIGTERM', () => { endSession(api,
|
|
667
|
+
process.on('beforeExit', () => endSession(api, resolveAuth()));
|
|
668
|
+
process.on('SIGINT', () => { endSession(api, resolveAuth()); process.exit(0); });
|
|
669
|
+
process.on('SIGTERM', () => { endSession(api, resolveAuth()); process.exit(0); });
|
|
587
670
|
|
|
588
671
|
// ----- get_mcp_analytics -----
|
|
589
672
|
server.tool('get_mcp_analytics',
|
|
@@ -594,27 +677,65 @@ export function registerAnalytics(server, { z, api, auth, ok, checkToolAccess })
|
|
|
594
677
|
async (args) => {
|
|
595
678
|
const period = args.period || '7d';
|
|
596
679
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
680
|
+
// Coverage travels with every answer. `tool_invocations.total: 0` from a
|
|
681
|
+
// fully instrumented surface means nobody called anything; the same zero
|
|
682
|
+
// from a surface the wrapper never reached means nothing was measured.
|
|
683
|
+
// Reporting the second as the first is how this counter read 0 for 227
|
|
684
|
+
// of 254 tools without anyone noticing.
|
|
685
|
+
const coverage = getAnalyticsCoverage(server);
|
|
686
|
+
|
|
687
|
+
if (resolveAuth().authenticated) {
|
|
688
|
+
// Authenticated hosted sessions persist invocation rows in the API,
|
|
689
|
+
// not ~/.adrata. Reading the local dashboard here used to both report
|
|
690
|
+
// zero and attempt to mkdir /home/adrata/.adrata in the read-only ECS
|
|
691
|
+
// image. The profile endpoint is the canonical named-tool rollup.
|
|
601
692
|
try {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
693
|
+
const profileData = await api('GET', '/api/v1/mcp/profile');
|
|
694
|
+
const profile = profileData?.data || profileData || {};
|
|
695
|
+
const stats = profile.stats || {};
|
|
696
|
+
const topTools = Array.isArray(stats.top_tools)
|
|
697
|
+
? stats.top_tools.map((entry) => ({
|
|
698
|
+
name: entry.name || entry.tool,
|
|
699
|
+
count: entry.count,
|
|
700
|
+
}))
|
|
701
|
+
: [];
|
|
702
|
+
|
|
703
|
+
let usage = null;
|
|
704
|
+
try {
|
|
705
|
+
const subscriptionData = await api('GET', '/api/v1/mcp/subscription');
|
|
706
|
+
const subscription = subscriptionData?.data || subscriptionData || {};
|
|
707
|
+
usage = subscription.usage || null;
|
|
708
|
+
} catch {
|
|
709
|
+
// Named invocation analytics remain valid when billing is absent.
|
|
710
|
+
}
|
|
711
|
+
|
|
604
712
|
return ok({
|
|
605
713
|
period,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
714
|
+
coverage,
|
|
715
|
+
source: 'server',
|
|
716
|
+
server_scope: 'all_time',
|
|
717
|
+
period_note: 'The server profile currently reports all-time named invocation totals; period applies only to the local fallback.',
|
|
718
|
+
tool_invocations: {
|
|
719
|
+
total: Number(stats.total_queries || 0),
|
|
720
|
+
success: null,
|
|
721
|
+
errors: null,
|
|
722
|
+
avg_latency_ms: null,
|
|
723
|
+
top_tools: topTools,
|
|
724
|
+
},
|
|
725
|
+
server: profile,
|
|
726
|
+
usage,
|
|
727
|
+
server_events_30d: usage ? usage.tool_invocations_30d : null,
|
|
609
728
|
});
|
|
610
729
|
} catch {
|
|
611
|
-
//
|
|
612
|
-
|
|
730
|
+
// Offline-compatible fallback. readLocalAnalytics does not create a
|
|
731
|
+
// directory merely to answer an empty read, so this cannot break a
|
|
732
|
+
// read-only hosted container either.
|
|
733
|
+
return ok({ ...getLocalDashboard(period), coverage, source: 'local_fallback' });
|
|
613
734
|
}
|
|
614
735
|
}
|
|
615
736
|
|
|
616
737
|
// Free tier: local analytics only
|
|
617
|
-
return ok(getLocalDashboard(period));
|
|
738
|
+
return ok({ ...getLocalDashboard(period), coverage });
|
|
618
739
|
}
|
|
619
740
|
);
|
|
620
741
|
|
|
@@ -627,7 +748,7 @@ export function registerAnalytics(server, { z, api, auth, ok, checkToolAccess })
|
|
|
627
748
|
trigger: z.string().optional().describe('What triggered the conversion'),
|
|
628
749
|
},
|
|
629
750
|
async (args) => {
|
|
630
|
-
trackConversion(api,
|
|
751
|
+
trackConversion(api, resolveAuth(), args.from_tier, args.to_tier, args.trigger || 'manual');
|
|
631
752
|
return ok({ tracked: true, from: args.from_tier, to: args.to_tier });
|
|
632
753
|
}
|
|
633
754
|
);
|
package/api-bridge.js
CHANGED
|
@@ -171,6 +171,14 @@ const ALLOWED_PREFIXES = [
|
|
|
171
171
|
'/api/v1/retention',
|
|
172
172
|
'/api/v1/revenue',
|
|
173
173
|
'/api/v1/revenue-cloud',
|
|
174
|
+
// Source control connections and their repository bindings. The board
|
|
175
|
+
// cannot learn that a pull request merged unless something can create the
|
|
176
|
+
// connection and register the callback, and until this row existed the
|
|
177
|
+
// bridge refused every /scm path — so the receiver at /webhooks/scm sat
|
|
178
|
+
// mounted and reachable with no connection ever pointing at it.
|
|
179
|
+
// The unauthenticated receiver itself is NOT here: it is mounted below the
|
|
180
|
+
// auth layer and is not an agent-callable path.
|
|
181
|
+
'/api/v1/scm',
|
|
174
182
|
'/api/v1/scoring',
|
|
175
183
|
'/api/v1/security',
|
|
176
184
|
'/api/v1/self-service',
|
|
@@ -279,6 +287,14 @@ const PATH_WRITE_SCOPES = [
|
|
|
279
287
|
// All three are listed separately because `matchesPathPrefix` is
|
|
280
288
|
// segment-boundary matched: `/work-boards` does not annex
|
|
281
289
|
// `/work-board-rollups`.
|
|
290
|
+
// Authorising a source-control account is the same act as connecting a CRM —
|
|
291
|
+
// an external system permitted to write into the workspace — so scope_guard
|
|
292
|
+
// maps it onto the integrations family rather than a new one. Deliberately
|
|
293
|
+
// not an `admin:` scope: those refuse first-party bypass and a human session
|
|
294
|
+
// JWT carries an empty permission list, which would 403 every person opening
|
|
295
|
+
// the Connections screen. Mirrors `family_matches(path, "/scm")` in
|
|
296
|
+
// code/api/crates/middleware/src/scope_guard/mod.rs.
|
|
297
|
+
['/api/v1/scm', 'write:integrations'],
|
|
282
298
|
['/api/v1/work-boards', 'write:tasks'],
|
|
283
299
|
['/api/v1/work-items', 'write:tasks'],
|
|
284
300
|
['/api/v1/work-board-rollups', 'write:tasks'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrata/adrata-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. 80+ tools for companies, people, deals, actions, buyer groups, warm intros, webhooks, intelligence, and more.",
|
|
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 server.test.js api-bridge.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 tool-annotations.test.js toolsets.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js scripts/local-dev-server.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/roadmap-tools.test.js governance/money.test.js"
|
|
12
|
+
"test": "node --test analytics.test.js server.test.js api-bridge.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 tool-annotations.test.js toolsets.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/roadmap-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|
package/server.js
CHANGED
|
@@ -50,13 +50,15 @@ import { connectWorkspace, disconnectWorkspace, getConnectionStatus, getValidTok
|
|
|
50
50
|
import { registerEnterpriseTools } from './tools/enterprise-tools.js';
|
|
51
51
|
import { registerEmailTools } from './tools/email-tools.js';
|
|
52
52
|
import { registerWorkBoardTools } from './tools/work-board-tools.js';
|
|
53
|
+
import { registerSourceControlTools } from './tools/source-control/connection-tools.js';
|
|
53
54
|
import { registerRoadmapTools } from './tools/roadmap-tools.js';
|
|
54
55
|
import { registerPaperTools } from './tools/paper-tools.js';
|
|
55
56
|
import { register as registerAlwaysLoadedTools } from './toolsets/revenue/always-loaded.js';
|
|
56
57
|
import { register as registerExtensibilityTools } from './toolsets/extensibility.js';
|
|
57
58
|
import { registerSpaceTools } from './toolsets/spaces.js';
|
|
59
|
+
import { composeCompetitorIntel, composeCustomerSignals } from './toolsets/revenue/competitive-coverage.js';
|
|
58
60
|
import { getDemoAvailability, scheduleDemo, scheduleMeeting } from './tools/scheduling.js';
|
|
59
|
-
import { registerAnalytics } from './analytics.js';
|
|
61
|
+
import { registerAnalytics, wrapWithAnalytics } from './analytics.js';
|
|
60
62
|
import {
|
|
61
63
|
apiBridgeCatalog,
|
|
62
64
|
buildMutationHeaders,
|
|
@@ -131,7 +133,7 @@ async function api(method, path, { params, body, headers: extraHeaders } = {}) {
|
|
|
131
133
|
const auth = currentAuth();
|
|
132
134
|
assertOAuthIssuerMatchesTarget(auth, API_BASE);
|
|
133
135
|
let currentToken = auth.token;
|
|
134
|
-
if (auth.source === 'stored') {
|
|
136
|
+
if (auth.source === 'stored' || auth.source === 'stored_pool') {
|
|
135
137
|
let freshToken;
|
|
136
138
|
try {
|
|
137
139
|
freshToken = await getValidToken(API_BASE);
|
|
@@ -177,7 +179,7 @@ async function api(method, path, { params, body, headers: extraHeaders } = {}) {
|
|
|
177
179
|
};
|
|
178
180
|
|
|
179
181
|
let res = await request(currentToken);
|
|
180
|
-
if (res.status === 401 && auth.source === 'stored') {
|
|
182
|
+
if (res.status === 401 && (auth.source === 'stored' || auth.source === 'stored_pool')) {
|
|
181
183
|
let refreshedToken;
|
|
182
184
|
try {
|
|
183
185
|
refreshedToken = await getValidToken(API_BASE, { forceRefresh: true });
|
|
@@ -298,6 +300,7 @@ const moneyWrite = {
|
|
|
298
300
|
*/
|
|
299
301
|
const SERVER_NAME = process.env.ADRATA_MCP_SERVER_NAME?.trim() || '@adrata/adrata-mcp';
|
|
300
302
|
|
|
303
|
+
function createMcpServer() {
|
|
301
304
|
const server = new McpServer({ name: SERVER_NAME, version: '1.0.0' });
|
|
302
305
|
|
|
303
306
|
// ---------------------------------------------------------------------------
|
|
@@ -367,6 +370,26 @@ wrapWithEventLogging(server, api, AUTH);
|
|
|
367
370
|
// Must be applied AFTER tier-gating and event logging wrappers above.
|
|
368
371
|
applySecurityLayer(server, currentAuth, api);
|
|
369
372
|
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Analytics wrapper
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// MUST be installed here — after the wrappers above, and BEFORE the first
|
|
377
|
+
// server.tool(...) call below. It monkey-patches `server.tool`, so it can only
|
|
378
|
+
// instrument FUTURE registrations. It used to be installed from
|
|
379
|
+
// registerAnalytics() ~1975 lines further down, which put 227 of the 254 tools
|
|
380
|
+
// this server exposes ahead of it: trackToolInvocation and recordSessionTool
|
|
381
|
+
// could not fire for 89% of the surface, and get_mcp_analytics reported
|
|
382
|
+
// tool_invocations.total: 0 as though it were a measurement.
|
|
383
|
+
//
|
|
384
|
+
// The cost is deliberate and worth naming: every counted invocation now also
|
|
385
|
+
// fire-and-forget POSTs a `tool_invocation` event, roughly doubling the writes
|
|
386
|
+
// on /api/v1/mcp/events (the event-logging wrapper above already posts one per
|
|
387
|
+
// call for the whole surface). ADRATA_TELEMETRY=off disables both.
|
|
388
|
+
//
|
|
389
|
+
// registerAnalytics() still calls this; the wrapper is idempotent, so the later
|
|
390
|
+
// call is a no-op rather than a second layer.
|
|
391
|
+
wrapWithAnalytics(server, api, currentAuth, checkToolAccess);
|
|
392
|
+
|
|
370
393
|
// ===== WORKSPACE CONNECTION: OAuth 2.0 (available at all tiers) =====
|
|
371
394
|
|
|
372
395
|
server.tool('connect_workspace',
|
|
@@ -381,7 +404,18 @@ server.tool('connect_workspace',
|
|
|
381
404
|
AUTH = reauthenticate();
|
|
382
405
|
return ok(result);
|
|
383
406
|
} catch (err) {
|
|
384
|
-
|
|
407
|
+
// `invalid_scope` is not a browser problem, and saying it is costs real
|
|
408
|
+
// time: it means the authorization server rejected one of the scopes we
|
|
409
|
+
// asked for, which in practice means the deployed API is running behind
|
|
410
|
+
// this client. The whole request is refused, so read-only will still
|
|
411
|
+
// connect — that asymmetry is the tell.
|
|
412
|
+
const hint = /invalid_scope/.test(err.message ?? '')
|
|
413
|
+
? 'The API rejected a requested scope, so it granted nothing — this is NOT a browser problem. '
|
|
414
|
+
+ 'The deployed API is probably behind this client. Compare them: '
|
|
415
|
+
+ 'curl -s ' + API_BASE + '/health | jq -r .build_sha '
|
|
416
|
+
+ 'Read-only connect_workspace() should still work; use it to keep going.'
|
|
417
|
+
: 'Make sure your browser is accessible and try again.';
|
|
418
|
+
return ok({ error: true, message: err.message, hint });
|
|
385
419
|
}
|
|
386
420
|
});
|
|
387
421
|
|
|
@@ -412,7 +446,7 @@ server.tool('workspace_status',
|
|
|
412
446
|
// answer from a diagnostic: it sends someone to reconnect a connection that
|
|
413
447
|
// was never the problem, and it is the tool a connect flow tells you to run
|
|
414
448
|
// to confirm the setup worked.
|
|
415
|
-
const usingStoredSession = AUTH.source === 'stored';
|
|
449
|
+
const usingStoredSession = AUTH.source === 'stored' || AUTH.source === 'stored_pool';
|
|
416
450
|
const stored = await getConnectionStatus(API_BASE);
|
|
417
451
|
|
|
418
452
|
// AUTH.workspaceId is only populated when the workspace was pinned via env
|
|
@@ -452,8 +486,11 @@ server.tool('workspace_status',
|
|
|
452
486
|
return ok({
|
|
453
487
|
...stored,
|
|
454
488
|
tier: stored.connected ? AUTH.tier : TIERS.FREE,
|
|
455
|
-
credential:
|
|
489
|
+
credential: AUTH.source === 'stored_pool'
|
|
490
|
+
? `dedicated OAuth session for named identity pool ${AUTH.identityPool}`
|
|
491
|
+
: 'stored OAuth session (~/.adrata/tokens.json)',
|
|
456
492
|
authSource: AUTH.source,
|
|
493
|
+
identityPool: AUTH.identityPool || null,
|
|
457
494
|
apiBase: API_BASE,
|
|
458
495
|
workspaceId,
|
|
459
496
|
workspaceName,
|
|
@@ -1756,7 +1793,7 @@ server.tool('get_intent_signals', 'Get buying intent signals for a company.',
|
|
|
1756
1793
|
// are workspace-scoped from the token; filter by subject (a company or person),
|
|
1757
1794
|
// source, signal type, or minimum score, and page with the returned cursor.
|
|
1758
1795
|
server.tool('list_customer_signals',
|
|
1759
|
-
'List first-party customer signals (the store live plays fire from) for the workspace, or scope to one company/person via subjectType + subjectId. Distinct from get_intent_signals (third-party intent).',
|
|
1796
|
+
'List first-party customer signals (the store live plays fire from) for the workspace, or scope to one company/person via subjectType + subjectId. Distinct from get_intent_signals (third-party intent). Always relay the returned `coverage`: an empty result is checked against the unfiltered workspace, so a real zero is distinguished from a store nothing has ever written to.',
|
|
1760
1797
|
{
|
|
1761
1798
|
subjectType: z.enum(['company', 'person']).optional().describe('Scope to a subject kind; pair with subjectId'),
|
|
1762
1799
|
subjectId: z.string().optional().describe('Company or person ID to scope signals to'),
|
|
@@ -1768,11 +1805,31 @@ server.tool('list_customer_signals',
|
|
|
1768
1805
|
limit: z.number().optional().describe('Page size (default 50, max 500)'),
|
|
1769
1806
|
cursor: z.string().optional().describe('Opaque cursor from a previous page'),
|
|
1770
1807
|
},
|
|
1771
|
-
async (a) =>
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1808
|
+
async (a) => {
|
|
1809
|
+
const filtersApplied = {
|
|
1810
|
+
subjectType: a.subjectType, subjectId: a.subjectId, source: a.source,
|
|
1811
|
+
signalType: a.signalType, minScore: a.minScore, since: a.since, until: a.until,
|
|
1812
|
+
};
|
|
1813
|
+
const payload = await api('GET', '/api/v1/customer-signals', { params: {
|
|
1814
|
+
...filtersApplied, limit: a.limit || 50, cursor: a.cursor,
|
|
1815
|
+
} });
|
|
1816
|
+
|
|
1817
|
+
// The probe is spent ONLY on the ambiguous case. Rows returned means the store is
|
|
1818
|
+
// demonstrably written to and the count speaks for itself; an empty array is the reading a
|
|
1819
|
+
// seller cannot interpret, and one extra request is what separates "no signals match these
|
|
1820
|
+
// filters" from "nothing has ever been ingested".
|
|
1821
|
+
const empty = Array.isArray(payload?.items) ? payload.items.length === 0 : undefined;
|
|
1822
|
+
let workspaceProbePayload;
|
|
1823
|
+
if (empty === true) {
|
|
1824
|
+
try {
|
|
1825
|
+
workspaceProbePayload = await api('GET', '/api/v1/customer-signals', { params: { limit: 1 } });
|
|
1826
|
+
} catch {
|
|
1827
|
+
workspaceProbePayload = null;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return ok(composeCustomerSignals({ payload, filtersApplied, workspaceProbePayload }));
|
|
1832
|
+
});
|
|
1776
1833
|
|
|
1777
1834
|
server.tool('get_deal_authority', 'Get authority mapping and stakeholder analysis for a deal.',
|
|
1778
1835
|
{ opportunityId: z.string() },
|
|
@@ -1781,9 +1838,39 @@ server.tool('get_deal_authority', 'Get authority mapping and stakeholder analysi
|
|
|
1781
1838
|
params: { opportunityId: a.opportunityId },
|
|
1782
1839
|
})));
|
|
1783
1840
|
|
|
1784
|
-
server.tool('get_competitor_intel',
|
|
1841
|
+
server.tool('get_competitor_intel',
|
|
1842
|
+
'Competitive intelligence for ONE company: the competitor mentions attributed to that account, plus the workspace-wide competitor definitions they are scored against. Always relay the returned `coverage` — it says which half is company-scoped and which is workspace-wide, and states whether a zero is a real zero or an unconfigured store. `/api/v1/competitors` is a workspace configuration list and takes no company filter; the company-scoped evidence lives at `/api/v1/competitors/mentions?companyId=`.',
|
|
1785
1843
|
{ companyId: z.string() },
|
|
1786
|
-
async (a) =>
|
|
1844
|
+
async (a) => {
|
|
1845
|
+
// The definition list is workspace-scoped by construction. Sending `companyId` to it — as
|
|
1846
|
+
// this tool used to — got the parameter silently discarded and returned the same answer for
|
|
1847
|
+
// every company. The two reads are separate because their SCOPES are separate.
|
|
1848
|
+
const definitionsPayload = await api('GET', '/api/v1/competitors');
|
|
1849
|
+
|
|
1850
|
+
let mentionsPayload;
|
|
1851
|
+
let workspaceMentionsPayload;
|
|
1852
|
+
let mentionsUnavailable = false;
|
|
1853
|
+
try {
|
|
1854
|
+
mentionsPayload = await api('GET', '/api/v1/competitors/mentions', {
|
|
1855
|
+
params: { companyId: a.companyId, limit: 50 },
|
|
1856
|
+
});
|
|
1857
|
+
// The denominator. One extra cheap request buys the difference between "this account is
|
|
1858
|
+
// quiet" and "nothing has ever been scanned".
|
|
1859
|
+
workspaceMentionsPayload = await api('GET', '/api/v1/competitors/mentions', {
|
|
1860
|
+
params: { limit: 1 },
|
|
1861
|
+
});
|
|
1862
|
+
} catch {
|
|
1863
|
+
mentionsUnavailable = true;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
return ok(composeCompetitorIntel({
|
|
1867
|
+
companyId: a.companyId,
|
|
1868
|
+
definitionsPayload,
|
|
1869
|
+
mentionsPayload,
|
|
1870
|
+
workspaceMentionsPayload,
|
|
1871
|
+
mentionsUnavailable,
|
|
1872
|
+
}));
|
|
1873
|
+
});
|
|
1787
1874
|
|
|
1788
1875
|
// ===== ANALYTICS =====
|
|
1789
1876
|
|
|
@@ -2266,6 +2353,8 @@ registerWorkBoardTools(server, {
|
|
|
2266
2353
|
getGrantedScope: () => loadTokens()?.scope,
|
|
2267
2354
|
});
|
|
2268
2355
|
|
|
2356
|
+
registerSourceControlTools(server, { z, api, ok });
|
|
2357
|
+
|
|
2269
2358
|
// The containers above the cards, and the "add this to the roadmap" verb
|
|
2270
2359
|
// (company/decisions/2026-08-06-spoq-roadmap-sync.md).
|
|
2271
2360
|
registerRoadmapTools(server, {
|
|
@@ -2323,7 +2412,7 @@ server.tool('schedule_meeting',
|
|
|
2323
2412
|
// Tracks tool invocations, search queries, tier gates, conversions, sessions.
|
|
2324
2413
|
// Builds on top of existing event logging — adds classification and dashboards.
|
|
2325
2414
|
// ADRATA_TELEMETRY=off disables all tracking. PII is hashed before storage.
|
|
2326
|
-
registerAnalytics(server, { z, api, auth:
|
|
2415
|
+
registerAnalytics(server, { z, api, auth: currentAuth, ok, checkToolAccess });
|
|
2327
2416
|
|
|
2328
2417
|
// ===== COMPOSITE TOOLSETS =====
|
|
2329
2418
|
// 42 composite tools organized into 7 toolsets with dynamic discovery.
|
|
@@ -2344,6 +2433,9 @@ registerExtensibilityTools(server, api, AUTH);
|
|
|
2344
2433
|
// Service" got nothing, while the app answered it fine.
|
|
2345
2434
|
registerSpaceTools(server, { z, api, ok });
|
|
2346
2435
|
|
|
2436
|
+
return server;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2347
2439
|
// ---------------------------------------------------------------------------
|
|
2348
2440
|
// Start
|
|
2349
2441
|
// ---------------------------------------------------------------------------
|
|
@@ -2353,11 +2445,12 @@ const TRANSPORT_MODE = (process.env.ADRATA_MCP_TRANSPORT || 'stdio').toLowerCase
|
|
|
2353
2445
|
if (TRANSPORT_MODE === 'http' || TRANSPORT_MODE === 'sse') {
|
|
2354
2446
|
// Streamable HTTP transport — single POST /mcp endpoint that can upgrade to SSE
|
|
2355
2447
|
const { startHttpTransport } = await import('./transport-http.js');
|
|
2356
|
-
await startHttpTransport(
|
|
2448
|
+
await startHttpTransport(createMcpServer, {
|
|
2357
2449
|
runWithAuthContext,
|
|
2358
2450
|
});
|
|
2359
2451
|
} else {
|
|
2360
2452
|
// Default: stdio transport (backward compatible)
|
|
2453
|
+
const server = createMcpServer();
|
|
2361
2454
|
const transport = new StdioServerTransport();
|
|
2362
2455
|
await server.connect(transport);
|
|
2363
2456
|
}
|
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.6",
|
|
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.6",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
},
|
|
@@ -31,6 +31,18 @@
|
|
|
31
31
|
"description": "Enterprise OAuth 2.1 access token, audience-bound to the Adrata MCP resource (RFC 8707).",
|
|
32
32
|
"isRequired": false,
|
|
33
33
|
"isSecret": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "ADRATA_MCP_IDENTITY_POOL",
|
|
37
|
+
"description": "Optional named worker identity (for example qa1 or qa2). Named pools fail closed and only use their dedicated stored OAuth session.",
|
|
38
|
+
"isRequired": false,
|
|
39
|
+
"isSecret": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "ADRATA_MCP_CONFIG_DIR",
|
|
43
|
+
"description": "OAuth storage directory. Required and non-default when ADRATA_MCP_IDENTITY_POOL is set.",
|
|
44
|
+
"isRequired": false,
|
|
45
|
+
"isSecret": false
|
|
34
46
|
}
|
|
35
47
|
]
|
|
36
48
|
}
|