@adrata/adrata-mcp 1.0.0

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.
Files changed (41) hide show
  1. package/README.md +548 -0
  2. package/access/auth.js +289 -0
  3. package/access/oauth.js +1059 -0
  4. package/access/resource-metadata.js +167 -0
  5. package/access/tiers.js +422 -0
  6. package/analytics.js +634 -0
  7. package/api-bridge.js +499 -0
  8. package/governance/money.js +141 -0
  9. package/output-formatter.js +589 -0
  10. package/package.json +68 -0
  11. package/resources.js +246 -0
  12. package/security.js +690 -0
  13. package/server.js +2139 -0
  14. package/server.json +55 -0
  15. package/skills/backlog-triage/SKILL.md +115 -0
  16. package/skills/board-review/SKILL.md +96 -0
  17. package/skills/incident-to-card/SKILL.md +126 -0
  18. package/skills/log-outreach.md +62 -0
  19. package/skills/ship-the-card/SKILL.md +155 -0
  20. package/tool-annotations.js +269 -0
  21. package/tools/billing.js +149 -0
  22. package/tools/email-tools.js +652 -0
  23. package/tools/enterprise-tools.js +651 -0
  24. package/tools/free-search.js +160 -0
  25. package/tools/memory.js +440 -0
  26. package/tools/morning-brief.js +551 -0
  27. package/tools/paper-tools.js +563 -0
  28. package/tools/scheduling.js +322 -0
  29. package/tools/work-board-tools.js +758 -0
  30. package/toolsets/communications.js +276 -0
  31. package/toolsets/crm.js +495 -0
  32. package/toolsets/extensibility.js +1131 -0
  33. package/toolsets/infrastructure.js +757 -0
  34. package/toolsets/intelligence.js +232 -0
  35. package/toolsets/knowledge.js +154 -0
  36. package/toolsets/matrix.js +217 -0
  37. package/toolsets/outreach.js +432 -0
  38. package/toolsets/prospecting.js +314 -0
  39. package/toolsets/revenue/always-loaded.js +341 -0
  40. package/toolsets/revenue/sloan-tools.js +81 -0
  41. package/transport-http.js +505 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728) and audience-bound
3
+ * token validation (RFC 8707 Resource Indicators) for the exposed Adrata
4
+ * MCP server.
5
+ *
6
+ * The MCP HTTP transport is an OAuth *resource server*. Per the 2025-06-18
7
+ * MCP authorization spec it MUST:
8
+ * - publish `/.well-known/oauth-protected-resource` describing itself and
9
+ * the authorization server(s) that mint tokens for it (RFC 9728), and
10
+ * - validate that every presented access token was issued *for this
11
+ * resource* (RFC 8707 audience binding) and REJECT tokens minted for a
12
+ * different audience — i.e. never blindly pass a bearer token through.
13
+ *
14
+ * This module is pure/stateless so it can be unit-tested without a running
15
+ * transport. The transport wires it into request handling.
16
+ */
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Configuration
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * The authorization server that issues tokens for this MCP resource.
24
+ * Adrata's Rust API serves RFC 8414 metadata at
25
+ * `${AS}/.well-known/oauth-authorization-server`.
26
+ */
27
+ export function authorizationServers() {
28
+ const raw = process.env.ADRATA_MCP_AUTHORIZATION_SERVER
29
+ || process.env.ADRATA_API_URL
30
+ || 'https://api.adrata.com';
31
+ return raw.split(',').map(s => s.trim()).filter(Boolean);
32
+ }
33
+
34
+ /**
35
+ * The canonical resource identifier for this MCP server. RFC 8707 clients
36
+ * send this as the `resource` parameter so the AS can bind the token's
37
+ * audience to it. Defaults to the REST MCP resource the Rust AS advertises.
38
+ */
39
+ export function canonicalResource() {
40
+ if (process.env.ADRATA_MCP_RESOURCE) return process.env.ADRATA_MCP_RESOURCE.trim();
41
+ const as = authorizationServers()[0] || 'https://api.adrata.com';
42
+ return `${as.replace(/\/$/, '')}/api/v1/mcp`;
43
+ }
44
+
45
+ /**
46
+ * The set of audience values this resource server will accept on a token.
47
+ * Always includes:
48
+ * - the Adrata AS default audience ("adrata"), which every OAuth access
49
+ * token minted by the Rust API currently carries, and
50
+ * - this server's canonical RFC 8707 resource URI.
51
+ * Extra values can be added via ADRATA_MCP_ACCEPTED_AUDIENCES (comma list).
52
+ */
53
+ export function acceptedAudiences() {
54
+ const base = new Set(['adrata', canonicalResource()]);
55
+ const extra = process.env.ADRATA_MCP_ACCEPTED_AUDIENCES;
56
+ if (extra) for (const a of extra.split(',').map(s => s.trim()).filter(Boolean)) base.add(a);
57
+ return [...base];
58
+ }
59
+
60
+ /**
61
+ * Whether a token that carries NO audience claim at all is rejected.
62
+ * Strict mode (default off) rejects aud-less JWTs; the always-on rule is
63
+ * that a token whose aud is present but does NOT match is rejected.
64
+ */
65
+ function strictAudienceEnforcement() {
66
+ return process.env.ADRATA_MCP_AUDIENCE_ENFORCEMENT === 'strict';
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // RFC 9728 metadata document
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Build the RFC 9728 protected-resource-metadata document for this server.
75
+ * @param {string} [selfUrl] - The metadata document's own URL, echoed back so
76
+ * a discovering client can confirm it fetched the right resource metadata.
77
+ */
78
+ export function protectedResourceMetadata(selfUrl) {
79
+ return {
80
+ resource: canonicalResource(),
81
+ authorization_servers: authorizationServers(),
82
+ // Must advertise every scope OAUTH_SCOPE requests. Discovery-driven clients
83
+ // build their authorize request from this list, so anything missing here is
84
+ // a scope they never ask for. `ai:base` is required by the AI tool
85
+ // dispatcher (POST /api/v1/ai-crm-tools/execute) — without it every
86
+ // dispatcher-backed tool (rank_people_by_icp, path-to-power, break-in
87
+ // targeting, ...) fails with HTTP 403 insufficient_scope.
88
+ scopes_supported: [
89
+ 'read:companies', 'read:people', 'read:opportunities', 'read:actions',
90
+ 'ai:base',
91
+ 'write:crm', 'read:email', 'write:email', 'mcp:tools',
92
+ ],
93
+ bearer_methods_supported: ['header'],
94
+ resource_name: 'Adrata MCP Server',
95
+ resource_documentation: 'https://adrata.com/developers',
96
+ // MCP-specific hints for discovery clients.
97
+ mcp_protocol_versions: ['2025-06-18', '2025-03-26', '2024-11-05'],
98
+ ...(selfUrl ? { resource_metadata: selfUrl } : {}),
99
+ };
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // JWT decoding (no signature verification — the AS/API verifies signatures)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Decode a JWT payload without verifying its signature. Returns null when the
108
+ * token is not a well-formed three-segment JWT (e.g. an opaque API key).
109
+ */
110
+ export function decodeJwtPayload(token) {
111
+ if (typeof token !== 'string') return null;
112
+ const parts = token.split('.');
113
+ if (parts.length !== 3) return null;
114
+ try {
115
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
116
+ const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4));
117
+ const json = Buffer.from(b64 + pad, 'base64').toString('utf8');
118
+ return JSON.parse(json);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // RFC 8707 audience validation (no token passthrough)
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Validate that an access token is bound to this resource server's audience.
130
+ *
131
+ * Security property (confused-deputy / token-passthrough defense): a JWT whose
132
+ * `aud` claim is present but does NOT intersect this server's accepted
133
+ * audiences is REJECTED. A token minted for another resource/client therefore
134
+ * cannot be replayed against the Adrata MCP server.
135
+ *
136
+ * Opaque (non-JWT) tokens cannot be introspected locally and are deferred to
137
+ * the API, which verifies signature + audience server-side on every call.
138
+ *
139
+ * @param {string} token - The bearer token.
140
+ * @param {{ accepted?: string[] }} [opts]
141
+ * @returns {{ valid: boolean, reason?: string, audience?: string[]|string }}
142
+ */
143
+ export function validateTokenAudience(token, opts = {}) {
144
+ if (!token) return { valid: false, reason: 'missing_token' };
145
+
146
+ const accepted = opts.accepted || acceptedAudiences();
147
+ const payload = decodeJwtPayload(token);
148
+
149
+ // Opaque token — defer to the authorization server / API for verification.
150
+ if (!payload) return { valid: true, reason: 'opaque_token_deferred' };
151
+
152
+ const aud = payload.aud;
153
+ const audList = aud == null ? [] : Array.isArray(aud) ? aud : [aud];
154
+
155
+ if (audList.length === 0) {
156
+ // Token has no audience binding at all.
157
+ return strictAudienceEnforcement()
158
+ ? { valid: false, reason: 'missing_audience' }
159
+ : { valid: true, reason: 'no_audience_claim', audience: [] };
160
+ }
161
+
162
+ const match = audList.some(a => accepted.includes(a));
163
+ if (!match) {
164
+ return { valid: false, reason: 'audience_mismatch', audience: audList };
165
+ }
166
+ return { valid: true, audience: audList };
167
+ }
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Tool tier definitions for Adrata MCP Server.
3
+ *
4
+ * Each tool is tagged as 'free', 'pro', or 'enterprise'.
5
+ * - free: available without any API key
6
+ * - pro: requires ADRATA_API_KEY
7
+ * - enterprise: requires OAuth bearer token (workspace connection)
8
+ */
9
+
10
+ export const TIERS = {
11
+ FREE: 'free',
12
+ PRO: 'pro',
13
+ ENTERPRISE: 'enterprise',
14
+ };
15
+
16
+ /**
17
+ * Map of tool name -> required tier.
18
+ * Tools not listed here fail closed to 'enterprise' (see getToolTier).
19
+ */
20
+ export const TOOL_TIERS = {
21
+ // --- FREE tier: workspace connection (available to all tiers) ---
22
+ connect_workspace: TIERS.FREE,
23
+ disconnect_workspace: TIERS.FREE,
24
+ workspace_status: TIERS.FREE,
25
+ // Multi-workspace navigation. A user who administers more than one workspace
26
+ // (e.g. an agency or a founder across two tenants) could not previously see
27
+ // or reach a second workspace from the CLI at all.
28
+ list_workspaces: TIERS.ENTERPRISE,
29
+ switch_workspace: TIERS.ENTERPRISE,
30
+ adrata_api_catalog: TIERS.FREE,
31
+ paper_app_audit: TIERS.FREE,
32
+ adrata_desktop_app_audit: TIERS.FREE,
33
+
34
+ // --- FREE tier: tools that genuinely work with NO API key ---
35
+ // find_company/find_person are Claude-training-data prompts (no API call),
36
+ // get_demo_availability/schedule_demo use Cal.com (no Adrata auth), and
37
+ // describe_fields returns a local schema.
38
+ find_company: TIERS.FREE,
39
+ find_person: TIERS.FREE,
40
+ get_demo_availability: TIERS.FREE,
41
+ schedule_demo: TIERS.FREE,
42
+ describe_fields: TIERS.FREE,
43
+
44
+ // --- PRO tier: authenticated API reads. These were mislabeled FREE, which
45
+ // advertised tools that always 401'd for unauthenticated users. ---
46
+ search_companies: TIERS.PRO,
47
+ search_people: TIERS.PRO,
48
+ search_emails: TIERS.PRO,
49
+ get_email: TIERS.PRO,
50
+ count_emails: TIERS.PRO,
51
+ check_inbox: TIERS.PRO,
52
+ find_or_create_person: TIERS.PRO,
53
+ find_or_create_company: TIERS.PRO,
54
+ list_custom_fields: TIERS.PRO,
55
+ search_leads: TIERS.PRO,
56
+
57
+ // --- FREE tier: morning brief (free=teaser, pro+=full brief) ---
58
+ morning_brief: TIERS.FREE,
59
+
60
+ // --- FREE tier: billing tools (available at all tiers) ---
61
+ upgrade_account: TIERS.FREE,
62
+ check_subscription: TIERS.FREE,
63
+
64
+ // --- FREE tier: memory tools (free=local, pro+=server-side) ---
65
+ save_memory: TIERS.FREE,
66
+ recall: TIERS.FREE,
67
+ who_am_i: TIERS.FREE,
68
+ forget: TIERS.FREE,
69
+
70
+ // --- PRO tier: intelligence, analytics, enrichment, coaching ---
71
+ get_company: TIERS.PRO,
72
+ get_person: TIERS.PRO,
73
+ get_opportunity: TIERS.PRO,
74
+ get_action: TIERS.PRO,
75
+ enrich_company: TIERS.PRO,
76
+ enrich_person: TIERS.PRO,
77
+ get_intent_signals: TIERS.PRO,
78
+ list_customer_signals: TIERS.PRO,
79
+ get_deal_authority: TIERS.PRO,
80
+ get_competitor_intel: TIERS.PRO,
81
+ get_pipeline_metrics: TIERS.PRO,
82
+ get_forecast_data: TIERS.PRO,
83
+ get_activity_summary: TIERS.PRO,
84
+ // Finance installed base + Adrata Cloud reads. Authenticated workspace data,
85
+ // so PRO at minimum — never FREE, which would advertise a tool that always 401s.
86
+ get_installed_base: TIERS.PRO,
87
+ get_company_invoices: TIERS.PRO,
88
+ get_cloud_records: TIERS.PRO,
89
+ get_speedrun_list: TIERS.PRO,
90
+ get_priority_pursuits: TIERS.PRO,
91
+ score_company_icp: TIERS.PRO,
92
+ list_icp_profiles: TIERS.PRO,
93
+ get_icp_distribution: TIERS.PRO,
94
+ count_records: TIERS.PRO,
95
+ list_meetings: TIERS.PRO,
96
+ get_meeting: TIERS.PRO,
97
+ get_meeting_summary: TIERS.PRO,
98
+ get_meeting_action_items: TIERS.PRO,
99
+ list_actions: TIERS.PRO,
100
+ list_overdue_actions: TIERS.PRO,
101
+ list_today_actions: TIERS.PRO,
102
+ list_notes: TIERS.PRO,
103
+ log_interaction: TIERS.PRO,
104
+
105
+ // --- ENTERPRISE tier: CRM CRUD, sequences, campaigns, admin, bulk ---
106
+ create_company: TIERS.ENTERPRISE,
107
+ update_company: TIERS.ENTERPRISE,
108
+ delete_company: TIERS.ENTERPRISE,
109
+ create_person: TIERS.ENTERPRISE,
110
+ update_person: TIERS.ENTERPRISE,
111
+ delete_person: TIERS.ENTERPRISE,
112
+ create_opportunity: TIERS.ENTERPRISE,
113
+ update_opportunity: TIERS.ENTERPRISE,
114
+ delete_opportunity: TIERS.ENTERPRISE,
115
+ create_action: TIERS.ENTERPRISE,
116
+ update_action: TIERS.ENTERPRISE,
117
+ complete_action: TIERS.ENTERPRISE,
118
+ delete_action: TIERS.ENTERPRISE,
119
+ create_note: TIERS.ENTERPRISE,
120
+ update_note: TIERS.ENTERPRISE,
121
+ delete_note: TIERS.ENTERPRISE,
122
+ get_company_people: TIERS.ENTERPRISE,
123
+ get_company_opportunities: TIERS.ENTERPRISE,
124
+ get_company_actions: TIERS.ENTERPRISE,
125
+ search_opportunities: TIERS.ENTERPRISE,
126
+ list_buyer_groups: TIERS.ENTERPRISE,
127
+ get_buyer_group: TIERS.ENTERPRISE,
128
+ create_buyer_group: TIERS.ENTERPRISE,
129
+ add_buyer_group_member: TIERS.ENTERPRISE,
130
+ get_buyer_group_members: TIERS.ENTERPRISE,
131
+ update_buyer_group: TIERS.ENTERPRISE,
132
+ update_buyer_group_member: TIERS.ENTERPRISE,
133
+ remove_buyer_group_member: TIERS.ENTERPRISE,
134
+ delete_buyer_group: TIERS.ENTERPRISE,
135
+ bulk_delete_buyer_groups: TIERS.ENTERPRISE,
136
+ list_company_lists: TIERS.ENTERPRISE,
137
+ create_company_list: TIERS.ENTERPRISE,
138
+ get_company_list: TIERS.ENTERPRISE,
139
+ add_to_company_list: TIERS.ENTERPRISE,
140
+ list_intro_requests: TIERS.ENTERPRISE,
141
+ get_intro_pipeline: TIERS.ENTERPRISE,
142
+ create_intro_request: TIERS.ENTERPRISE,
143
+ update_intro_request: TIERS.ENTERPRISE,
144
+ find_intro_path: TIERS.PRO,
145
+ get_network_stats: TIERS.PRO,
146
+ list_agent_tasks: TIERS.ENTERPRISE,
147
+ create_agent_task: TIERS.ENTERPRISE,
148
+ get_agent_task: TIERS.ENTERPRISE,
149
+ cancel_agent_task: TIERS.ENTERPRISE,
150
+ list_webhook_events: TIERS.PRO,
151
+ list_webhooks: TIERS.ENTERPRISE,
152
+ create_webhook: TIERS.ENTERPRISE,
153
+ update_webhook: TIERS.ENTERPRISE,
154
+ delete_webhook: TIERS.ENTERPRISE,
155
+ test_webhook: TIERS.ENTERPRISE,
156
+ list_webhook_deliveries: TIERS.ENTERPRISE,
157
+ get_webhook_delivery: TIERS.ENTERPRISE,
158
+ replay_webhook_delivery: TIERS.ENTERPRISE,
159
+ list_campaigns: TIERS.ENTERPRISE,
160
+ get_campaign: TIERS.ENTERPRISE,
161
+ list_sequences: TIERS.ENTERPRISE,
162
+ get_sequence: TIERS.ENTERPRISE,
163
+ list_users: TIERS.ENTERPRISE,
164
+ get_user: TIERS.ENTERPRISE,
165
+ get_current_user: TIERS.ENTERPRISE,
166
+ adrata_api_request: TIERS.ENTERPRISE,
167
+ adrata_ai_tool_catalog: TIERS.ENTERPRISE,
168
+ adrata_ai_tool_execute: TIERS.ENTERPRISE,
169
+ // Sloan (AI executive assistant) — same governed dispatcher path as
170
+ // adrata_ai_tool_execute, so same tier.
171
+ sloan_status: TIERS.ENTERPRISE,
172
+ sloan_handoff: TIERS.ENTERPRISE,
173
+ configure_sloan: TIERS.ENTERPRISE,
174
+ get_account_read: TIERS.ENTERPRISE,
175
+ build_pursuit_command_center: TIERS.ENTERPRISE,
176
+ rank_paths_to_power: TIERS.ENTERPRISE,
177
+ recommend_deal_move: TIERS.ENTERPRISE,
178
+ list_external_pipelines: TIERS.ENTERPRISE,
179
+ list_external_pipeline_members: TIERS.ENTERPRISE,
180
+ import_external_pipeline_members: TIERS.ENTERPRISE,
181
+ list_external_companies: TIERS.ENTERPRISE,
182
+ import_external_companies: TIERS.ENTERPRISE,
183
+ rank_companies_by_icp: TIERS.ENTERPRISE,
184
+ rank_people_by_icp: TIERS.ENTERPRISE,
185
+ check_batch_import_status: TIERS.ENTERPRISE,
186
+ move_pipeline_card: TIERS.ENTERPRISE,
187
+
188
+ // --- ENTERPRISE tier: Starfield work boards ---
189
+ // Every one of these reads or mutates real workspace records through the
190
+ // governed API, so none can work without a workspace connection. Listing a
191
+ // read as FREE here would advertise a tool that always 401s — the exact
192
+ // mislabelling the PRO block above was written to correct.
193
+ // "What is assigned to me" needs a real workspace connection twice over: the
194
+ // cards are workspace records, and the caller's identity — the whole point of
195
+ // the tool — only exists on an OAuth token.
196
+ list_my_work_items: TIERS.ENTERPRISE,
197
+ list_work_boards: TIERS.ENTERPRISE,
198
+ get_work_board: TIERS.ENTERPRISE,
199
+ get_work_item: TIERS.ENTERPRISE,
200
+ get_work_item_history: TIERS.ENTERPRISE,
201
+ get_work_item_comments: TIERS.ENTERPRISE,
202
+ get_work_board_rollup: TIERS.ENTERPRISE,
203
+ list_work_board_rollups: TIERS.ENTERPRISE,
204
+ move_work_item: TIERS.ENTERPRISE,
205
+ set_work_item_tag: TIERS.ENTERPRISE,
206
+ set_work_item_kind: TIERS.ENTERPRISE,
207
+ create_work_item: TIERS.ENTERPRISE,
208
+ comment_on_work_item: TIERS.ENTERPRISE,
209
+ flag_work_item: TIERS.ENTERPRISE,
210
+
211
+ // --- ENTERPRISE tier: Paper desktop app surfaces ---
212
+ paper_list_documents: TIERS.ENTERPRISE,
213
+ paper_get_document: TIERS.ENTERPRISE,
214
+ paper_create_document: TIERS.ENTERPRISE,
215
+ paper_update_document: TIERS.ENTERPRISE,
216
+ paper_archive_document: TIERS.ENTERPRISE,
217
+ paper_delete_document: TIERS.ENTERPRISE,
218
+ paper_list_shares: TIERS.ENTERPRISE,
219
+ paper_create_share: TIERS.ENTERPRISE,
220
+ paper_revoke_share: TIERS.ENTERPRISE,
221
+ paper_resolve_share: TIERS.ENTERPRISE,
222
+ paper_list_tasks: TIERS.ENTERPRISE,
223
+ paper_create_task: TIERS.ENTERPRISE,
224
+ paper_update_task: TIERS.ENTERPRISE,
225
+ paper_list_calendar_events: TIERS.ENTERPRISE,
226
+ paper_create_calendar_event: TIERS.ENTERPRISE,
227
+ paper_list_email_threads: TIERS.ENTERPRISE,
228
+ paper_get_email_thread: TIERS.ENTERPRISE,
229
+ paper_send_email: TIERS.ENTERPRISE,
230
+ paper_get_company: TIERS.ENTERPRISE,
231
+
232
+ // --- ENTERPRISE tier: enterprise-only tools (bulk, export, admin) ---
233
+ bulk_import: TIERS.ENTERPRISE,
234
+ export_data: TIERS.ENTERPRISE,
235
+ manage_custom_fields: TIERS.ENTERPRISE,
236
+ get_workspace_settings: TIERS.ENTERPRISE,
237
+
238
+ // --- ENTERPRISE tier: email infrastructure (domains, mailboxes, sequences) ---
239
+ search_domains: TIERS.ENTERPRISE,
240
+ purchase_domain: TIERS.ENTERPRISE,
241
+ setup_domain: TIERS.ENTERPRISE,
242
+ verify_domain: TIERS.ENTERPRISE,
243
+ list_domains: TIERS.ENTERPRISE,
244
+ create_email_account: TIERS.ENTERPRISE,
245
+ list_email_accounts: TIERS.ENTERPRISE,
246
+ warmup_email: TIERS.ENTERPRISE,
247
+ get_email_health: TIERS.ENTERPRISE,
248
+ create_sequence: TIERS.ENTERPRISE,
249
+ list_sequences_full: TIERS.ENTERPRISE,
250
+ add_sequence_step: TIERS.ENTERPRISE,
251
+ activate_sequence: TIERS.ENTERPRISE,
252
+ pause_sequence: TIERS.ENTERPRISE,
253
+ get_sequence_analytics: TIERS.ENTERPRISE,
254
+ enroll_contacts: TIERS.ENTERPRISE,
255
+
256
+ // --- PARTNER ECOSYSTEM: reads are pro, every write is enterprise ---
257
+ // These previously had no entries at all and silently fell back to 'pro',
258
+ // which left partner WRITES (attribution, consumption, approval) one tier
259
+ // below every other CRM write. Reads stay pro to match the other
260
+ // workspace-read surfaces (get_company, get_pipeline_metrics, ...).
261
+ list_partners: TIERS.PRO,
262
+ get_partner: TIERS.PRO,
263
+ rank_partners_for_account: TIERS.PRO,
264
+ get_partner_revenue: TIERS.PRO,
265
+ list_partner_attributions: TIERS.PRO,
266
+ list_strategic_work_streams: TIERS.PRO,
267
+ list_executive_partner_meetings: TIERS.PRO,
268
+ list_partner_consumption: TIERS.PRO,
269
+ get_partner_consumption_rollup: TIERS.PRO,
270
+ create_partner: TIERS.ENTERPRISE,
271
+ update_partner: TIERS.ENTERPRISE,
272
+ delete_partner: TIERS.ENTERPRISE,
273
+ attribute_partner_to_deal: TIERS.ENTERPRISE,
274
+ approve_partner_attribution: TIERS.ENTERPRISE,
275
+ create_strategic_work_stream: TIERS.ENTERPRISE,
276
+ create_executive_partner_meeting: TIERS.ENTERPRISE,
277
+ record_partner_consumption: TIERS.ENTERPRISE,
278
+
279
+ // --- PRO tier: access graph / path-to-power intelligence reads ---
280
+ get_access_graph_projection: TIERS.PRO,
281
+ get_access_score: TIERS.PRO,
282
+ get_path_to_power: TIERS.PRO,
283
+
284
+ // --- PRO tier: enrichment read (same tier as enrich_company) ---
285
+ get_company_firmographics: TIERS.PRO,
286
+
287
+ // --- PRO tier: Matrix analytics (read-only; module documents "Pro tier").
288
+ // registerGetTool registers each surface under BOTH a dotted and an
289
+ // underscore alias, and both names hit the same tier gate. ---
290
+ ask_matrix: TIERS.PRO,
291
+ get_matrix_anomalies: TIERS.PRO,
292
+ get_matrix_deal_probability: TIERS.PRO,
293
+ get_matrix_digest_preview: TIERS.PRO,
294
+ get_matrix_recommended_actions: TIERS.PRO,
295
+ 'analytics.matrix.nl_ask': TIERS.PRO,
296
+ matrix_nl_ask: TIERS.PRO,
297
+ 'analytics.matrix.anomalies_feed': TIERS.PRO,
298
+ matrix_anomalies_feed: TIERS.PRO,
299
+ 'analytics.matrix.forecast_commit_best_worst': TIERS.PRO,
300
+ matrix_forecast_commit_best_worst: TIERS.PRO,
301
+ 'analytics.matrix.forecast_multi_model_consensus': TIERS.PRO,
302
+ matrix_forecast_multi_model_consensus: TIERS.PRO,
303
+ 'analytics.matrix.market_competitor_deal_coaching': TIERS.PRO,
304
+ matrix_market_competitor_deal_coaching: TIERS.PRO,
305
+ 'analytics.matrix.market_competitor_pulse': TIERS.PRO,
306
+ matrix_market_competitor_pulse: TIERS.PRO,
307
+ 'analytics.matrix.people_causal_lift': TIERS.PRO,
308
+ matrix_people_causal_lift: TIERS.PRO,
309
+ 'analytics.matrix.people_single_thread_risk': TIERS.PRO,
310
+ matrix_people_single_thread_risk: TIERS.PRO,
311
+ 'analytics.matrix.pipeline_deal_health_queue': TIERS.PRO,
312
+ matrix_pipeline_deal_health_queue: TIERS.PRO,
313
+ 'analytics.matrix.pipeline_snapshot_diff': TIERS.PRO,
314
+ matrix_pipeline_snapshot_diff: TIERS.PRO,
315
+
316
+ // --- PRO tier: knowledge hub (module documents "Pro tier") ---
317
+ search_knowledge: TIERS.PRO,
318
+ get_account_wiki: TIERS.PRO,
319
+ create_knowledge_file: TIERS.PRO,
320
+ link_file_to_entity: TIERS.PRO,
321
+
322
+ // --- PRO tier: extensibility / workflow authoring. These were on the 'pro'
323
+ // fallback; the tier is now explicit. Writes stay governed by the headless
324
+ // operation policy (dry-run default + control-plane review), not by tier. ---
325
+ inspect_provider_catalog: TIERS.PRO,
326
+ get_provider_connect_plan: TIERS.PRO,
327
+ list_provider_catalog: TIERS.PRO,
328
+ list_provider_endpoints: TIERS.PRO,
329
+ add_provider_action_column: TIERS.PRO,
330
+ test_provider_credential: TIERS.PRO,
331
+ request_provider_action_execution: TIERS.PRO,
332
+ request_deployment: TIERS.PRO,
333
+ submit_adrata_command: TIERS.PRO,
334
+ draft_workflow: TIERS.PRO,
335
+ validate_workflow_draft: TIERS.PRO,
336
+ dry_run_workflow: TIERS.PRO,
337
+ request_workflow_deployment: TIERS.PRO,
338
+ replay_workflow_run: TIERS.PRO,
339
+ validate_extension_manifest: TIERS.PRO,
340
+ validate_integration_manifest: TIERS.PRO,
341
+
342
+ // --- ANALYTICS: local dashboard (free), server-side (pro+) ---
343
+ get_mcp_analytics: TIERS.FREE,
344
+ track_conversion: TIERS.PRO,
345
+
346
+ // --- COMPOSITE TOOLSETS: always-loaded (free tier) ---
347
+ list_toolsets: TIERS.FREE,
348
+ enable_toolset: TIERS.FREE,
349
+ get_demo_availability: TIERS.FREE,
350
+ // find_company, find_person already mapped above
351
+
352
+ // --- COMPOSITE TOOLSETS: prospecting (pro tier) ---
353
+ qualify_company: TIERS.PRO,
354
+ research_company: TIERS.PRO,
355
+ research_person: TIERS.PRO,
356
+ // get_speedrun_list already mapped above
357
+ discover_prospects: TIERS.PRO,
358
+ get_next_contacts: TIERS.PRO,
359
+
360
+ // --- COMPOSITE TOOLSETS: intelligence (pro tier) ---
361
+ get_competitive_intel: TIERS.PRO,
362
+ get_meeting_brief: TIERS.PRO,
363
+ get_deal_coaching: TIERS.PRO,
364
+ get_signals_dashboard: TIERS.PRO,
365
+ get_forecast: TIERS.PRO,
366
+
367
+ // --- COMPOSITE TOOLSETS: outreach (pro/enterprise tier) ---
368
+ draft_email: TIERS.PRO,
369
+ send_email: TIERS.ENTERPRISE,
370
+ reply_to_email: TIERS.ENTERPRISE,
371
+ manage_sequences: TIERS.ENTERPRISE,
372
+ get_outreach_analytics: TIERS.PRO,
373
+ get_network_paths: TIERS.PRO,
374
+ search_emails_composite: TIERS.PRO,
375
+ get_email_thread: TIERS.PRO,
376
+
377
+ // --- COMPOSITE TOOLSETS: crm (enterprise tier) ---
378
+ manage_company: TIERS.ENTERPRISE,
379
+ manage_person: TIERS.ENTERPRISE,
380
+ manage_opportunity: TIERS.ENTERPRISE,
381
+ manage_activity: TIERS.ENTERPRISE,
382
+ manage_buyer_group: TIERS.ENTERPRISE,
383
+ get_action_history: TIERS.ENTERPRISE,
384
+
385
+ // --- COMPOSITE TOOLSETS: communications (enterprise tier) ---
386
+ make_call: TIERS.ENTERPRISE,
387
+ send_sms: TIERS.ENTERPRISE,
388
+ get_call_transcript: TIERS.ENTERPRISE,
389
+ check_calendar: TIERS.ENTERPRISE,
390
+ schedule_meeting: TIERS.ENTERPRISE,
391
+
392
+ // --- COMPOSITE TOOLSETS: infrastructure (enterprise tier) ---
393
+ manage_domains: TIERS.ENTERPRISE,
394
+ manage_mailboxes: TIERS.ENTERPRISE,
395
+ get_deliverability: TIERS.ENTERPRISE,
396
+ connect_provider: TIERS.ENTERPRISE,
397
+ manage_workspace: TIERS.ENTERPRISE,
398
+ manage_data: TIERS.ENTERPRISE,
399
+ };
400
+
401
+ /**
402
+ * Get the required tier for a tool.
403
+ *
404
+ * Unregistered tools default to the MOST restrictive tier (enterprise), never
405
+ * 'pro': a permissive fallback is how ~20 partner tools — including writes —
406
+ * silently shipped one tier below every other CRM write. A registry test
407
+ * asserts every registered tool has an explicit entry, so this fallback only
408
+ * exists as a fail-closed guard for future omissions.
409
+ */
410
+ export function getToolTier(toolName) {
411
+ return TOOL_TIERS[toolName] || TIERS.ENTERPRISE;
412
+ }
413
+
414
+ /**
415
+ * Tier hierarchy: enterprise > pro > free.
416
+ * Returns true if userTier >= requiredTier.
417
+ */
418
+ const TIER_RANK = { [TIERS.FREE]: 0, [TIERS.PRO]: 1, [TIERS.ENTERPRISE]: 2 };
419
+
420
+ export function tierSatisfies(userTier, requiredTier) {
421
+ return (TIER_RANK[userTier] ?? 0) >= (TIER_RANK[requiredTier] ?? 0);
422
+ }