@vantageos/vantage-crm-mcp 0.1.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 (48) hide show
  1. package/README.md +260 -0
  2. package/dist/convex/crm/_helpers.js +24 -0
  3. package/dist/convex/crm/activities.js +220 -0
  4. package/dist/convex/crm/briefing.js +198 -0
  5. package/dist/convex/crm/calendarCron.js +92 -0
  6. package/dist/convex/crm/calendarCronDispatch.js +83 -0
  7. package/dist/convex/crm/calendarSync.js +294 -0
  8. package/dist/convex/crm/companies.js +323 -0
  9. package/dist/convex/crm/contacts.js +346 -0
  10. package/dist/convex/crm/deals.js +481 -0
  11. package/dist/convex/crm/emailActions.js +158 -0
  12. package/dist/convex/crm/emailCron.js +210 -0
  13. package/dist/convex/crm/emailCronDispatch.js +76 -0
  14. package/dist/convex/crm/emailSync.js +260 -0
  15. package/dist/convex/crm/onboarding.js +185 -0
  16. package/dist/convex/crm/stats.js +75 -0
  17. package/dist/convex/crm/tasks.js +109 -0
  18. package/dist/convex/crons.js +25 -0
  19. package/dist/convex/integrations.js +183 -0
  20. package/dist/convex/lib/auditLog.js +109 -0
  21. package/dist/convex/lib/auth.js +372 -0
  22. package/dist/convex/lib/rbac.js +123 -0
  23. package/dist/convex/lib/workspace.js +171 -0
  24. package/dist/convex/organizations.js +192 -0
  25. package/dist/convex/schema.js +690 -0
  26. package/dist/convex/users.js +217 -0
  27. package/dist/convex/workspaces.js +603 -0
  28. package/dist/mcp-server/lib/convexClient.js +50 -0
  29. package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
  30. package/dist/mcp-server/registry.js +116 -0
  31. package/dist/mcp-server/server.js +97 -0
  32. package/dist/mcp-server/tests/registry.test.js +163 -0
  33. package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
  34. package/dist/mcp-server/tests/security.test.js +257 -0
  35. package/dist/mcp-server/tests/tools.test.js +272 -0
  36. package/dist/mcp-server/tools/activities.js +207 -0
  37. package/dist/mcp-server/tools/admin.js +190 -0
  38. package/dist/mcp-server/tools/companies.js +233 -0
  39. package/dist/mcp-server/tools/contacts.js +306 -0
  40. package/dist/mcp-server/tools/customFields.js +222 -0
  41. package/dist/mcp-server/tools/customObjects.js +235 -0
  42. package/dist/mcp-server/tools/deals.js +297 -0
  43. package/dist/mcp-server/tools/rbac.js +177 -0
  44. package/dist/mcp-server/tools/search.js +155 -0
  45. package/dist/mcp-server/tools/workflows.js +234 -0
  46. package/dist/mcp-server/transport/http.js +257 -0
  47. package/dist/mcp-server/transport/stdio.js +90 -0
  48. package/package.json +45 -0
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP Server — Scope Enforcement
4
+ *
5
+ * 4-tier scope hierarchy:
6
+ * read — list/get queries (any authenticated token)
7
+ * write — create/update/delete mutations
8
+ * admin — admin-only operations (audit log, purge, RBAC mutations)
9
+ * cloud-admin — cross-workspace operations (multi-tenant platform admin)
10
+ *
11
+ * Stdio mode: full local access, no scope filtering (trust local user).
12
+ * HTTP mode: scopes extracted from Bearer token → filter + reject on mismatch.
13
+ *
14
+ * Ref: elpi-corp/analysis/vantage-crm-spec-2026-05-20.md §3
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ScopeError = void 0;
18
+ exports.hasScope = hasScope;
19
+ exports.requireScope = requireScope;
20
+ exports.filterToolsByScope = filterToolsByScope;
21
+ /** Ordered scope hierarchy for inheritance checks */
22
+ const SCOPE_LEVEL = {
23
+ 'read': 1,
24
+ 'write': 2,
25
+ 'workflow-trigger': 2, // same level as write, different domain
26
+ 'admin': 3,
27
+ 'cloud-admin': 4,
28
+ };
29
+ /**
30
+ * Check if the token's scopes satisfy the required scope.
31
+ * cloud-admin satisfies all scopes (superscope).
32
+ * admin satisfies read + write.
33
+ * write satisfies read.
34
+ */
35
+ function hasScope(tokenScopes, required) {
36
+ if (tokenScopes.includes('cloud-admin'))
37
+ return true;
38
+ if (tokenScopes.includes(required))
39
+ return true;
40
+ // admin implies read + write
41
+ if (tokenScopes.includes('admin')) {
42
+ if (required === 'read' || required === 'write')
43
+ return true;
44
+ }
45
+ // write implies read
46
+ if (tokenScopes.includes('write') && required === 'read')
47
+ return true;
48
+ return false;
49
+ }
50
+ /**
51
+ * Require a scope — throws ScopeError if not satisfied.
52
+ * Used in HTTP mode before every tool handler.
53
+ */
54
+ function requireScope(tokenScopes, required, toolName) {
55
+ if (!hasScope(tokenScopes, required)) {
56
+ throw new ScopeError(`Tool '${toolName}' requires scope '${required}'. Token has: [${tokenScopes.join(', ')}]`, required, tokenScopes);
57
+ }
58
+ }
59
+ class ScopeError extends Error {
60
+ required;
61
+ provided;
62
+ constructor(message, required, provided) {
63
+ super(message);
64
+ this.name = 'ScopeError';
65
+ this.required = required;
66
+ this.provided = provided;
67
+ }
68
+ }
69
+ exports.ScopeError = ScopeError;
70
+ /**
71
+ * Filter a tool list to only include tools satisfying the given token scopes.
72
+ * Used in HTTP mode to build the visible tool list per token.
73
+ */
74
+ function filterToolsByScope(tools, tokenScopes) {
75
+ return tools.filter((tool) => hasScope(tokenScopes, tool.requiredScope));
76
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP — Central Tool Registry
4
+ *
5
+ * Aggregates all 60 tools from 10 categories.
6
+ * Each tool: name, description, requiredScope, inputSchema, handler.
7
+ *
8
+ * Tool count: 8 + 6 + 8 + 5 + 5 + 7 + 7 + 4 + 5 + 5 = 60
9
+ *
10
+ * Ref: elpi-corp/analysis/vantage-crm-spec-2026-05-20.md §3.12
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.TOOL_HANDLERS = exports.ALL_TOOL_DEFINITIONS = void 0;
14
+ const contacts_1 = require("./tools/contacts");
15
+ const companies_1 = require("./tools/companies");
16
+ const deals_1 = require("./tools/deals");
17
+ const activities_1 = require("./tools/activities");
18
+ const customFields_1 = require("./tools/customFields");
19
+ const customObjects_1 = require("./tools/customObjects");
20
+ const workflows_1 = require("./tools/workflows");
21
+ const search_1 = require("./tools/search");
22
+ const admin_1 = require("./tools/admin");
23
+ const rbac_1 = require("./tools/rbac");
24
+ /** All tool definitions for MCP registration */
25
+ exports.ALL_TOOL_DEFINITIONS = [
26
+ ...contacts_1.CONTACT_TOOLS, // 8
27
+ ...companies_1.COMPANY_TOOLS, // 6
28
+ ...deals_1.DEAL_TOOLS, // 8
29
+ ...activities_1.ACTIVITY_TOOLS, // 5
30
+ ...customFields_1.CUSTOM_FIELD_TOOLS, // 5
31
+ ...customObjects_1.CUSTOM_OBJECT_TOOLS, // 7
32
+ ...workflows_1.WORKFLOW_TOOLS, // 7
33
+ ...search_1.SEARCH_TOOLS, // 4
34
+ ...admin_1.ADMIN_TOOLS, // 5
35
+ ...rbac_1.RBAC_TOOLS, // 5
36
+ // TOTAL: 60
37
+ ];
38
+ exports.TOOL_HANDLERS = {
39
+ // Contacts (8)
40
+ create_contact: contacts_1.create_contact,
41
+ get_contact: contacts_1.get_contact,
42
+ update_contact: contacts_1.update_contact,
43
+ list_contacts: contacts_1.list_contacts,
44
+ search_contacts: contacts_1.search_contacts,
45
+ delete_contact: contacts_1.delete_contact,
46
+ list_contacts_by_custom_field: contacts_1.list_contacts_by_custom_field,
47
+ restore_contact: contacts_1.restore_contact,
48
+ // Companies (6)
49
+ create_company: companies_1.create_company,
50
+ get_company: companies_1.get_company,
51
+ update_company: companies_1.update_company,
52
+ list_companies: companies_1.list_companies,
53
+ search_companies: companies_1.search_companies,
54
+ delete_company: companies_1.delete_company,
55
+ // Deals (8)
56
+ create_deal: deals_1.create_deal,
57
+ get_deal: deals_1.get_deal,
58
+ update_deal: deals_1.update_deal,
59
+ list_deals: deals_1.list_deals,
60
+ search_deals: deals_1.search_deals,
61
+ delete_deal: deals_1.delete_deal,
62
+ move_deal_stage: deals_1.move_deal_stage,
63
+ forecast_pipeline: deals_1.forecast_pipeline,
64
+ // Activities (5)
65
+ create_activity: activities_1.create_activity,
66
+ get_activity: activities_1.get_activity,
67
+ update_activity: activities_1.update_activity,
68
+ list_activities: activities_1.list_activities,
69
+ list_activities_by_type: activities_1.list_activities_by_type,
70
+ // Custom Fields (5)
71
+ add_custom_field: customFields_1.add_custom_field,
72
+ update_custom_field_definition: customFields_1.update_custom_field_definition,
73
+ list_custom_fields: customFields_1.list_custom_fields,
74
+ set_custom_field_value: customFields_1.set_custom_field_value,
75
+ delete_custom_field: customFields_1.delete_custom_field,
76
+ // Custom Objects (7)
77
+ add_custom_object: customObjects_1.add_custom_object,
78
+ list_custom_objects: customObjects_1.list_custom_objects,
79
+ create_record: customObjects_1.create_record,
80
+ update_record: customObjects_1.update_record,
81
+ delete_record: customObjects_1.delete_record,
82
+ list_records: customObjects_1.list_records,
83
+ search_records: customObjects_1.search_records,
84
+ // Workflows (7)
85
+ create_workflow: workflows_1.create_workflow,
86
+ update_workflow: workflows_1.update_workflow,
87
+ list_workflows: workflows_1.list_workflows,
88
+ pause_workflow: workflows_1.pause_workflow,
89
+ resume_workflow: workflows_1.resume_workflow,
90
+ list_executions: workflows_1.list_executions,
91
+ replay_execution: workflows_1.replay_execution,
92
+ // Search / Analytics (4)
93
+ pipeline_value: search_1.pipeline_value,
94
+ win_rate: search_1.win_rate,
95
+ conversion_rate: search_1.conversion_rate,
96
+ activity_summary: search_1.activity_summary,
97
+ // Admin (5)
98
+ list_audit_log: admin_1.list_audit_log,
99
+ get_entity_history: admin_1.get_entity_history,
100
+ get_actor_history: admin_1.get_actor_history,
101
+ purge_archived_records: admin_1.purge_archived_records,
102
+ list_workspace_limits: admin_1.list_workspace_limits,
103
+ // RBAC (5)
104
+ list_workspace_members: rbac_1.list_workspace_members,
105
+ add_workspace_member: rbac_1.add_workspace_member,
106
+ update_member_role: rbac_1.update_member_role,
107
+ remove_workspace_member: rbac_1.remove_workspace_member,
108
+ update_member_permissions: rbac_1.update_member_permissions,
109
+ };
110
+ /** Validate handler count matches definition count at module load */
111
+ const handlerCount = Object.keys(exports.TOOL_HANDLERS).length;
112
+ const definitionCount = exports.ALL_TOOL_DEFINITIONS.length;
113
+ if (handlerCount !== definitionCount) {
114
+ throw new Error(`Registry mismatch: ${handlerCount} handlers vs ${definitionCount} definitions. ` +
115
+ `Check mcp-server/registry.ts.`);
116
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * VantageCRM MCP Server — Entry Point
5
+ *
6
+ * Mode detection (stdio vs HTTP):
7
+ * 1. CLI flag: --mode stdio | --mode http
8
+ * 2. Env var: VANTAGE_MCP_MODE=stdio | VANTAGE_MCP_MODE=http
9
+ * 3. Default: stdio (safe for local/self-host)
10
+ *
11
+ * Required env:
12
+ * CONVEX_URL — Convex deployment URL (all modes)
13
+ *
14
+ * Optional env (HTTP mode only):
15
+ * PORT — HTTP port (default 3001)
16
+ *
17
+ * Usage examples:
18
+ * stdio: npx ts-node mcp-server/server.ts --mode stdio
19
+ * HTTP: VANTAGE_MCP_MODE=http PORT=3001 npx ts-node mcp-server/server.ts
20
+ *
21
+ * Ref: elpi-corp/analysis/vantage-crm-spec-2026-05-20.md §3.1
22
+ */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ function detectMode() {
57
+ // Check CLI args first
58
+ const modeArgIndex = process.argv.indexOf('--mode');
59
+ if (modeArgIndex !== -1) {
60
+ const modeValue = process.argv[modeArgIndex + 1];
61
+ if (modeValue === 'http')
62
+ return 'http';
63
+ if (modeValue === 'stdio')
64
+ return 'stdio';
65
+ }
66
+ // Check env var
67
+ const envMode = process.env.VANTAGE_MCP_MODE?.toLowerCase();
68
+ if (envMode === 'http')
69
+ return 'http';
70
+ if (envMode === 'stdio')
71
+ return 'stdio';
72
+ // Default: stdio (safest for self-host)
73
+ return 'stdio';
74
+ }
75
+ async function main() {
76
+ const mode = detectMode();
77
+ // Validate CONVEX_URL is set (required in both modes)
78
+ if (!process.env.CONVEX_URL) {
79
+ process.stderr.write('[VantageCRM MCP] ERROR: CONVEX_URL environment variable is not set.\n' +
80
+ ' Set it to your Convex deployment URL: https://your-deployment.convex.cloud\n');
81
+ process.exit(1);
82
+ }
83
+ process.stderr.write(`[VantageCRM MCP] Starting in ${mode} mode...\n`);
84
+ if (mode === 'stdio') {
85
+ const { startStdioServer } = await Promise.resolve().then(() => __importStar(require('./transport/stdio')));
86
+ await startStdioServer();
87
+ }
88
+ else {
89
+ const { startHttpServer } = await Promise.resolve().then(() => __importStar(require('./transport/http')));
90
+ await startHttpServer();
91
+ }
92
+ }
93
+ main().catch((err) => {
94
+ const message = err instanceof Error ? err.message : String(err);
95
+ process.stderr.write(`[VantageCRM MCP] Fatal error: ${message}\n`);
96
+ process.exit(1);
97
+ });
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Server — Registry Tests
4
+ *
5
+ * Validates:
6
+ * - Exactly 60 tools are registered
7
+ * - Every tool has a handler
8
+ * - Every tool has required fields (name, description, requiredScope, inputSchema)
9
+ * - No duplicate tool names
10
+ * - All categories have correct counts
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const vitest_1 = require("vitest");
14
+ const registry_1 = require("../registry");
15
+ (0, vitest_1.describe)('MCP Registry', () => {
16
+ (0, vitest_1.it)('has exactly 60 tools', () => {
17
+ (0, vitest_1.expect)(registry_1.ALL_TOOL_DEFINITIONS).toHaveLength(60);
18
+ });
19
+ (0, vitest_1.it)('has exactly 60 handlers', () => {
20
+ (0, vitest_1.expect)(Object.keys(registry_1.TOOL_HANDLERS)).toHaveLength(60);
21
+ });
22
+ (0, vitest_1.it)('every tool definition has a handler', () => {
23
+ const missing = registry_1.ALL_TOOL_DEFINITIONS.filter((def) => !registry_1.TOOL_HANDLERS[def.name]);
24
+ (0, vitest_1.expect)(missing.map((d) => d.name)).toEqual([]);
25
+ });
26
+ (0, vitest_1.it)('no duplicate tool names', () => {
27
+ const names = registry_1.ALL_TOOL_DEFINITIONS.map((d) => d.name);
28
+ const unique = new Set(names);
29
+ (0, vitest_1.expect)(unique.size).toBe(names.length);
30
+ });
31
+ (0, vitest_1.it)('every tool has required fields', () => {
32
+ for (const def of registry_1.ALL_TOOL_DEFINITIONS) {
33
+ (0, vitest_1.expect)(def.name, `Tool missing name`).toBeTruthy();
34
+ (0, vitest_1.expect)(def.description, `${def.name} missing description`).toBeTruthy();
35
+ (0, vitest_1.expect)(def.requiredScope, `${def.name} missing requiredScope`).toBeTruthy();
36
+ (0, vitest_1.expect)(def.inputSchema, `${def.name} missing inputSchema`).toBeTruthy();
37
+ }
38
+ });
39
+ (0, vitest_1.it)('all scope values are valid', () => {
40
+ const validScopes = new Set(['read', 'write', 'admin', 'cloud-admin', 'workflow-trigger']);
41
+ for (const def of registry_1.ALL_TOOL_DEFINITIONS) {
42
+ (0, vitest_1.expect)(validScopes.has(def.requiredScope), `${def.name} has invalid scope: ${def.requiredScope}`).toBe(true);
43
+ }
44
+ });
45
+ (0, vitest_1.describe)('category counts', () => {
46
+ const CONTACT_TOOLS = ['create_contact', 'get_contact', 'update_contact', 'list_contacts', 'search_contacts', 'delete_contact', 'list_contacts_by_custom_field', 'restore_contact'];
47
+ const COMPANY_TOOLS = ['create_company', 'get_company', 'update_company', 'list_companies', 'search_companies', 'delete_company'];
48
+ const DEAL_TOOLS = ['create_deal', 'get_deal', 'update_deal', 'list_deals', 'search_deals', 'delete_deal', 'move_deal_stage', 'forecast_pipeline'];
49
+ const ACTIVITY_TOOLS = ['create_activity', 'get_activity', 'update_activity', 'list_activities', 'list_activities_by_type'];
50
+ const CUSTOM_FIELD_TOOLS = ['add_custom_field', 'update_custom_field_definition', 'list_custom_fields', 'set_custom_field_value', 'delete_custom_field'];
51
+ const CUSTOM_OBJECT_TOOLS = ['add_custom_object', 'list_custom_objects', 'create_record', 'update_record', 'delete_record', 'list_records', 'search_records'];
52
+ const WORKFLOW_TOOLS = ['create_workflow', 'update_workflow', 'list_workflows', 'pause_workflow', 'resume_workflow', 'list_executions', 'replay_execution'];
53
+ const SEARCH_TOOLS = ['pipeline_value', 'win_rate', 'conversion_rate', 'activity_summary'];
54
+ const ADMIN_TOOLS = ['list_audit_log', 'get_entity_history', 'get_actor_history', 'purge_archived_records', 'list_workspace_limits'];
55
+ const RBAC_TOOLS = ['list_workspace_members', 'add_workspace_member', 'update_member_role', 'remove_workspace_member', 'update_member_permissions'];
56
+ const registeredNames = new Set(registry_1.ALL_TOOL_DEFINITIONS.map((d) => d.name));
57
+ (0, vitest_1.it)('contacts: 8 tools', () => {
58
+ for (const name of CONTACT_TOOLS) {
59
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
60
+ }
61
+ (0, vitest_1.expect)(CONTACT_TOOLS).toHaveLength(8);
62
+ });
63
+ (0, vitest_1.it)('companies: 6 tools', () => {
64
+ for (const name of COMPANY_TOOLS) {
65
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
66
+ }
67
+ (0, vitest_1.expect)(COMPANY_TOOLS).toHaveLength(6);
68
+ });
69
+ (0, vitest_1.it)('deals: 8 tools', () => {
70
+ for (const name of DEAL_TOOLS) {
71
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
72
+ }
73
+ (0, vitest_1.expect)(DEAL_TOOLS).toHaveLength(8);
74
+ });
75
+ (0, vitest_1.it)('activities: 5 tools (no delete — OQ-4)', () => {
76
+ for (const name of ACTIVITY_TOOLS) {
77
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
78
+ }
79
+ (0, vitest_1.expect)(ACTIVITY_TOOLS).toHaveLength(5);
80
+ (0, vitest_1.expect)(registeredNames.has('delete_activity')).toBe(false);
81
+ });
82
+ (0, vitest_1.it)('customFields: 5 tools', () => {
83
+ for (const name of CUSTOM_FIELD_TOOLS) {
84
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
85
+ }
86
+ (0, vitest_1.expect)(CUSTOM_FIELD_TOOLS).toHaveLength(5);
87
+ });
88
+ (0, vitest_1.it)('customObjects: 7 tools', () => {
89
+ for (const name of CUSTOM_OBJECT_TOOLS) {
90
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
91
+ }
92
+ (0, vitest_1.expect)(CUSTOM_OBJECT_TOOLS).toHaveLength(7);
93
+ });
94
+ (0, vitest_1.it)('workflows: 7 tools', () => {
95
+ for (const name of WORKFLOW_TOOLS) {
96
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
97
+ }
98
+ (0, vitest_1.expect)(WORKFLOW_TOOLS).toHaveLength(7);
99
+ });
100
+ (0, vitest_1.it)('search/analytics: 4 tools', () => {
101
+ for (const name of SEARCH_TOOLS) {
102
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
103
+ }
104
+ (0, vitest_1.expect)(SEARCH_TOOLS).toHaveLength(4);
105
+ });
106
+ (0, vitest_1.it)('admin: 5 tools (admin scope minimum)', () => {
107
+ for (const name of ADMIN_TOOLS) {
108
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
109
+ }
110
+ (0, vitest_1.expect)(ADMIN_TOOLS).toHaveLength(5);
111
+ // Verify all admin tools require at minimum admin scope
112
+ for (const name of ADMIN_TOOLS) {
113
+ const def = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === name);
114
+ (0, vitest_1.expect)(def.requiredScope === 'admin' || def.requiredScope === 'cloud-admin', `${name} must require admin or cloud-admin scope`).toBe(true);
115
+ }
116
+ });
117
+ (0, vitest_1.it)('rbac: 5 tools', () => {
118
+ for (const name of RBAC_TOOLS) {
119
+ (0, vitest_1.expect)(registeredNames.has(name), `missing: ${name}`).toBe(true);
120
+ }
121
+ (0, vitest_1.expect)(RBAC_TOOLS).toHaveLength(5);
122
+ });
123
+ });
124
+ (0, vitest_1.describe)('scope integrity', () => {
125
+ (0, vitest_1.it)('all admin tools require admin+ scope', () => {
126
+ const adminToolNames = ['list_audit_log', 'get_entity_history', 'get_actor_history', 'purge_archived_records'];
127
+ for (const name of adminToolNames) {
128
+ const def = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === name);
129
+ (0, vitest_1.expect)(def.requiredScope).toBe('admin');
130
+ }
131
+ });
132
+ (0, vitest_1.it)('delete_custom_field requires admin scope', () => {
133
+ const def = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === 'delete_custom_field');
134
+ (0, vitest_1.expect)(def.requiredScope).toBe('admin');
135
+ });
136
+ (0, vitest_1.it)('add_custom_object requires admin scope (DDL)', () => {
137
+ const def = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === 'add_custom_object');
138
+ (0, vitest_1.expect)(def.requiredScope).toBe('admin');
139
+ });
140
+ (0, vitest_1.it)('replay_execution requires workflow-trigger scope', () => {
141
+ const def = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === 'replay_execution');
142
+ (0, vitest_1.expect)(def.requiredScope).toBe('workflow-trigger');
143
+ });
144
+ (0, vitest_1.it)('all list/get tools require only read scope', () => {
145
+ const readOnlyTools = ['get_contact', 'list_contacts', 'get_company', 'list_companies',
146
+ 'get_deal', 'list_deals', 'get_activity', 'list_activities',
147
+ 'list_custom_fields', 'list_custom_objects', 'list_records',
148
+ 'list_workflows', 'list_executions', 'pipeline_value', 'win_rate',
149
+ 'conversion_rate', 'activity_summary', 'list_workspace_members'];
150
+ for (const name of readOnlyTools) {
151
+ const def = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === name);
152
+ (0, vitest_1.expect)(def?.requiredScope, `${name} should be read scope`).toBe('read');
153
+ }
154
+ });
155
+ });
156
+ (0, vitest_1.describe)('handler validation', () => {
157
+ (0, vitest_1.it)('all handlers are functions', () => {
158
+ for (const [name, handler] of Object.entries(registry_1.TOOL_HANDLERS)) {
159
+ (0, vitest_1.expect)(typeof handler, `handler for ${name} is not a function`).toBe('function');
160
+ }
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Server — Scope Enforcement Tests
4
+ *
5
+ * Tests the 4-tier scope hierarchy:
6
+ * read < write < admin < cloud-admin
7
+ *
8
+ * Validates: read token → write tool rejected
9
+ * write token → admin tool rejected
10
+ * admin token → read/write tools pass
11
+ * cloud-admin → all tools pass
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const vitest_1 = require("vitest");
15
+ const scopeEnforcement_1 = require("../lib/scopeEnforcement");
16
+ // ---------------------------------------------------------------------------
17
+ // hasScope
18
+ // ---------------------------------------------------------------------------
19
+ (0, vitest_1.describe)('hasScope', () => {
20
+ (0, vitest_1.it)('read token satisfies read', () => {
21
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['read'], 'read')).toBe(true);
22
+ });
23
+ (0, vitest_1.it)('read token does NOT satisfy write', () => {
24
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['read'], 'write')).toBe(false);
25
+ });
26
+ (0, vitest_1.it)('read token does NOT satisfy admin', () => {
27
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['read'], 'admin')).toBe(false);
28
+ });
29
+ (0, vitest_1.it)('write token satisfies read (inheritance)', () => {
30
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['write'], 'read')).toBe(true);
31
+ });
32
+ (0, vitest_1.it)('write token satisfies write', () => {
33
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['write'], 'write')).toBe(true);
34
+ });
35
+ (0, vitest_1.it)('write token does NOT satisfy admin', () => {
36
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['write'], 'admin')).toBe(false);
37
+ });
38
+ (0, vitest_1.it)('admin token satisfies read (inheritance)', () => {
39
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['admin'], 'read')).toBe(true);
40
+ });
41
+ (0, vitest_1.it)('admin token satisfies write (inheritance)', () => {
42
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['admin'], 'write')).toBe(true);
43
+ });
44
+ (0, vitest_1.it)('admin token satisfies admin', () => {
45
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['admin'], 'admin')).toBe(true);
46
+ });
47
+ (0, vitest_1.it)('admin token does NOT satisfy cloud-admin', () => {
48
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['admin'], 'cloud-admin')).toBe(false);
49
+ });
50
+ (0, vitest_1.it)('cloud-admin satisfies ALL scopes', () => {
51
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['cloud-admin'], 'read')).toBe(true);
52
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['cloud-admin'], 'write')).toBe(true);
53
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['cloud-admin'], 'admin')).toBe(true);
54
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['cloud-admin'], 'cloud-admin')).toBe(true);
55
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['cloud-admin'], 'workflow-trigger')).toBe(true);
56
+ });
57
+ (0, vitest_1.it)('empty scopes satisfy nothing', () => {
58
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)([], 'read')).toBe(false);
59
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)([], 'write')).toBe(false);
60
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)([], 'admin')).toBe(false);
61
+ });
62
+ (0, vitest_1.it)('multiple scopes: [read, write] satisfies write', () => {
63
+ (0, vitest_1.expect)((0, scopeEnforcement_1.hasScope)(['read', 'write'], 'write')).toBe(true);
64
+ });
65
+ });
66
+ // ---------------------------------------------------------------------------
67
+ // requireScope
68
+ // ---------------------------------------------------------------------------
69
+ (0, vitest_1.describe)('requireScope', () => {
70
+ (0, vitest_1.it)('passes when scope is satisfied', () => {
71
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['write'], 'write', 'test_tool')).not.toThrow();
72
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['write'], 'read', 'test_tool')).not.toThrow();
73
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['admin'], 'write', 'test_tool')).not.toThrow();
74
+ });
75
+ (0, vitest_1.it)('throws ScopeError when scope is insufficient', () => {
76
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['read'], 'write', 'create_contact')).toThrow(scopeEnforcement_1.ScopeError);
77
+ });
78
+ (0, vitest_1.it)('ScopeError contains required scope and provided scopes', () => {
79
+ try {
80
+ (0, scopeEnforcement_1.requireScope)(['read'], 'admin', 'purge_records');
81
+ vitest_1.expect.fail('should have thrown');
82
+ }
83
+ catch (err) {
84
+ (0, vitest_1.expect)(err).toBeInstanceOf(scopeEnforcement_1.ScopeError);
85
+ const scopeErr = err;
86
+ (0, vitest_1.expect)(scopeErr.required).toBe('admin');
87
+ (0, vitest_1.expect)(scopeErr.provided).toEqual(['read']);
88
+ (0, vitest_1.expect)(scopeErr.message).toContain('purge_records');
89
+ }
90
+ });
91
+ (0, vitest_1.it)('read token → write tool = rejected', () => {
92
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['read'], 'write', 'create_deal')).toThrow(scopeEnforcement_1.ScopeError);
93
+ });
94
+ (0, vitest_1.it)('write token → admin tool = rejected', () => {
95
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['write'], 'admin', 'list_audit_log')).toThrow(scopeEnforcement_1.ScopeError);
96
+ });
97
+ (0, vitest_1.it)('admin token → all non-cloud-admin tools = pass', () => {
98
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['admin'], 'read', 'list_contacts')).not.toThrow();
99
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['admin'], 'write', 'create_contact')).not.toThrow();
100
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['admin'], 'admin', 'list_audit_log')).not.toThrow();
101
+ });
102
+ (0, vitest_1.it)('cloud-admin → all tools = pass', () => {
103
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['cloud-admin'], 'admin', 'purge_archived_records')).not.toThrow();
104
+ (0, vitest_1.expect)(() => (0, scopeEnforcement_1.requireScope)(['cloud-admin'], 'cloud-admin', 'any_tool')).not.toThrow();
105
+ });
106
+ });
107
+ // ---------------------------------------------------------------------------
108
+ // filterToolsByScope
109
+ // ---------------------------------------------------------------------------
110
+ (0, vitest_1.describe)('filterToolsByScope', () => {
111
+ const tools = [
112
+ { name: 'get_contact', description: '', requiredScope: 'read', inputSchema: {} },
113
+ { name: 'create_contact', description: '', requiredScope: 'write', inputSchema: {} },
114
+ { name: 'list_audit_log', description: '', requiredScope: 'admin', inputSchema: {} },
115
+ { name: 'cross_workspace_op', description: '', requiredScope: 'cloud-admin', inputSchema: {} },
116
+ ];
117
+ (0, vitest_1.it)('read token sees only read tools', () => {
118
+ const visible = (0, scopeEnforcement_1.filterToolsByScope)(tools, ['read']);
119
+ (0, vitest_1.expect)(visible.map((t) => t.name)).toEqual(['get_contact']);
120
+ });
121
+ (0, vitest_1.it)('write token sees read + write tools', () => {
122
+ const visible = (0, scopeEnforcement_1.filterToolsByScope)(tools, ['write']);
123
+ (0, vitest_1.expect)(visible.map((t) => t.name)).toEqual(['get_contact', 'create_contact']);
124
+ });
125
+ (0, vitest_1.it)('admin token sees read + write + admin tools', () => {
126
+ const visible = (0, scopeEnforcement_1.filterToolsByScope)(tools, ['admin']);
127
+ (0, vitest_1.expect)(visible.map((t) => t.name)).toEqual(['get_contact', 'create_contact', 'list_audit_log']);
128
+ });
129
+ (0, vitest_1.it)('cloud-admin sees all tools', () => {
130
+ const visible = (0, scopeEnforcement_1.filterToolsByScope)(tools, ['cloud-admin']);
131
+ (0, vitest_1.expect)(visible).toHaveLength(4);
132
+ });
133
+ (0, vitest_1.it)('empty scopes see nothing', () => {
134
+ const visible = (0, scopeEnforcement_1.filterToolsByScope)(tools, []);
135
+ (0, vitest_1.expect)(visible).toHaveLength(0);
136
+ });
137
+ });