@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.
- package/README.md +260 -0
- package/dist/convex/crm/_helpers.js +24 -0
- package/dist/convex/crm/activities.js +220 -0
- package/dist/convex/crm/briefing.js +198 -0
- package/dist/convex/crm/calendarCron.js +92 -0
- package/dist/convex/crm/calendarCronDispatch.js +83 -0
- package/dist/convex/crm/calendarSync.js +294 -0
- package/dist/convex/crm/companies.js +323 -0
- package/dist/convex/crm/contacts.js +346 -0
- package/dist/convex/crm/deals.js +481 -0
- package/dist/convex/crm/emailActions.js +158 -0
- package/dist/convex/crm/emailCron.js +210 -0
- package/dist/convex/crm/emailCronDispatch.js +76 -0
- package/dist/convex/crm/emailSync.js +260 -0
- package/dist/convex/crm/onboarding.js +185 -0
- package/dist/convex/crm/stats.js +75 -0
- package/dist/convex/crm/tasks.js +109 -0
- package/dist/convex/crons.js +25 -0
- package/dist/convex/integrations.js +183 -0
- package/dist/convex/lib/auditLog.js +109 -0
- package/dist/convex/lib/auth.js +372 -0
- package/dist/convex/lib/rbac.js +123 -0
- package/dist/convex/lib/workspace.js +171 -0
- package/dist/convex/organizations.js +192 -0
- package/dist/convex/schema.js +690 -0
- package/dist/convex/users.js +217 -0
- package/dist/convex/workspaces.js +603 -0
- package/dist/mcp-server/lib/convexClient.js +50 -0
- package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
- package/dist/mcp-server/registry.js +116 -0
- package/dist/mcp-server/server.js +97 -0
- package/dist/mcp-server/tests/registry.test.js +163 -0
- package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
- package/dist/mcp-server/tests/security.test.js +257 -0
- package/dist/mcp-server/tests/tools.test.js +272 -0
- package/dist/mcp-server/tools/activities.js +207 -0
- package/dist/mcp-server/tools/admin.js +190 -0
- package/dist/mcp-server/tools/companies.js +233 -0
- package/dist/mcp-server/tools/contacts.js +306 -0
- package/dist/mcp-server/tools/customFields.js +222 -0
- package/dist/mcp-server/tools/customObjects.js +235 -0
- package/dist/mcp-server/tools/deals.js +297 -0
- package/dist/mcp-server/tools/rbac.js +177 -0
- package/dist/mcp-server/tools/search.js +155 -0
- package/dist/mcp-server/tools/workflows.js +234 -0
- package/dist/mcp-server/transport/http.js +257 -0
- package/dist/mcp-server/transport/stdio.js +90 -0
- 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
|
+
}
|