@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,257 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP — HTTP Transport (Cloud Railway)
4
+ *
5
+ * OAuth Bearer token authentication via validateAccessToken (convex/lib/oauth.ts).
6
+ * Scopes extracted from token → filter visible tools + enforce per-call.
7
+ *
8
+ * Endpoints:
9
+ * GET /mcp/tools — list tools visible to this token's scopes
10
+ * POST /mcp/call — call a tool (scope enforced)
11
+ * GET /health — liveness probe
12
+ *
13
+ * Environment variables:
14
+ * PORT — HTTP port (default 3001)
15
+ * CONVEX_URL — Convex deployment URL
16
+ * VANTAGE_MCP_MODE — set to 'http' to enable this transport
17
+ *
18
+ * Auth header: Authorization: Bearer <access_token>
19
+ * Token validation is proxied to Convex via the validateToken action.
20
+ *
21
+ * Ref: spec §3.1, convex/lib/oauth.ts
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
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.startHttpServer = startHttpServer;
58
+ const http = __importStar(require("http"));
59
+ const browser_1 = require("convex/browser");
60
+ const registry_1 = require("../registry");
61
+ const scopeEnforcement_1 = require("../lib/scopeEnforcement");
62
+ const server_1 = require("convex/server");
63
+ // ---------------------------------------------------------------------------
64
+ // Multi-tenant workspace isolation helper (BL-1)
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Cross-check that the token's bound workspaceId matches any workspaceId
68
+ * supplied in the tool args. Prevents IDOR cross-tenant access where a
69
+ * caller holds a valid token for workspace A but passes workspace B's ID.
70
+ *
71
+ * Must be called before every tool dispatch in HTTP transport.
72
+ */
73
+ function requireWorkspaceMatchesToken(tokenContext, args) {
74
+ const tokenWorkspaceId = tokenContext.workspaceId ?? tokenContext.orgId;
75
+ const argsWorkspaceId = args['workspaceId'];
76
+ if (argsWorkspaceId !== undefined &&
77
+ argsWorkspaceId !== null &&
78
+ tokenWorkspaceId !== null &&
79
+ argsWorkspaceId !== tokenWorkspaceId) {
80
+ throw new WorkspaceMismatchError(`Token workspace (${tokenWorkspaceId}) does not match args.workspaceId (${String(argsWorkspaceId)}). Cross-tenant access denied.`);
81
+ }
82
+ }
83
+ class WorkspaceMismatchError extends Error {
84
+ code = 'FORBIDDEN_WORKSPACE_MISMATCH';
85
+ constructor(message) {
86
+ super(message);
87
+ this.name = 'WorkspaceMismatchError';
88
+ }
89
+ }
90
+ async function validateBearerToken(authHeader) {
91
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
92
+ throw new Error('Missing or malformed Authorization header. Expected: Bearer <token>');
93
+ }
94
+ const rawToken = authHeader.slice('Bearer '.length).trim();
95
+ if (!rawToken) {
96
+ throw new Error('Empty Bearer token');
97
+ }
98
+ const convexUrl = process.env.CONVEX_URL;
99
+ if (!convexUrl) {
100
+ throw new Error('CONVEX_URL not configured');
101
+ }
102
+ const client = new browser_1.ConvexHttpClient(convexUrl);
103
+ // Use the Convex validateToken query that wraps oauth.ts validateAccessToken.
104
+ // anyApi is used since oauthActions.ts is new and not in generated API yet.
105
+ const result = await client.query(server_1.anyApi.lib.oauthActions.validateToken, { rawToken }).catch(() => null);
106
+ if (!result) {
107
+ throw new Error('Invalid, expired, or revoked access token');
108
+ }
109
+ return result;
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Request helpers
113
+ // ---------------------------------------------------------------------------
114
+ function readBody(req) {
115
+ return new Promise((resolve, reject) => {
116
+ let body = '';
117
+ req.on('data', (chunk) => { body += chunk.toString(); });
118
+ req.on('end', () => resolve(body));
119
+ req.on('error', reject);
120
+ });
121
+ }
122
+ function sendJson(res, status, data) {
123
+ const body = JSON.stringify(data);
124
+ res.writeHead(status, {
125
+ 'Content-Type': 'application/json',
126
+ 'Content-Length': Buffer.byteLength(body),
127
+ });
128
+ res.end(body);
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // HTTP server
132
+ // ---------------------------------------------------------------------------
133
+ async function startHttpServer() {
134
+ const port = parseInt(process.env.PORT ?? '3001', 10);
135
+ const server = http.createServer(async (req, res) => {
136
+ // CORS headers
137
+ res.setHeader('Access-Control-Allow-Origin', '*');
138
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
139
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
140
+ if (req.method === 'OPTIONS') {
141
+ res.writeHead(204);
142
+ res.end();
143
+ return;
144
+ }
145
+ // Health probe — no auth required
146
+ if (req.url === '/health' && req.method === 'GET') {
147
+ sendJson(res, 200, { status: 'ok', tools: registry_1.ALL_TOOL_DEFINITIONS.length, version: '0.1.0' });
148
+ return;
149
+ }
150
+ // All other endpoints require auth
151
+ let token;
152
+ try {
153
+ token = await validateBearerToken(req.headers.authorization);
154
+ }
155
+ catch (err) {
156
+ sendJson(res, 401, {
157
+ error: 'UNAUTHORIZED',
158
+ message: err instanceof Error ? err.message : 'Authentication failed',
159
+ });
160
+ return;
161
+ }
162
+ // GET /mcp/tools — list tools visible for this token's scopes
163
+ if (req.url === '/mcp/tools' && req.method === 'GET') {
164
+ const visible = (0, scopeEnforcement_1.filterToolsByScope)(registry_1.ALL_TOOL_DEFINITIONS, token.scopes);
165
+ sendJson(res, 200, {
166
+ tools: visible.map((def) => ({
167
+ name: def.name,
168
+ description: def.description,
169
+ requiredScope: def.requiredScope,
170
+ inputSchema: def.inputSchema,
171
+ })),
172
+ totalVisible: visible.length,
173
+ totalAvailable: registry_1.ALL_TOOL_DEFINITIONS.length,
174
+ scopes: token.scopes,
175
+ });
176
+ return;
177
+ }
178
+ // POST /mcp/call — call a tool
179
+ if (req.url === '/mcp/call' && req.method === 'POST') {
180
+ let body;
181
+ try {
182
+ body = JSON.parse(await readBody(req));
183
+ }
184
+ catch {
185
+ sendJson(res, 400, { error: 'INVALID_JSON', message: 'Request body must be valid JSON' });
186
+ return;
187
+ }
188
+ const toolName = body.tool;
189
+ if (!toolName || typeof toolName !== 'string') {
190
+ sendJson(res, 400, { error: 'MISSING_TOOL', message: 'Request body must include "tool" field' });
191
+ return;
192
+ }
193
+ const toolDef = registry_1.ALL_TOOL_DEFINITIONS.find((d) => d.name === toolName);
194
+ if (!toolDef) {
195
+ sendJson(res, 404, { error: 'TOOL_NOT_FOUND', message: `Unknown tool: ${toolName}` });
196
+ return;
197
+ }
198
+ // Scope enforcement
199
+ try {
200
+ (0, scopeEnforcement_1.requireScope)(token.scopes, toolDef.requiredScope, toolName);
201
+ }
202
+ catch (err) {
203
+ if (err instanceof scopeEnforcement_1.ScopeError) {
204
+ sendJson(res, 403, {
205
+ error: 'INSUFFICIENT_SCOPE',
206
+ message: err.message,
207
+ required: err.required,
208
+ provided: err.provided,
209
+ });
210
+ return;
211
+ }
212
+ throw err;
213
+ }
214
+ const handler = registry_1.TOOL_HANDLERS[toolName];
215
+ if (!handler) {
216
+ sendJson(res, 500, { error: 'HANDLER_MISSING', message: `No handler for tool: ${toolName}` });
217
+ return;
218
+ }
219
+ // BL-1: Multi-tenant workspace isolation — cross-check token vs args before dispatch
220
+ const rawArgs = (body.arguments ?? {});
221
+ try {
222
+ requireWorkspaceMatchesToken(token, rawArgs);
223
+ }
224
+ catch (err) {
225
+ if (err instanceof WorkspaceMismatchError) {
226
+ sendJson(res, 403, {
227
+ success: false,
228
+ error: {
229
+ code: err.code,
230
+ message: err.message,
231
+ },
232
+ });
233
+ return;
234
+ }
235
+ throw err;
236
+ }
237
+ try {
238
+ const result = await handler(rawArgs);
239
+ sendJson(res, 200, result);
240
+ }
241
+ catch (err) {
242
+ const message = err instanceof Error ? err.message : String(err);
243
+ sendJson(res, 500, { error: 'HANDLER_ERROR', message });
244
+ }
245
+ return;
246
+ }
247
+ // 404 for unknown routes
248
+ sendJson(res, 404, { error: 'NOT_FOUND', message: `${req.method} ${req.url} not found` });
249
+ });
250
+ await new Promise((resolve, reject) => {
251
+ server.on('error', reject);
252
+ server.listen(port, () => {
253
+ process.stderr.write(`[VantageCRM MCP] HTTP server started on port ${port} — 60 tools, OAuth Bearer\n`);
254
+ resolve();
255
+ });
256
+ });
257
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP — Stdio Transport (self-host CLI)
4
+ *
5
+ * Used when mode=stdio (default for local/self-hosted deployments).
6
+ * Full local access — no OAuth token, no scope filtering.
7
+ * Trust model: user runs the CLI on their own machine/server.
8
+ *
9
+ * Usage: node mcp-server/server.js --mode stdio
10
+ * or: VANTAGE_MCP_MODE=stdio node mcp-server/server.js
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.startStdioServer = startStdioServer;
14
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
15
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
16
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
17
+ const registry_1 = require("../registry");
18
+ async function startStdioServer() {
19
+ const server = new index_js_1.Server({
20
+ name: 'vantageos-crm',
21
+ version: '0.1.0',
22
+ }, {
23
+ capabilities: {
24
+ tools: {},
25
+ },
26
+ });
27
+ // ---------------------------------------------------------------------------
28
+ // ListTools — return all 60 tools (stdio = full access, no scope filter)
29
+ // ---------------------------------------------------------------------------
30
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
31
+ return {
32
+ tools: registry_1.ALL_TOOL_DEFINITIONS.map((def) => ({
33
+ name: def.name,
34
+ description: def.description,
35
+ inputSchema: def.inputSchema,
36
+ })),
37
+ };
38
+ });
39
+ // ---------------------------------------------------------------------------
40
+ // CallTool — dispatch to handler, wrap in envelope
41
+ // ---------------------------------------------------------------------------
42
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
43
+ const { name, arguments: args } = request.params;
44
+ const handler = registry_1.TOOL_HANDLERS[name];
45
+ if (!handler) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: JSON.stringify({
51
+ success: false,
52
+ error: { code: 'TOOL_NOT_FOUND', message: `Unknown tool: ${name}` },
53
+ }),
54
+ },
55
+ ],
56
+ isError: true,
57
+ };
58
+ }
59
+ try {
60
+ const result = await handler(args ?? {});
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: JSON.stringify(result),
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ catch (err) {
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: JSON.stringify({
77
+ success: false,
78
+ error: { code: 'HANDLER_ERROR', message },
79
+ }),
80
+ },
81
+ ],
82
+ isError: true,
83
+ };
84
+ }
85
+ });
86
+ const transport = new stdio_js_1.StdioServerTransport();
87
+ await server.connect(transport);
88
+ // Signal ready on stderr (not stdout — stdout is used for MCP protocol)
89
+ process.stderr.write('[VantageCRM MCP] stdio server started — 60 tools available\n');
90
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@vantageos/vantage-crm-mcp",
3
+ "version": "0.1.0",
4
+ "description": "VantageCRM MCP Server — agents-first CRM over Model Context Protocol",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org"
9
+ },
10
+ "main": "dist/server.js",
11
+ "bin": {
12
+ "vantage-crm-mcp": "dist/server.js"
13
+ },
14
+ "types": "dist/server.d.ts",
15
+ "files": [
16
+ "dist/",
17
+ "README.md",
18
+ "../../LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "clean": "rm -rf dist",
23
+ "prepublishOnly": "npm run clean && npm run build"
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "crm",
31
+ "model-context-protocol",
32
+ "vantageos",
33
+ "agents"
34
+ ],
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.29.0",
37
+ "convex": "^1.29.3",
38
+ "zod": "^4.3.5"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/elpiarthera/vantageos-crm.git"
43
+ },
44
+ "homepage": "https://github.com/elpiarthera/vantageos-crm/tree/main/mcp-server#readme"
45
+ }