@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,603 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace management functions
4
+ *
5
+ * Workspaces are the primary container for all user/team data:
6
+ * - Personal workspaces: owned by a user (1 default + unlimited additional)
7
+ * - Organization workspaces: owned by an org, shared by team members
8
+ *
9
+ * Each workspace has its own chats, flows, integrations, and settings.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getContext = exports.remove = exports.removeMember = exports.addMember = exports.update = exports.switchTo = exports.ensureDefaultWorkspace = exports.createForOrg = exports.createPersonal = exports.getMembers = exports.getCurrent = exports.getById = exports.list = void 0;
13
+ const values_1 = require("convex/values");
14
+ const server_1 = require("./_generated/server");
15
+ // ============================================================================
16
+ // QUERIES
17
+ // ============================================================================
18
+ /**
19
+ * Get all workspaces the current user has access to
20
+ */
21
+ exports.list = (0, server_1.query)({
22
+ args: {},
23
+ handler: async (ctx) => {
24
+ const identity = await ctx.auth.getUserIdentity();
25
+ if (!identity)
26
+ return [];
27
+ // Find user
28
+ const user = await ctx.db
29
+ .query('users')
30
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
31
+ .first();
32
+ if (!user)
33
+ return [];
34
+ // Get personal workspaces (owned by user)
35
+ const personalWorkspaces = await ctx.db
36
+ .query('workspaces')
37
+ .withIndex('by_owner', (q) => q.eq('ownerId', user._id))
38
+ .filter((q) => q.eq(q.field('ownerType'), 'user'))
39
+ .collect();
40
+ // Get org workspaces user is a member of
41
+ const workspaceMemberships = await ctx.db
42
+ .query('workspaceMembers')
43
+ .withIndex('by_user', (q) => q.eq('userId', user._id))
44
+ .collect();
45
+ const orgWorkspaces = await Promise.all(workspaceMemberships.map(async (membership) => {
46
+ const workspace = await ctx.db.get(membership.workspaceId);
47
+ if (!workspace)
48
+ return null;
49
+ return {
50
+ ...workspace,
51
+ memberRole: membership.role,
52
+ memberPermissions: membership.permissions,
53
+ };
54
+ }));
55
+ // Combine and sort
56
+ const allWorkspaces = [
57
+ ...personalWorkspaces.map((ws) => ({
58
+ ...ws,
59
+ memberRole: 'owner',
60
+ memberPermissions: null,
61
+ })),
62
+ ...orgWorkspaces.filter(Boolean),
63
+ ].sort((a, b) => {
64
+ // Default workspace first
65
+ if (a?.isDefault)
66
+ return -1;
67
+ if (b?.isDefault)
68
+ return 1;
69
+ // Then by last accessed
70
+ return (b?.lastAccessedAt || 0) - (a?.lastAccessedAt || 0);
71
+ });
72
+ return allWorkspaces;
73
+ },
74
+ });
75
+ /**
76
+ * Get a specific workspace by ID
77
+ */
78
+ exports.getById = (0, server_1.query)({
79
+ args: { workspaceId: values_1.v.id('workspaces') },
80
+ handler: async (ctx, { workspaceId }) => {
81
+ const identity = await ctx.auth.getUserIdentity();
82
+ if (!identity)
83
+ return null;
84
+ const workspace = await ctx.db.get(workspaceId);
85
+ if (!workspace)
86
+ return null;
87
+ // Check access
88
+ const user = await ctx.db
89
+ .query('users')
90
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
91
+ .first();
92
+ if (!user)
93
+ return null;
94
+ // Owner always has access
95
+ if (workspace.ownerId === user._id) {
96
+ return { ...workspace, memberRole: 'owner', canEdit: true };
97
+ }
98
+ // Check workspace membership
99
+ const membership = await ctx.db
100
+ .query('workspaceMembers')
101
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', user._id))
102
+ .first();
103
+ if (membership) {
104
+ const canEdit = ['owner', 'admin', 'editor'].includes(membership.role);
105
+ return { ...workspace, memberRole: membership.role, canEdit };
106
+ }
107
+ return null; // No access
108
+ },
109
+ });
110
+ /**
111
+ * Get the current/active workspace for the user
112
+ */
113
+ exports.getCurrent = (0, server_1.query)({
114
+ args: {},
115
+ handler: async (ctx) => {
116
+ const identity = await ctx.auth.getUserIdentity();
117
+ if (!identity)
118
+ return null;
119
+ const user = await ctx.db
120
+ .query('users')
121
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
122
+ .first();
123
+ if (!user)
124
+ return null;
125
+ // If user has an active workspace, return it
126
+ if (user.activeWorkspaceId) {
127
+ const workspace = await ctx.db.get(user.activeWorkspaceId);
128
+ if (workspace)
129
+ return workspace;
130
+ }
131
+ // Otherwise, return their default personal workspace
132
+ const defaultWorkspace = await ctx.db
133
+ .query('workspaces')
134
+ .withIndex('by_owner', (q) => q.eq('ownerId', user._id))
135
+ .filter((q) => q.and(q.eq(q.field('ownerType'), 'user'), q.eq(q.field('isDefault'), true)))
136
+ .first();
137
+ return defaultWorkspace;
138
+ },
139
+ });
140
+ /**
141
+ * Get workspace members
142
+ */
143
+ exports.getMembers = (0, server_1.query)({
144
+ args: { workspaceId: values_1.v.id('workspaces') },
145
+ handler: async (ctx, { workspaceId }) => {
146
+ const memberships = await ctx.db
147
+ .query('workspaceMembers')
148
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', workspaceId))
149
+ .collect();
150
+ const members = await Promise.all(memberships.map(async (membership) => {
151
+ const user = await ctx.db.get(membership.userId);
152
+ return {
153
+ ...membership,
154
+ user: user
155
+ ? {
156
+ _id: user._id,
157
+ name: user.name,
158
+ email: user.email,
159
+ avatarUrl: user.avatarUrl,
160
+ }
161
+ : null,
162
+ };
163
+ }));
164
+ return members;
165
+ },
166
+ });
167
+ // ============================================================================
168
+ // MUTATIONS
169
+ // ============================================================================
170
+ /**
171
+ * Create a new personal workspace
172
+ */
173
+ exports.createPersonal = (0, server_1.mutation)({
174
+ args: {
175
+ name: values_1.v.string(),
176
+ slug: values_1.v.optional(values_1.v.string()),
177
+ description: values_1.v.optional(values_1.v.string()),
178
+ icon: values_1.v.optional(values_1.v.string()),
179
+ color: values_1.v.optional(values_1.v.string()),
180
+ },
181
+ handler: async (ctx, args) => {
182
+ const identity = await ctx.auth.getUserIdentity();
183
+ if (!identity)
184
+ throw new Error('Not authenticated');
185
+ const user = await ctx.db
186
+ .query('users')
187
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
188
+ .first();
189
+ if (!user)
190
+ throw new Error('User not found');
191
+ // Check if this is the first workspace (make it default)
192
+ const existingWorkspaces = await ctx.db
193
+ .query('workspaces')
194
+ .withIndex('by_owner', (q) => q.eq('ownerId', user._id))
195
+ .filter((q) => q.eq(q.field('ownerType'), 'user'))
196
+ .collect();
197
+ const isDefault = existingWorkspaces.length === 0;
198
+ const workspaceId = await ctx.db.insert('workspaces', {
199
+ name: args.name,
200
+ slug: args.slug,
201
+ description: args.description,
202
+ icon: args.icon,
203
+ color: args.color,
204
+ ownerType: 'user',
205
+ ownerId: user._id,
206
+ isDefault,
207
+ createdAt: Date.now(),
208
+ updatedAt: Date.now(),
209
+ });
210
+ // Set as active if it's the first one
211
+ if (isDefault) {
212
+ await ctx.db.patch(user._id, { activeWorkspaceId: workspaceId });
213
+ }
214
+ return workspaceId;
215
+ },
216
+ });
217
+ /**
218
+ * Create a new organization workspace
219
+ */
220
+ exports.createForOrg = (0, server_1.mutation)({
221
+ args: {
222
+ orgId: values_1.v.id('organizations'),
223
+ name: values_1.v.string(),
224
+ slug: values_1.v.optional(values_1.v.string()),
225
+ description: values_1.v.optional(values_1.v.string()),
226
+ icon: values_1.v.optional(values_1.v.string()),
227
+ color: values_1.v.optional(values_1.v.string()),
228
+ },
229
+ handler: async (ctx, args) => {
230
+ const identity = await ctx.auth.getUserIdentity();
231
+ if (!identity)
232
+ throw new Error('Not authenticated');
233
+ const user = await ctx.db
234
+ .query('users')
235
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
236
+ .first();
237
+ if (!user)
238
+ throw new Error('User not found');
239
+ // Check user is admin of the org
240
+ const membership = await ctx.db
241
+ .query('memberships')
242
+ .withIndex('by_user_org', (q) => q.eq('userId', user._id).eq('orgId', args.orgId))
243
+ .first();
244
+ const canCreate = membership?.role === 'org:admin' ||
245
+ membership?.permissions?.canCreateWorkspaces;
246
+ if (!canCreate) {
247
+ throw new Error('You need admin access to create organization workspaces');
248
+ }
249
+ const workspaceId = await ctx.db.insert('workspaces', {
250
+ name: args.name,
251
+ slug: args.slug,
252
+ description: args.description,
253
+ icon: args.icon,
254
+ color: args.color,
255
+ ownerType: 'organization',
256
+ ownerId: user._id, // Creator
257
+ orgId: args.orgId,
258
+ createdAt: Date.now(),
259
+ updatedAt: Date.now(),
260
+ });
261
+ // Add creator as workspace owner
262
+ await ctx.db.insert('workspaceMembers', {
263
+ workspaceId,
264
+ userId: user._id,
265
+ role: 'owner',
266
+ addedAt: Date.now(),
267
+ });
268
+ // Update org workspace count
269
+ const org = await ctx.db.get(args.orgId);
270
+ if (org) {
271
+ await ctx.db.patch(args.orgId, {
272
+ workspaceCount: (org.workspaceCount || 0) + 1,
273
+ updatedAt: Date.now(),
274
+ });
275
+ }
276
+ return workspaceId;
277
+ },
278
+ });
279
+ /**
280
+ * Ensure user has a default personal workspace
281
+ * Called during user sync
282
+ */
283
+ exports.ensureDefaultWorkspace = (0, server_1.mutation)({
284
+ args: { userId: values_1.v.id('users') },
285
+ handler: async (ctx, { userId }) => {
286
+ const user = await ctx.db.get(userId);
287
+ if (!user)
288
+ throw new Error('User not found');
289
+ // Check if default workspace exists
290
+ const defaultWorkspace = await ctx.db
291
+ .query('workspaces')
292
+ .withIndex('by_owner', (q) => q.eq('ownerId', userId))
293
+ .filter((q) => q.and(q.eq(q.field('ownerType'), 'user'), q.eq(q.field('isDefault'), true)))
294
+ .first();
295
+ if (defaultWorkspace) {
296
+ return defaultWorkspace._id;
297
+ }
298
+ // Create default personal workspace
299
+ const workspaceId = await ctx.db.insert('workspaces', {
300
+ name: 'Personal',
301
+ ownerType: 'user',
302
+ ownerId: userId,
303
+ isDefault: true,
304
+ createdAt: Date.now(),
305
+ updatedAt: Date.now(),
306
+ });
307
+ // Set as active workspace
308
+ await ctx.db.patch(userId, { activeWorkspaceId: workspaceId });
309
+ return workspaceId;
310
+ },
311
+ });
312
+ /**
313
+ * Switch to a different workspace
314
+ */
315
+ exports.switchTo = (0, server_1.mutation)({
316
+ args: { workspaceId: values_1.v.id('workspaces') },
317
+ handler: async (ctx, { workspaceId }) => {
318
+ const identity = await ctx.auth.getUserIdentity();
319
+ if (!identity)
320
+ throw new Error('Not authenticated');
321
+ const user = await ctx.db
322
+ .query('users')
323
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
324
+ .first();
325
+ if (!user)
326
+ throw new Error('User not found');
327
+ // Verify access to workspace
328
+ const workspace = await ctx.db.get(workspaceId);
329
+ if (!workspace)
330
+ throw new Error('Workspace not found');
331
+ // Check ownership or membership
332
+ const hasAccess = workspace.ownerId === user._id ||
333
+ (await ctx.db
334
+ .query('workspaceMembers')
335
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', user._id))
336
+ .first());
337
+ if (!hasAccess)
338
+ throw new Error('No access to this workspace');
339
+ // Update user's active workspace
340
+ await ctx.db.patch(user._id, { activeWorkspaceId: workspaceId });
341
+ // Update workspace last accessed
342
+ await ctx.db.patch(workspaceId, { lastAccessedAt: Date.now() });
343
+ return { success: true };
344
+ },
345
+ });
346
+ /**
347
+ * Update workspace details
348
+ */
349
+ exports.update = (0, server_1.mutation)({
350
+ args: {
351
+ workspaceId: values_1.v.id('workspaces'),
352
+ name: values_1.v.optional(values_1.v.string()),
353
+ slug: values_1.v.optional(values_1.v.string()),
354
+ description: values_1.v.optional(values_1.v.string()),
355
+ icon: values_1.v.optional(values_1.v.string()),
356
+ color: values_1.v.optional(values_1.v.string()),
357
+ settings: values_1.v.optional(values_1.v.object({
358
+ defaultModel: values_1.v.optional(values_1.v.string()),
359
+ theme: values_1.v.optional(values_1.v.string()),
360
+ enabledTools: values_1.v.optional(values_1.v.array(values_1.v.string())),
361
+ })),
362
+ },
363
+ handler: async (ctx, { workspaceId, ...updates }) => {
364
+ const identity = await ctx.auth.getUserIdentity();
365
+ if (!identity)
366
+ throw new Error('Not authenticated');
367
+ const user = await ctx.db
368
+ .query('users')
369
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
370
+ .first();
371
+ if (!user)
372
+ throw new Error('User not found');
373
+ const workspace = await ctx.db.get(workspaceId);
374
+ if (!workspace)
375
+ throw new Error('Workspace not found');
376
+ // Check edit permission
377
+ let canEdit = workspace.ownerId === user._id;
378
+ if (!canEdit) {
379
+ const membership = await ctx.db
380
+ .query('workspaceMembers')
381
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', user._id))
382
+ .first();
383
+ canEdit = ['owner', 'admin'].includes(membership?.role || '');
384
+ }
385
+ if (!canEdit)
386
+ throw new Error('No permission to edit this workspace');
387
+ // Build update object
388
+ const patchData = { updatedAt: Date.now() };
389
+ if (updates.name !== undefined)
390
+ patchData.name = updates.name;
391
+ if (updates.slug !== undefined)
392
+ patchData.slug = updates.slug;
393
+ if (updates.description !== undefined)
394
+ patchData.description = updates.description;
395
+ if (updates.icon !== undefined)
396
+ patchData.icon = updates.icon;
397
+ if (updates.color !== undefined)
398
+ patchData.color = updates.color;
399
+ if (updates.settings !== undefined) {
400
+ patchData.settings = { ...workspace.settings, ...updates.settings };
401
+ }
402
+ await ctx.db.patch(workspaceId, patchData);
403
+ return { success: true };
404
+ },
405
+ });
406
+ /**
407
+ * Add a member to an org workspace
408
+ */
409
+ exports.addMember = (0, server_1.mutation)({
410
+ args: {
411
+ workspaceId: values_1.v.id('workspaces'),
412
+ userId: values_1.v.id('users'),
413
+ role: values_1.v.union(values_1.v.literal('admin'), values_1.v.literal('editor'), values_1.v.literal('viewer')),
414
+ },
415
+ handler: async (ctx, { workspaceId, userId, role }) => {
416
+ const identity = await ctx.auth.getUserIdentity();
417
+ if (!identity)
418
+ throw new Error('Not authenticated');
419
+ const currentUser = await ctx.db
420
+ .query('users')
421
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
422
+ .first();
423
+ if (!currentUser)
424
+ throw new Error('User not found');
425
+ const workspace = await ctx.db.get(workspaceId);
426
+ if (!workspace)
427
+ throw new Error('Workspace not found');
428
+ // Only org workspaces can have members added
429
+ if (workspace.ownerType !== 'organization') {
430
+ throw new Error('Can only add members to organization workspaces');
431
+ }
432
+ // Check permission
433
+ let canInvite = workspace.ownerId === currentUser._id;
434
+ if (!canInvite) {
435
+ const membership = await ctx.db
436
+ .query('workspaceMembers')
437
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', currentUser._id))
438
+ .first();
439
+ canInvite =
440
+ ['owner', 'admin'].includes(membership?.role || '') ||
441
+ membership?.permissions?.canInviteMembers === true;
442
+ }
443
+ if (!canInvite)
444
+ throw new Error('No permission to add members');
445
+ // Check if already a member
446
+ const existingMembership = await ctx.db
447
+ .query('workspaceMembers')
448
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', userId))
449
+ .first();
450
+ if (existingMembership) {
451
+ throw new Error('User is already a member of this workspace');
452
+ }
453
+ // Add member
454
+ await ctx.db.insert('workspaceMembers', {
455
+ workspaceId,
456
+ userId,
457
+ role,
458
+ addedAt: Date.now(),
459
+ addedBy: currentUser._id,
460
+ });
461
+ // Update member count
462
+ await ctx.db.patch(workspaceId, {
463
+ memberCount: (workspace.memberCount || 0) + 1,
464
+ updatedAt: Date.now(),
465
+ });
466
+ return { success: true };
467
+ },
468
+ });
469
+ /**
470
+ * Remove a member from workspace
471
+ */
472
+ exports.removeMember = (0, server_1.mutation)({
473
+ args: {
474
+ workspaceId: values_1.v.id('workspaces'),
475
+ userId: values_1.v.id('users'),
476
+ },
477
+ handler: async (ctx, { workspaceId, userId }) => {
478
+ const identity = await ctx.auth.getUserIdentity();
479
+ if (!identity)
480
+ throw new Error('Not authenticated');
481
+ const currentUser = await ctx.db
482
+ .query('users')
483
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
484
+ .first();
485
+ if (!currentUser)
486
+ throw new Error('User not found');
487
+ const workspace = await ctx.db.get(workspaceId);
488
+ if (!workspace)
489
+ throw new Error('Workspace not found');
490
+ // Check permission (owner/admin can remove, or self-remove)
491
+ const isSelfRemove = userId === currentUser._id;
492
+ let canRemove = isSelfRemove || workspace.ownerId === currentUser._id;
493
+ if (!canRemove) {
494
+ const membership = await ctx.db
495
+ .query('workspaceMembers')
496
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', currentUser._id))
497
+ .first();
498
+ canRemove = ['owner', 'admin'].includes(membership?.role || '');
499
+ }
500
+ if (!canRemove)
501
+ throw new Error('No permission to remove members');
502
+ // Find and delete membership
503
+ const membershipToRemove = await ctx.db
504
+ .query('workspaceMembers')
505
+ .withIndex('by_workspace_user', (q) => q.eq('workspaceId', workspaceId).eq('userId', userId))
506
+ .first();
507
+ if (!membershipToRemove) {
508
+ throw new Error('User is not a member of this workspace');
509
+ }
510
+ // Can't remove the last owner
511
+ if (membershipToRemove.role === 'owner') {
512
+ const ownerCount = await ctx.db
513
+ .query('workspaceMembers')
514
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', workspaceId))
515
+ .filter((q) => q.eq(q.field('role'), 'owner'))
516
+ .collect();
517
+ if (ownerCount.length <= 1) {
518
+ throw new Error('Cannot remove the last owner of a workspace');
519
+ }
520
+ }
521
+ await ctx.db.delete(membershipToRemove._id);
522
+ // Update member count
523
+ await ctx.db.patch(workspaceId, {
524
+ memberCount: Math.max((workspace.memberCount || 1) - 1, 0),
525
+ updatedAt: Date.now(),
526
+ });
527
+ return { success: true };
528
+ },
529
+ });
530
+ /**
531
+ * Delete a workspace
532
+ */
533
+ exports.remove = (0, server_1.mutation)({
534
+ args: { workspaceId: values_1.v.id('workspaces') },
535
+ handler: async (ctx, { workspaceId }) => {
536
+ const identity = await ctx.auth.getUserIdentity();
537
+ if (!identity)
538
+ throw new Error('Not authenticated');
539
+ const user = await ctx.db
540
+ .query('users')
541
+ .withIndex('by_clerk_id', (q) => q.eq('clerkId', identity.subject))
542
+ .first();
543
+ if (!user)
544
+ throw new Error('User not found');
545
+ const workspace = await ctx.db.get(workspaceId);
546
+ if (!workspace)
547
+ throw new Error('Workspace not found');
548
+ // Can't delete default workspace
549
+ if (workspace.isDefault) {
550
+ throw new Error('Cannot delete your default personal workspace');
551
+ }
552
+ // Only owner can delete
553
+ if (workspace.ownerId !== user._id) {
554
+ throw new Error('Only the workspace owner can delete it');
555
+ }
556
+ // Delete all workspace members
557
+ const members = await ctx.db
558
+ .query('workspaceMembers')
559
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', workspaceId))
560
+ .collect();
561
+ for (const member of members) {
562
+ await ctx.db.delete(member._id);
563
+ }
564
+ // Note: In production, you'd also want to handle:
565
+ // - Moving/archiving chats
566
+ // - Moving/archiving flows
567
+ // - Disconnecting integrations
568
+ // For now, we'll just delete the workspace
569
+ await ctx.db.delete(workspaceId);
570
+ // If this was user's active workspace, switch to default
571
+ if (user.activeWorkspaceId === workspaceId) {
572
+ const defaultWorkspace = await ctx.db
573
+ .query('workspaces')
574
+ .withIndex('by_owner', (q) => q.eq('ownerId', user._id))
575
+ .filter((q) => q.eq(q.field('isDefault'), true))
576
+ .first();
577
+ if (defaultWorkspace) {
578
+ await ctx.db.patch(user._id, {
579
+ activeWorkspaceId: defaultWorkspace._id,
580
+ });
581
+ }
582
+ }
583
+ return { success: true };
584
+ },
585
+ });
586
+ /**
587
+ * Get workspace context for V0.1.0 CRM
588
+ * Returns workspace info. Missions removed in V0.1.0 schema migration.
589
+ */
590
+ exports.getContext = (0, server_1.query)({
591
+ args: { workspaceId: values_1.v.id('workspaces') },
592
+ handler: async (ctx, args) => {
593
+ const identity = await ctx.auth.getUserIdentity();
594
+ if (!identity)
595
+ return null;
596
+ const workspace = await ctx.db.get(args.workspaceId);
597
+ if (!workspace)
598
+ return null;
599
+ return {
600
+ workspaceName: workspace.name,
601
+ };
602
+ },
603
+ });
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP Server — Convex HTTP Client wrapper
4
+ *
5
+ * Provides a typed wrapper around ConvexHttpClient for use in MCP tool handlers.
6
+ * The MCP server is a standalone Node process — it calls Convex over HTTP.
7
+ *
8
+ * Environment variables:
9
+ * CONVEX_URL — required, e.g. https://your-deployment.convex.cloud
10
+ * CONVEX_AUTH_TOKEN — optional, used for system-level Convex auth in HTTP mode
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.getConvexClient = getConvexClient;
14
+ exports.ok = ok;
15
+ exports.err = err;
16
+ exports.withEnvelope = withEnvelope;
17
+ const browser_1 = require("convex/browser");
18
+ let _client = null;
19
+ function getConvexClient() {
20
+ if (_client)
21
+ return _client;
22
+ const url = process.env.CONVEX_URL;
23
+ if (!url) {
24
+ throw new Error('CONVEX_URL environment variable is required');
25
+ }
26
+ _client = new browser_1.ConvexHttpClient(url);
27
+ return _client;
28
+ }
29
+ function ok(data) {
30
+ return { success: true, data };
31
+ }
32
+ function err(code, message) {
33
+ return { success: false, error: { code, message } };
34
+ }
35
+ /**
36
+ * Wrap a tool handler with standard try/catch envelope.
37
+ * Returns ToolResult in all cases — never throws.
38
+ */
39
+ async function withEnvelope(fn) {
40
+ try {
41
+ const data = await fn();
42
+ return ok(data);
43
+ }
44
+ catch (e) {
45
+ const message = e instanceof Error ? e.message : String(e);
46
+ const code = e instanceof Error && e.name !== 'Error' ? e.name : 'CONVEX_ERROR';
47
+ // Cast: error path returns ToolResult with no data, compatible with ToolResult<T>
48
+ return { success: false, error: { code, message } };
49
+ }
50
+ }