@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,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
|
+
}
|