@adrata/adrata-mcp 1.0.44 → 1.0.47

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 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.44",
3
+ "version": "1.0.47",
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 tools/free-search.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"
13
13
  },
14
14
  "keywords": [
15
15
  "mcp",
package/server.js CHANGED
@@ -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, spend, policy, and idempotency controls. Product profiles refuse references outside their namespace.',
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/spend. Reuse the same key on retry.'),
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. Free reads run directly. Reads that can spend external data credits return a spend hold; execute one only with confirmSpend:true, a reason, and an idempotencyKey (reuse the same key on retry). The response includes the approved ceiling and an exact provider-reported receipt where available. 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.',
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 for a spend acknowledgment or when requesting a live-write confirmation token.'),
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('Required true to execute a read held for external vendor spend. Never authorizes a workspace write.'),
753
- idempotencyKey: z.string().optional().describe('Required with confirmSpend:true. Reuse the same key on retry; the API replays the first response instead of calling the paid provider twice.'),
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 {
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.44",
6
+ "version": "1.0.47",
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.44",
18
+ "version": "1.0.47",
19
19
  "transport": {
20
20
  "type": "stdio"
21
21
  },
@@ -392,6 +392,42 @@ export function validateOnboardingArgs(args = {}) {
392
392
  return problems;
393
393
  }
394
394
 
395
+ /**
396
+ * Whether the adaptive payload (launch, preparedAccountIds, preparedNote,
397
+ * source) is durable, read FROM the invite response rather than assumed. The
398
+ * route returns `recorded` alongside `inviteUrl`: `true` means the
399
+ * workspace_invitation_records row exists; `false` means the link works but
400
+ * the record was not written. An absent `recorded` is reported as unknown —
401
+ * `null` — rather than asserted either way.
402
+ */
403
+ export function describeAdaptivePayloadDurability(payload = {}) {
404
+ const recorded = payload?.recorded;
405
+ if (recorded === true) {
406
+ return {
407
+ adaptivePayloadDurable: true,
408
+ adaptivePayloadNote:
409
+ 'The seat and the link are live, and the launch, preparedAccountIds, preparedNote and source'
410
+ + ' were stored with the invitation (recorded: true).',
411
+ };
412
+ }
413
+ if (recorded === false) {
414
+ return {
415
+ adaptivePayloadDurable: false,
416
+ adaptivePayloadNote:
417
+ 'The seat and the link are live, but the endpoint reported recorded: false — the invitation'
418
+ + ' record carrying launch, preparedAccountIds, preparedNote and source was not written, so it'
419
+ + ' will not appear in the issued list and cannot be revoked from it.',
420
+ };
421
+ }
422
+ return {
423
+ adaptivePayloadDurable: null,
424
+ adaptivePayloadNote:
425
+ 'The seat and the link are live. The endpoint did not confirm whether the invitation record'
426
+ + ' carrying launch, preparedAccountIds, preparedNote and source was written (no `recorded`'
427
+ + ' field in the response), so durability is unknown rather than asserted.',
428
+ };
429
+ }
430
+
395
431
  /** The exact body this tool would send. Kept in one place so preview and live write cannot drift. */
396
432
  export function buildInvitationBody(args = {}) {
397
433
  const body = {
@@ -404,19 +440,19 @@ export function buildInvitationBody(args = {}) {
404
440
  if (args.managerId) body.managerId = args.managerId;
405
441
 
406
442
  // ── The adaptive payload ────────────────────────────────────────────────
407
- // INTEGRATION POINT (2026-09-06): these four fields are sent and the
408
- // DEPLOYED endpoint ignores them. `InviteMemberRequest` in
409
- // `code/api/src/routes/v1/entities/org/workspace/members.rs` declares
410
- // exactly `email`, `role` and `manager_id`; serde drops unknown fields
411
- // silently, so the call succeeds, a link is minted, and the launch is
412
- // simply not recorded. They are sent anyway rather than withheld, because
413
- // the route change carrying `InvitationContext` is in flight on
414
- // `feature/onboarding-grant-record` and the wire shape should not need a
415
- // second edit when it lands.
443
+ // These four fields are stored by the deployed endpoint. `InviteMemberRequest`
444
+ // in `code/api/src/routes/v1/entities/org/workspace/members.rs` declares
445
+ // `launch`, `prepared_account_ids`, `prepared_note` and `source` with
446
+ // `#[serde(default)]` (camelCase via `alias`), and `mint_workspace_invitation`
447
+ // in `invitations.rs` binds them into `workspace_invitation_records`. The
448
+ // route answers with `recorded: true|false` saying whether that durable row
449
+ // was written, and the live-write result below reports durability FROM that
450
+ // field rather than from an assumption about what the server does.
416
451
  //
417
- // What this means for a caller TODAY, and it is stated in the tool's own
418
- // response rather than only here: the seat and the link are real, and the
419
- // adaptive configuration is not yet durable.
452
+ // (History: until 2026-09-06 the endpoint accepted only email, role and
453
+ // managerId and dropped the rest; the tool then said so in its response.
454
+ // That note outlived the fix and told an AE a stored configuration was not
455
+ // durable — hence the rule above: report what the response says.)
420
456
  //
421
457
  // Note also that `InviteMemberRequest` carries no
422
458
  // `#[serde(rename_all = "camelCase")]` — `manager_id` works only because it
@@ -625,12 +661,12 @@ export function registerProvisioningTools(
625
661
  preparedAccountIds: body.preparedAccountIds ?? [],
626
662
  preparedAccountCount: body.preparedAccountIds?.length ?? 0,
627
663
  preparedNote: body.preparedNote ?? null,
628
- adaptivePayloadDurable: false,
664
+ adaptivePayloadDurable: null,
629
665
  adaptivePayloadNote:
630
- 'INTEGRATION POINT: the deployed invite endpoint accepts email, role and managerId only, so'
631
- + ' launch, preparedAccountIds, preparedNote and source are sent but not yet stored. The seat'
632
- + ' and the link are real; the adaptive configuration becomes durable when the'
633
- + ' workspace_invitation_records route change ships.',
666
+ 'Not known until minted. The invite endpoint stores launch, preparedAccountIds, preparedNote'
667
+ + ' and source with the invitation (members.rs InviteMemberRequest → invitations.rs'
668
+ + ' mint_workspace_invitation) and reports `recorded` on the live write; the live result'
669
+ + ' carries that answer.',
634
670
  preview: validation,
635
671
  body,
636
672
  note:
@@ -712,10 +748,7 @@ export function registerProvisioningTools(
712
748
  inviteUrl,
713
749
  launch: body.launch ?? null,
714
750
  preparedAccountIds: body.preparedAccountIds ?? [],
715
- adaptivePayloadDurable: false,
716
- adaptivePayloadNote:
717
- 'The seat and the link are live. launch/preparedAccountIds/preparedNote were sent but the'
718
- + ' deployed endpoint does not yet store them — see the integration point in tools/provisioning.js.',
751
+ ...describeAdaptivePayloadDurability(payload),
719
752
  credentialWarning:
720
753
  'inviteUrl is a credential: anyone holding it can seat themselves in this workspace. Deliver it'
721
754
  + ' over a trusted channel and do not paste it into a log, a ticket, or a shared document.',