@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,306 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP — Contacts tools (8 tools)
4
+ *
5
+ * Tools: create_contact, get_contact, update_contact, list_contacts,
6
+ * search_contacts, delete_contact, list_contacts_by_custom_field, restore_contact
7
+ *
8
+ * Scope: read for queries, write for mutations
9
+ * Ref: spec §3.2
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CONTACT_TOOLS = void 0;
13
+ exports.create_contact = create_contact;
14
+ exports.get_contact = get_contact;
15
+ exports.update_contact = update_contact;
16
+ exports.list_contacts = list_contacts;
17
+ exports.search_contacts = search_contacts;
18
+ exports.delete_contact = delete_contact;
19
+ exports.list_contacts_by_custom_field = list_contacts_by_custom_field;
20
+ exports.restore_contact = restore_contact;
21
+ const zod_1 = require("zod");
22
+ const api_1 = require("../../convex/_generated/api");
23
+ const convexClient_1 = require("../lib/convexClient");
24
+ // ---------------------------------------------------------------------------
25
+ // Zod schemas
26
+ // ---------------------------------------------------------------------------
27
+ const CreateContactSchema = zod_1.z.object({
28
+ workspaceId: zod_1.z.string().min(1),
29
+ firstName: zod_1.z.string().min(1).max(100),
30
+ lastName: zod_1.z.string().min(1).max(100),
31
+ email: zod_1.z.string().email().optional(),
32
+ phone: zod_1.z.string().max(30).optional(),
33
+ companyId: zod_1.z.string().optional(),
34
+ type: zod_1.z.enum(['lead', 'prospect', 'client', 'partner', 'other']).default('lead'),
35
+ source: zod_1.z.string().max(100).optional(),
36
+ tags: zod_1.z.array(zod_1.z.string()).max(20).optional(),
37
+ notes: zod_1.z.string().max(10000).optional(),
38
+ jobTitle: zod_1.z.string().max(200).optional(),
39
+ linkedinUrl: zod_1.z.string().url().optional(),
40
+ customFields: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
41
+ });
42
+ const GetContactSchema = zod_1.z.object({
43
+ contactId: zod_1.z.string().min(1),
44
+ });
45
+ const UpdateContactSchema = zod_1.z.object({
46
+ contactId: zod_1.z.string().min(1),
47
+ firstName: zod_1.z.string().min(1).max(100).optional(),
48
+ lastName: zod_1.z.string().min(1).max(100).optional(),
49
+ email: zod_1.z.string().email().optional(),
50
+ phone: zod_1.z.string().max(30).optional(),
51
+ companyId: zod_1.z.string().optional(),
52
+ type: zod_1.z.enum(['lead', 'prospect', 'client', 'partner', 'other']).optional(),
53
+ source: zod_1.z.string().max(100).optional(),
54
+ tags: zod_1.z.array(zod_1.z.string()).max(20).optional(),
55
+ notes: zod_1.z.string().max(10000).optional(),
56
+ jobTitle: zod_1.z.string().max(200).optional(),
57
+ linkedinUrl: zod_1.z.string().url().optional(),
58
+ customFields: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
59
+ });
60
+ const ListContactsSchema = zod_1.z.object({
61
+ workspaceId: zod_1.z.string().min(1),
62
+ type: zod_1.z.enum(['lead', 'prospect', 'client', 'partner', 'other']).optional(),
63
+ companyId: zod_1.z.string().optional(),
64
+ limit: zod_1.z.number().int().min(1).max(100).default(20),
65
+ });
66
+ const SearchContactsSchema = zod_1.z.object({
67
+ workspaceId: zod_1.z.string().min(1),
68
+ query: zod_1.z.string().min(1).max(200),
69
+ limit: zod_1.z.number().int().min(1).max(50).default(10),
70
+ });
71
+ const DeleteContactSchema = zod_1.z.object({
72
+ contactId: zod_1.z.string().min(1),
73
+ });
74
+ const ListContactsByCustomFieldSchema = zod_1.z.object({
75
+ workspaceId: zod_1.z.string().min(1),
76
+ fieldKey: zod_1.z.string().min(1),
77
+ fieldValue: zod_1.z.unknown(),
78
+ limit: zod_1.z.number().int().min(1).max(100).default(20),
79
+ });
80
+ const RestoreContactSchema = zod_1.z.object({
81
+ contactId: zod_1.z.string().min(1),
82
+ });
83
+ // ---------------------------------------------------------------------------
84
+ // Handlers
85
+ // ---------------------------------------------------------------------------
86
+ async function create_contact(rawArgs) {
87
+ const args = CreateContactSchema.parse(rawArgs);
88
+ return (0, convexClient_1.withEnvelope)(() =>
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ (0, convexClient_1.getConvexClient)().mutation(api_1.api.crm.contacts.create, {
91
+ workspaceId: args.workspaceId,
92
+ firstName: args.firstName,
93
+ lastName: args.lastName,
94
+ email: args.email,
95
+ phone: args.phone,
96
+ companyId: args.companyId,
97
+ type: args.type,
98
+ source: args.source,
99
+ tags: args.tags,
100
+ notes: args.notes,
101
+ jobTitle: args.jobTitle,
102
+ linkedinUrl: args.linkedinUrl,
103
+ customFields: args.customFields,
104
+ }));
105
+ }
106
+ async function get_contact(rawArgs) {
107
+ const args = GetContactSchema.parse(rawArgs);
108
+ return (0, convexClient_1.withEnvelope)(() =>
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
+ (0, convexClient_1.getConvexClient)().query(api_1.api.crm.contacts.get, {
111
+ contactId: args.contactId,
112
+ }));
113
+ }
114
+ async function update_contact(rawArgs) {
115
+ const args = UpdateContactSchema.parse(rawArgs);
116
+ return (0, convexClient_1.withEnvelope)(() =>
117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+ (0, convexClient_1.getConvexClient)().mutation(api_1.api.crm.contacts.update, {
119
+ contactId: args.contactId,
120
+ firstName: args.firstName,
121
+ lastName: args.lastName,
122
+ email: args.email,
123
+ phone: args.phone,
124
+ companyId: args.companyId,
125
+ type: args.type,
126
+ source: args.source,
127
+ tags: args.tags,
128
+ notes: args.notes,
129
+ jobTitle: args.jobTitle,
130
+ linkedinUrl: args.linkedinUrl,
131
+ customFields: args.customFields,
132
+ }));
133
+ }
134
+ async function list_contacts(rawArgs) {
135
+ const args = ListContactsSchema.parse(rawArgs);
136
+ return (0, convexClient_1.withEnvelope)(() =>
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ (0, convexClient_1.getConvexClient)().query(api_1.api.crm.contacts.list, {
139
+ workspaceId: args.workspaceId,
140
+ type: args.type,
141
+ companyId: args.companyId,
142
+ limit: args.limit,
143
+ }));
144
+ }
145
+ async function search_contacts(rawArgs) {
146
+ const args = SearchContactsSchema.parse(rawArgs);
147
+ return (0, convexClient_1.withEnvelope)(() =>
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ (0, convexClient_1.getConvexClient)().query(api_1.api.crm.contacts.search, {
150
+ workspaceId: args.workspaceId,
151
+ query: args.query,
152
+ }));
153
+ }
154
+ async function delete_contact(rawArgs) {
155
+ const args = DeleteContactSchema.parse(rawArgs);
156
+ return (0, convexClient_1.withEnvelope)(() =>
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
+ (0, convexClient_1.getConvexClient)().mutation(api_1.api.crm.contacts.remove, {
159
+ contactId: args.contactId,
160
+ }));
161
+ }
162
+ async function list_contacts_by_custom_field(rawArgs) {
163
+ const args = ListContactsByCustomFieldSchema.parse(rawArgs);
164
+ // listByCustomField not yet in Convex layer — fall back to list + client-side filter
165
+ return (0, convexClient_1.withEnvelope)(async () => {
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ const all = await (0, convexClient_1.getConvexClient)().query(api_1.api.crm.contacts.list, {
168
+ workspaceId: args.workspaceId,
169
+ limit: 200,
170
+ });
171
+ return all.filter((c) => {
172
+ const cf = c['customFields'];
173
+ return cf && cf[args.fieldKey] === args.fieldValue;
174
+ }).slice(0, args.limit);
175
+ });
176
+ }
177
+ async function restore_contact(rawArgs) {
178
+ const args = RestoreContactSchema.parse(rawArgs);
179
+ return (0, convexClient_1.withEnvelope)(() =>
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ (0, convexClient_1.getConvexClient)().mutation(api_1.api.crm.contacts.restoreContact, {
182
+ contactId: args.contactId,
183
+ }));
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Tool definitions (for MCP registry)
187
+ // ---------------------------------------------------------------------------
188
+ exports.CONTACT_TOOLS = [
189
+ {
190
+ name: 'create_contact',
191
+ description: 'Create a CRM contact with optional custom fields',
192
+ requiredScope: 'write',
193
+ inputSchema: {
194
+ type: 'object',
195
+ properties: {
196
+ workspaceId: { type: 'string' },
197
+ firstName: { type: 'string' },
198
+ lastName: { type: 'string' },
199
+ email: { type: 'string' },
200
+ phone: { type: 'string' },
201
+ companyId: { type: 'string' },
202
+ type: { type: 'string', enum: ['lead', 'prospect', 'client', 'partner', 'other'] },
203
+ source: { type: 'string' },
204
+ tags: { type: 'array', items: { type: 'string' } },
205
+ notes: { type: 'string' },
206
+ jobTitle: { type: 'string' },
207
+ linkedinUrl: { type: 'string' },
208
+ customFields: { type: 'object' },
209
+ },
210
+ required: ['workspaceId', 'firstName', 'lastName'],
211
+ },
212
+ },
213
+ {
214
+ name: 'get_contact',
215
+ description: 'Retrieve a contact by ID',
216
+ requiredScope: 'read',
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: { contactId: { type: 'string' } },
220
+ required: ['contactId'],
221
+ },
222
+ },
223
+ {
224
+ name: 'update_contact',
225
+ description: 'Update a contact record',
226
+ requiredScope: 'write',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: {
230
+ contactId: { type: 'string' },
231
+ firstName: { type: 'string' },
232
+ lastName: { type: 'string' },
233
+ email: { type: 'string' },
234
+ phone: { type: 'string' },
235
+ type: { type: 'string' },
236
+ tags: { type: 'array', items: { type: 'string' } },
237
+ customFields: { type: 'object' },
238
+ },
239
+ required: ['contactId'],
240
+ },
241
+ },
242
+ {
243
+ name: 'list_contacts',
244
+ description: 'List contacts filtered by workspace/type',
245
+ requiredScope: 'read',
246
+ inputSchema: {
247
+ type: 'object',
248
+ properties: {
249
+ workspaceId: { type: 'string' },
250
+ type: { type: 'string' },
251
+ companyId: { type: 'string' },
252
+ limit: { type: 'number' },
253
+ },
254
+ required: ['workspaceId'],
255
+ },
256
+ },
257
+ {
258
+ name: 'search_contacts',
259
+ description: 'Full-text search contacts',
260
+ requiredScope: 'read',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ workspaceId: { type: 'string' },
265
+ query: { type: 'string' },
266
+ limit: { type: 'number' },
267
+ },
268
+ required: ['workspaceId', 'query'],
269
+ },
270
+ },
271
+ {
272
+ name: 'delete_contact',
273
+ description: 'Soft delete a contact (isArchived=true, 30-day grace period)',
274
+ requiredScope: 'write',
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: { contactId: { type: 'string' } },
278
+ required: ['contactId'],
279
+ },
280
+ },
281
+ {
282
+ name: 'list_contacts_by_custom_field',
283
+ description: 'Filter contacts by a custom field value',
284
+ requiredScope: 'read',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ workspaceId: { type: 'string' },
289
+ fieldKey: { type: 'string' },
290
+ fieldValue: {},
291
+ limit: { type: 'number' },
292
+ },
293
+ required: ['workspaceId', 'fieldKey', 'fieldValue'],
294
+ },
295
+ },
296
+ {
297
+ name: 'restore_contact',
298
+ description: 'Restore a soft-deleted contact (isArchived=false)',
299
+ requiredScope: 'write',
300
+ inputSchema: {
301
+ type: 'object',
302
+ properties: { contactId: { type: 'string' } },
303
+ required: ['contactId'],
304
+ },
305
+ },
306
+ ];
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ /**
3
+ * VantageCRM MCP — Custom Fields tools (5 tools)
4
+ *
5
+ * Tools: add_custom_field, update_custom_field_definition, list_custom_fields,
6
+ * set_custom_field_value, delete_custom_field
7
+ *
8
+ * Scope: read/write for most, admin for delete
9
+ * Ref: spec §3.6
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CUSTOM_FIELD_TOOLS = void 0;
13
+ exports.add_custom_field = add_custom_field;
14
+ exports.update_custom_field_definition = update_custom_field_definition;
15
+ exports.list_custom_fields = list_custom_fields;
16
+ exports.set_custom_field_value = set_custom_field_value;
17
+ exports.delete_custom_field = delete_custom_field;
18
+ const zod_1 = require("zod");
19
+ const server_1 = require("convex/server");
20
+ const convexClient_1 = require("../lib/convexClient");
21
+ // ---------------------------------------------------------------------------
22
+ // Zod schemas
23
+ // ---------------------------------------------------------------------------
24
+ const ENTITY_TYPES = ['contact', 'company', 'deal', 'activity'];
25
+ const FIELD_TYPES = ['text', 'number', 'boolean', 'date', 'select', 'multi_select', 'url', 'email', 'phone'];
26
+ const AddCustomFieldSchema = zod_1.z.object({
27
+ workspaceId: zod_1.z.string().min(1),
28
+ entityType: zod_1.z.enum(ENTITY_TYPES),
29
+ key: zod_1.z.string().min(1).max(100).regex(/^[a-z0-9_]+$/, 'Key must be snake_case'),
30
+ label: zod_1.z.string().min(1).max(200),
31
+ fieldType: zod_1.z.enum(FIELD_TYPES),
32
+ required: zod_1.z.boolean().default(false),
33
+ options: zod_1.z.array(zod_1.z.string()).optional(),
34
+ validation: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
35
+ description: zod_1.z.string().max(1000).optional(),
36
+ visibility: zod_1.z.enum(['all', 'admin', 'owner']).default('all'),
37
+ });
38
+ const UpdateCustomFieldDefinitionSchema = zod_1.z.object({
39
+ definitionId: zod_1.z.string().min(1),
40
+ label: zod_1.z.string().min(1).max(200).optional(),
41
+ options: zod_1.z.array(zod_1.z.string()).optional(),
42
+ validation: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
43
+ description: zod_1.z.string().max(1000).optional(),
44
+ visibility: zod_1.z.enum(['all', 'admin', 'owner']).optional(),
45
+ required: zod_1.z.boolean().optional(),
46
+ });
47
+ const ListCustomFieldsSchema = zod_1.z.object({
48
+ workspaceId: zod_1.z.string().min(1),
49
+ entityType: zod_1.z.enum(ENTITY_TYPES).optional(),
50
+ });
51
+ const SetCustomFieldValueSchema = zod_1.z.object({
52
+ workspaceId: zod_1.z.string().min(1),
53
+ entityType: zod_1.z.enum(ENTITY_TYPES),
54
+ entityId: zod_1.z.string().min(1),
55
+ key: zod_1.z.string().min(1),
56
+ value: zod_1.z.unknown(),
57
+ });
58
+ const DeleteCustomFieldSchema = zod_1.z.object({
59
+ definitionId: zod_1.z.string().min(1),
60
+ });
61
+ // ---------------------------------------------------------------------------
62
+ // Handlers
63
+ // ---------------------------------------------------------------------------
64
+ async function add_custom_field(rawArgs) {
65
+ const args = AddCustomFieldSchema.parse(rawArgs);
66
+ return (0, convexClient_1.withEnvelope)(() => (0, convexClient_1.getConvexClient)().mutation(server_1.anyApi.crm.customFields.defineCustomField, {
67
+ workspaceId: args.workspaceId,
68
+ entityType: args.entityType,
69
+ key: args.key,
70
+ label: args.label,
71
+ fieldType: args.fieldType,
72
+ required: args.required,
73
+ options: args.options,
74
+ description: args.description,
75
+ visibility: args.visibility,
76
+ }));
77
+ }
78
+ async function update_custom_field_definition(rawArgs) {
79
+ const args = UpdateCustomFieldDefinitionSchema.parse(rawArgs);
80
+ return (0, convexClient_1.withEnvelope)(() => (0, convexClient_1.getConvexClient)().mutation(server_1.anyApi.crm.customFields.updateCustomFieldDefinition, {
81
+ definitionId: args.definitionId,
82
+ label: args.label,
83
+ options: args.options,
84
+ description: args.description,
85
+ visibility: args.visibility,
86
+ required: args.required,
87
+ }));
88
+ }
89
+ async function list_custom_fields(rawArgs) {
90
+ const args = ListCustomFieldsSchema.parse(rawArgs);
91
+ return (0, convexClient_1.withEnvelope)(() => (0, convexClient_1.getConvexClient)().query(server_1.anyApi.crm.customFields.listCustomFieldDefinitions, {
92
+ workspaceId: args.workspaceId,
93
+ entityType: args.entityType,
94
+ }));
95
+ }
96
+ async function set_custom_field_value(rawArgs) {
97
+ const args = SetCustomFieldValueSchema.parse(rawArgs);
98
+ // setValue is implemented as a patch on the entity's customFields JSON column
99
+ // Map entityType to the correct mutation
100
+ return (0, convexClient_1.withEnvelope)(async () => {
101
+ const entityType = args.entityType;
102
+ const entityId = args.entityId;
103
+ const patch = { customFields: { [args.key]: args.value } };
104
+ if (entityType === 'contact') {
105
+ return (0, convexClient_1.getConvexClient)().mutation(server_1.anyApi.crm.contacts.update, {
106
+ contactId: entityId,
107
+ customFields: patch.customFields,
108
+ });
109
+ }
110
+ else if (entityType === 'company') {
111
+ return (0, convexClient_1.getConvexClient)().mutation(server_1.anyApi.crm.companies.update, {
112
+ companyId: entityId,
113
+ customFields: patch.customFields,
114
+ });
115
+ }
116
+ else if (entityType === 'deal') {
117
+ return (0, convexClient_1.getConvexClient)().mutation(server_1.anyApi.crm.deals.update, {
118
+ dealId: entityId,
119
+ customFields: patch.customFields,
120
+ });
121
+ }
122
+ else if (entityType === 'activity') {
123
+ return (0, convexClient_1.getConvexClient)().mutation(server_1.anyApi.crm.activities.update, {
124
+ activityId: entityId,
125
+ customFields: patch.customFields,
126
+ });
127
+ }
128
+ throw new Error(`Unsupported entityType: ${entityType}`);
129
+ });
130
+ }
131
+ async function delete_custom_field(rawArgs) {
132
+ DeleteCustomFieldSchema.parse(rawArgs);
133
+ // deleteDefinition not yet in Convex layer V0.1.0 — stable NOT_IMPLEMENTED envelope
134
+ return {
135
+ success: false,
136
+ error: {
137
+ code: 'NOT_IMPLEMENTED',
138
+ message: 'delete_custom_field: reserved for V0.2. Requires crm.customFields.deleteCustomFieldDefinition mutation.',
139
+ },
140
+ };
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // Tool definitions
144
+ // ---------------------------------------------------------------------------
145
+ exports.CUSTOM_FIELD_TOOLS = [
146
+ {
147
+ name: 'add_custom_field',
148
+ description: 'Create a custom field definition for an entity type',
149
+ requiredScope: 'write',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ workspaceId: { type: 'string' },
154
+ entityType: { type: 'string', enum: ['contact', 'company', 'deal', 'activity'] },
155
+ key: { type: 'string', description: 'snake_case identifier' },
156
+ label: { type: 'string' },
157
+ fieldType: { type: 'string', enum: ['text', 'number', 'boolean', 'date', 'select', 'multi_select', 'url', 'email', 'phone'] },
158
+ required: { type: 'boolean' },
159
+ options: { type: 'array', items: { type: 'string' } },
160
+ description: { type: 'string' },
161
+ visibility: { type: 'string', enum: ['all', 'admin', 'owner'] },
162
+ },
163
+ required: ['workspaceId', 'entityType', 'key', 'label', 'fieldType'],
164
+ },
165
+ },
166
+ {
167
+ name: 'update_custom_field_definition',
168
+ description: 'Update a custom field definition (label, options, validation)',
169
+ requiredScope: 'write',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ definitionId: { type: 'string' },
174
+ label: { type: 'string' },
175
+ options: { type: 'array', items: { type: 'string' } },
176
+ description: { type: 'string' },
177
+ visibility: { type: 'string' },
178
+ required: { type: 'boolean' },
179
+ },
180
+ required: ['definitionId'],
181
+ },
182
+ },
183
+ {
184
+ name: 'list_custom_fields',
185
+ description: 'List custom field definitions for a workspace/entity type',
186
+ requiredScope: 'read',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ workspaceId: { type: 'string' },
191
+ entityType: { type: 'string' },
192
+ },
193
+ required: ['workspaceId'],
194
+ },
195
+ },
196
+ {
197
+ name: 'set_custom_field_value',
198
+ description: 'Set a custom field value on an entity record',
199
+ requiredScope: 'write',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ workspaceId: { type: 'string' },
204
+ entityType: { type: 'string', enum: ['contact', 'company', 'deal', 'activity'] },
205
+ entityId: { type: 'string' },
206
+ key: { type: 'string' },
207
+ value: {},
208
+ },
209
+ required: ['workspaceId', 'entityType', 'entityId', 'key', 'value'],
210
+ },
211
+ },
212
+ {
213
+ name: 'delete_custom_field',
214
+ description: 'Delete a custom field definition (admin only — irreversible)',
215
+ requiredScope: 'admin',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: { definitionId: { type: 'string' } },
219
+ required: ['definitionId'],
220
+ },
221
+ },
222
+ ];