@firecms/mcp-server 3.3.0-canary.451aa49 → 3.3.0-canary.69e5ab1

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 (38) hide show
  1. package/README.md +86 -7
  2. package/dist/api-client.d.ts +70 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +207 -3
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/resources/project.d.ts +1 -0
  7. package/dist/resources/project.d.ts.map +1 -1
  8. package/dist/resources/project.js +55 -0
  9. package/dist/resources/project.js.map +1 -1
  10. package/dist/server.d.ts +12 -0
  11. package/dist/server.d.ts.map +1 -1
  12. package/dist/server.js +23 -2
  13. package/dist/server.js.map +1 -1
  14. package/dist/tools/collection-schemas.d.ts +8 -0
  15. package/dist/tools/collection-schemas.d.ts.map +1 -0
  16. package/dist/tools/collection-schemas.js +278 -0
  17. package/dist/tools/collection-schemas.js.map +1 -0
  18. package/dist/tools/import.d.ts +8 -0
  19. package/dist/tools/import.d.ts.map +1 -0
  20. package/dist/tools/import.js +57 -0
  21. package/dist/tools/import.js.map +1 -0
  22. package/dist/tools/project-config.d.ts +8 -0
  23. package/dist/tools/project-config.d.ts.map +1 -0
  24. package/dist/tools/project-config.js +174 -0
  25. package/dist/tools/project-config.js.map +1 -0
  26. package/dist/tools/users.d.ts +2 -0
  27. package/dist/tools/users.d.ts.map +1 -1
  28. package/dist/tools/users.js +10 -3
  29. package/dist/tools/users.js.map +1 -1
  30. package/package.json +6 -6
  31. package/src/api-client.ts +242 -3
  32. package/src/resources/project.ts +69 -0
  33. package/src/server.ts +26 -2
  34. package/src/tools/collection-schemas.ts +316 -0
  35. package/src/tools/import.ts +65 -0
  36. package/src/tools/project-config.ts +202 -0
  37. package/src/tools/users.ts +10 -3
  38. package/LICENSE +0 -6
@@ -0,0 +1,316 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { FireCMSApiClient } from "../api-client.js";
4
+
5
+ /**
6
+ * Property schema definition for collection properties.
7
+ * Supports all FireCMS property types.
8
+ */
9
+ const PropertySchema = z.object({
10
+ dataType: z.enum([
11
+ "string", "number", "boolean", "date", "map",
12
+ "array", "geopoint", "reference",
13
+ ]).describe("The data type of this property"),
14
+ name: z.string().optional().describe("Display name for the property"),
15
+ description: z.string().optional().describe("Description shown as a tooltip"),
16
+ validation: z.record(z.any()).optional().describe("Validation rules (e.g., { required: true, min: 0, max: 100 })"),
17
+ enumValues: z.array(z.object({
18
+ id: z.string().describe("Enum value ID"),
19
+ label: z.string().describe("Display label"),
20
+ color: z.string().optional().describe("Color for the enum chip"),
21
+ })).optional().describe("Enum values for string/number fields (renders as select/chips)"),
22
+ multiline: z.boolean().optional().describe("For strings: render as textarea"),
23
+ markdown: z.boolean().optional().describe("For strings: render as markdown editor"),
24
+ url: z.union([z.boolean(), z.enum(["image", "video", "audio"])]).optional().describe("For strings: treat as URL, optionally specify media type"),
25
+ email: z.boolean().optional().describe("For strings: validate as email"),
26
+ storage: z.object({
27
+ storagePath: z.string().describe("Firebase Storage path"),
28
+ acceptedFiles: z.array(z.string()).optional().describe("Accepted MIME types"),
29
+ maxSize: z.number().optional().describe("Max file size in bytes"),
30
+ }).optional().describe("For strings: file upload configuration"),
31
+ of: z.any().optional().describe("For arrays: the property definition of array items"),
32
+ properties: z.record(z.any()).optional().describe("For maps: nested property definitions"),
33
+ path: z.string().optional().describe("For references: target collection path"),
34
+ previewProperties: z.array(z.string()).optional().describe("For references: properties to show in preview"),
35
+ disabled: z.boolean().optional().describe("If true, field is read-only in the form"),
36
+ columnWidth: z.number().optional().describe("Column width in pixels for table view"),
37
+ hideFromCollection: z.boolean().optional().describe("If true, hide from the table view"),
38
+ }).passthrough();
39
+
40
+ /**
41
+ * Register collection schema CRUD tools — manage the collection configurations
42
+ * stored in FireCMS Cloud. Admin-only operations.
43
+ */
44
+ export function registerCollectionSchemaTools(server: McpServer, api: FireCMSApiClient) {
45
+
46
+ // ─── 1. List all collection schemas ────────────────────
47
+
48
+ server.registerTool(
49
+ "list_collection_schemas",
50
+ {
51
+ description: `List all persisted collection schemas for a FireCMS project. Returns the collection
52
+ configurations (name, path, properties, etc.) that define how data is displayed and edited in the CMS.`,
53
+ inputSchema: {
54
+ projectId: z.string().describe("Firebase project ID"),
55
+ },
56
+ annotations: { readOnlyHint: true },
57
+ },
58
+ async ({ projectId }) => {
59
+ try {
60
+ await api.assertAdmin(projectId);
61
+ const schemas = await api.listCollectionSchemas(projectId);
62
+ return {
63
+ content: [{
64
+ type: "text" as const,
65
+ text: JSON.stringify(schemas, null, 2),
66
+ }],
67
+ };
68
+ } catch (error: any) {
69
+ return {
70
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
71
+ isError: true,
72
+ };
73
+ }
74
+ }
75
+ );
76
+
77
+ // ─── 2. Get a single collection schema ─────────────────
78
+
79
+ server.registerTool(
80
+ "get_collection_schema",
81
+ {
82
+ description: `Get the full schema definition for a specific collection, including all properties,
83
+ validation rules, display configuration, and subcollection definitions.`,
84
+ inputSchema: {
85
+ projectId: z.string().describe("Firebase project ID"),
86
+ collectionId: z.string().describe("Collection ID (usually same as the Firestore path, e.g., 'products')"),
87
+ },
88
+ annotations: { readOnlyHint: true },
89
+ },
90
+ async ({ projectId, collectionId }) => {
91
+ try {
92
+ await api.assertAdmin(projectId);
93
+ const schema = await api.getCollectionSchema(projectId, collectionId);
94
+ return {
95
+ content: [{
96
+ type: "text" as const,
97
+ text: JSON.stringify(schema, null, 2),
98
+ }],
99
+ };
100
+ } catch (error: any) {
101
+ return {
102
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
103
+ isError: true,
104
+ };
105
+ }
106
+ }
107
+ );
108
+
109
+ // ─── 3. Save (create/replace) a collection schema ──────
110
+
111
+ server.registerTool(
112
+ "save_collection_schema",
113
+ {
114
+ description: `Create or fully replace a collection schema. This defines how a Firestore collection
115
+ is displayed and edited in FireCMS. Requires at minimum: id, path, and name.
116
+
117
+ Example schema:
118
+ {
119
+ "id": "products",
120
+ "path": "products",
121
+ "name": "Products",
122
+ "singularName": "Product",
123
+ "icon": "ShoppingCart",
124
+ "description": "Product catalog",
125
+ "group": "Shop",
126
+ "properties": {
127
+ "name": { "dataType": "string", "name": "Name", "validation": { "required": true } },
128
+ "price": { "dataType": "number", "name": "Price", "validation": { "required": true, "min": 0 } },
129
+ "status": { "dataType": "string", "name": "Status", "enumValues": [
130
+ { "id": "draft", "label": "Draft" },
131
+ { "id": "published", "label": "Published" }
132
+ ]}
133
+ },
134
+ "propertiesOrder": ["name", "price", "status"]
135
+ }`,
136
+ inputSchema: {
137
+ projectId: z.string().describe("Firebase project ID"),
138
+ collectionId: z.string().describe("Collection ID (e.g., 'products')"),
139
+ schema: z.object({
140
+ path: z.string().describe("Firestore collection path"),
141
+ name: z.string().describe("Display name for the collection"),
142
+ singularName: z.string().optional().describe("Singular name (e.g., 'Product')"),
143
+ description: z.string().optional().describe("Collection description"),
144
+ icon: z.string().optional().describe("Material icon name (e.g., 'ShoppingCart')"),
145
+ group: z.string().optional().describe("Navigation group name"),
146
+ properties: z.record(PropertySchema).describe("Property definitions keyed by field name"),
147
+ propertiesOrder: z.array(z.string()).optional().describe("Display order of properties"),
148
+ defaultSize: z.enum(["xs", "s", "m", "l", "xl"]).optional().describe("Default row size in table"),
149
+ initialSort: z.tuple([z.string(), z.enum(["asc", "desc"])]).optional().describe("Default sort [field, direction]"),
150
+ }).passthrough().describe("Complete collection schema definition"),
151
+ },
152
+ },
153
+ async ({ projectId, collectionId, schema }) => {
154
+ try {
155
+ await api.assertAdmin(projectId);
156
+ const fullSchema = { id: collectionId, ...schema };
157
+ const result = await api.saveCollectionSchema(projectId, collectionId, fullSchema);
158
+ return {
159
+ content: [{
160
+ type: "text" as const,
161
+ text: `Collection schema "${collectionId}" saved successfully.\n\n${JSON.stringify(result, null, 2)}`,
162
+ }],
163
+ };
164
+ } catch (error: any) {
165
+ return {
166
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
167
+ isError: true,
168
+ };
169
+ }
170
+ }
171
+ );
172
+
173
+ // ─── 4. Update (partial) a collection schema ───────────
174
+
175
+ server.registerTool(
176
+ "update_collection_schema",
177
+ {
178
+ description: `Partially update an existing collection schema. Only the specified fields are modified
179
+ (merged with the existing schema). Use this for changes like renaming, updating the group,
180
+ changing display settings, or adding new properties.`,
181
+ inputSchema: {
182
+ projectId: z.string().describe("Firebase project ID"),
183
+ collectionId: z.string().describe("Collection ID to update"),
184
+ data: z.record(z.any()).describe("Fields to update (merged with existing schema)"),
185
+ },
186
+ },
187
+ async ({ projectId, collectionId, data }) => {
188
+ try {
189
+ await api.assertAdmin(projectId);
190
+ const result = await api.updateCollectionSchema(projectId, collectionId, data);
191
+ return {
192
+ content: [{
193
+ type: "text" as const,
194
+ text: `Collection schema "${collectionId}" updated successfully.\n\n${JSON.stringify(result, null, 2)}`,
195
+ }],
196
+ };
197
+ } catch (error: any) {
198
+ return {
199
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
200
+ isError: true,
201
+ };
202
+ }
203
+ }
204
+ );
205
+
206
+ // ─── 5. Delete a collection schema ─────────────────────
207
+
208
+ server.registerTool(
209
+ "delete_collection_schema",
210
+ {
211
+ description: `Delete a collection schema from FireCMS. This removes the collection configuration
212
+ from the CMS — it does NOT delete the underlying Firestore data. The collection will simply
213
+ no longer appear in the FireCMS UI.`,
214
+ inputSchema: {
215
+ projectId: z.string().describe("Firebase project ID"),
216
+ collectionId: z.string().describe("Collection ID to delete"),
217
+ },
218
+ annotations: { destructiveHint: true },
219
+ },
220
+ async ({ projectId, collectionId }) => {
221
+ try {
222
+ await api.assertAdmin(projectId);
223
+ await api.deleteCollectionSchema(projectId, collectionId);
224
+ return {
225
+ content: [{
226
+ type: "text" as const,
227
+ text: `Collection schema "${collectionId}" deleted successfully. Note: the underlying Firestore data was not affected.`,
228
+ }],
229
+ };
230
+ } catch (error: any) {
231
+ return {
232
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
233
+ isError: true,
234
+ };
235
+ }
236
+ }
237
+ );
238
+
239
+ // ─── 6. Save (add/update) a single property ────────────
240
+
241
+ server.registerTool(
242
+ "save_property",
243
+ {
244
+ description: `Add or update a single property in a collection schema. This is more granular than
245
+ updating the entire schema — use it when you want to add a new field or modify an existing
246
+ one without affecting other properties.
247
+
248
+ Example property:
249
+ {
250
+ "dataType": "string",
251
+ "name": "Description",
252
+ "description": "Product description",
253
+ "multiline": true,
254
+ "validation": { "required": true, "max": 500 }
255
+ }`,
256
+ inputSchema: {
257
+ projectId: z.string().describe("Firebase project ID"),
258
+ collectionId: z.string().describe("Collection ID"),
259
+ propertyKey: z.string().describe("Property field key (e.g., 'description', 'price')"),
260
+ property: PropertySchema.describe("Property definition"),
261
+ namespace: z.string().optional().describe("Dot-separated namespace for nested properties in maps (e.g., 'address' for address.street)"),
262
+ },
263
+ },
264
+ async ({ projectId, collectionId, propertyKey, property, namespace }) => {
265
+ try {
266
+ await api.assertAdmin(projectId);
267
+ const result = await api.saveProperty(projectId, collectionId, propertyKey, property, namespace);
268
+ return {
269
+ content: [{
270
+ type: "text" as const,
271
+ text: `Property "${propertyKey}" saved in collection "${collectionId}" successfully.\n\n${JSON.stringify(result, null, 2)}`,
272
+ }],
273
+ };
274
+ } catch (error: any) {
275
+ return {
276
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
277
+ isError: true,
278
+ };
279
+ }
280
+ }
281
+ );
282
+
283
+ // ─── 7. Delete a property ──────────────────────────────
284
+
285
+ server.registerTool(
286
+ "delete_property",
287
+ {
288
+ description: `Remove a property from a collection schema. This removes the field definition from
289
+ the CMS configuration — it does NOT delete the field from existing Firestore documents.`,
290
+ inputSchema: {
291
+ projectId: z.string().describe("Firebase project ID"),
292
+ collectionId: z.string().describe("Collection ID"),
293
+ propertyKey: z.string().describe("Property key to remove"),
294
+ namespace: z.string().optional().describe("Dot-separated namespace for nested properties"),
295
+ },
296
+ annotations: { destructiveHint: true },
297
+ },
298
+ async ({ projectId, collectionId, propertyKey, namespace }) => {
299
+ try {
300
+ await api.assertAdmin(projectId);
301
+ await api.deleteProperty(projectId, collectionId, propertyKey, namespace);
302
+ return {
303
+ content: [{
304
+ type: "text" as const,
305
+ text: `Property "${propertyKey}" removed from collection "${collectionId}". Note: existing Firestore data was not affected.`,
306
+ }],
307
+ };
308
+ } catch (error: any) {
309
+ return {
310
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
311
+ isError: true,
312
+ };
313
+ }
314
+ }
315
+ );
316
+ }
@@ -0,0 +1,65 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { FireCMSApiClient } from "../api-client.js";
4
+
5
+ /**
6
+ * Register data import tools — bulk operations for Firestore documents.
7
+ * Admin-only operations.
8
+ */
9
+ export function registerImportTools(server: McpServer, api: FireCMSApiClient) {
10
+
11
+ server.registerTool(
12
+ "import_documents",
13
+ {
14
+ description: `Bulk import documents into a Firestore collection. Useful for seeding data,
15
+ migrations, or restoring from a backup. Each document can optionally specify an ID;
16
+ if omitted, Firestore generates one. Use "merge: true" to update existing documents
17
+ instead of overwriting.
18
+
19
+ Maximum 500 documents per call. For larger imports, call multiple times.`,
20
+ inputSchema: {
21
+ projectId: z.string().describe("Firebase project ID"),
22
+ collectionPath: z.string().describe("Target collection path (e.g., 'products')"),
23
+ documents: z.array(z.object({
24
+ id: z.string().optional().describe("Optional document ID"),
25
+ data: z.record(z.any()).describe("Document fields"),
26
+ })).describe("Array of documents to import (max 500)"),
27
+ merge: z.boolean().optional().describe("If true, merge with existing documents instead of overwriting (default: false)"),
28
+ databaseId: z.string().optional().describe("Firestore database ID (default: '(default)')"),
29
+ },
30
+ },
31
+ async ({ projectId, collectionPath, documents, merge, databaseId }) => {
32
+ try {
33
+ await api.assertAdmin(projectId);
34
+
35
+ if (documents.length > 500) {
36
+ return {
37
+ content: [{
38
+ type: "text" as const,
39
+ text: `Error: Maximum 500 documents per import call. You provided ${documents.length}. Split into batches.`,
40
+ }],
41
+ isError: true,
42
+ };
43
+ }
44
+
45
+ const result = await api.importDocuments(projectId, {
46
+ path: collectionPath,
47
+ documents,
48
+ merge: merge ?? false,
49
+ databaseId,
50
+ });
51
+ return {
52
+ content: [{
53
+ type: "text" as const,
54
+ text: `Successfully imported ${documents.length} document(s) into "${collectionPath}".\n\n${JSON.stringify(result, null, 2)}`,
55
+ }],
56
+ };
57
+ } catch (error: any) {
58
+ return {
59
+ content: [{ type: "text" as const, text: `Error importing: ${error.response?.data?.error ?? error.message}` }],
60
+ isError: true,
61
+ };
62
+ }
63
+ }
64
+ );
65
+ }
@@ -0,0 +1,202 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { FireCMSApiClient } from "../api-client.js";
4
+
5
+ /**
6
+ * Register project configuration tools — manage project settings like
7
+ * name, colors, locale, feature toggles, etc. Admin-only operations.
8
+ */
9
+ export function registerProjectConfigTools(server: McpServer, api: FireCMSApiClient) {
10
+
11
+ // ─── 1. Get project configuration ──────────────────────
12
+
13
+ server.registerTool(
14
+ "get_project_config",
15
+ {
16
+ description: `Get the full configuration for a FireCMS project, including:
17
+ - Project name, logo, and brand colors (primary/secondary)
18
+ - Subscription plan and trial status
19
+ - Feature toggles (text search, entity history, App Check)
20
+ - Default locale settings
21
+ - Customization revision info`,
22
+ inputSchema: {
23
+ projectId: z.string().describe("Firebase project ID"),
24
+ },
25
+ annotations: { readOnlyHint: true },
26
+ },
27
+ async ({ projectId }) => {
28
+ try {
29
+ await api.assertAdmin(projectId);
30
+ const config = await api.getProjectConfig(projectId);
31
+ return {
32
+ content: [{
33
+ type: "text" as const,
34
+ text: JSON.stringify(config, null, 2),
35
+ }],
36
+ };
37
+ } catch (error: any) {
38
+ return {
39
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
40
+ isError: true,
41
+ };
42
+ }
43
+ }
44
+ );
45
+
46
+ // ─── 2. Update project name ────────────────────────────
47
+
48
+ server.registerTool(
49
+ "update_project_name",
50
+ {
51
+ description: "Update the display name of a FireCMS project.",
52
+ inputSchema: {
53
+ projectId: z.string().describe("Firebase project ID"),
54
+ name: z.string().describe("New project name"),
55
+ },
56
+ },
57
+ async ({ projectId, name }) => {
58
+ try {
59
+ await api.assertAdmin(projectId);
60
+ await api.updateProjectConfig(projectId, { name });
61
+ return {
62
+ content: [{
63
+ type: "text" as const,
64
+ text: `Project name updated to "${name}".`,
65
+ }],
66
+ };
67
+ } catch (error: any) {
68
+ return {
69
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
70
+ isError: true,
71
+ };
72
+ }
73
+ }
74
+ );
75
+
76
+ // ─── 3. Update brand colors ────────────────────────────
77
+
78
+ server.registerTool(
79
+ "update_project_colors",
80
+ {
81
+ description: "Update the primary and/or secondary brand colors for the CMS UI. Colors should be hex values (e.g., '#0070F4').",
82
+ inputSchema: {
83
+ projectId: z.string().describe("Firebase project ID"),
84
+ primaryColor: z.string().optional().describe("Primary color hex (e.g., '#0070F4')"),
85
+ secondaryColor: z.string().optional().describe("Secondary color hex (e.g., '#FF5B79')"),
86
+ },
87
+ },
88
+ async ({ projectId, primaryColor, secondaryColor }) => {
89
+ try {
90
+ await api.assertAdmin(projectId);
91
+ const data: Record<string, any> = {};
92
+ if (primaryColor) data.primary_color = primaryColor;
93
+ if (secondaryColor) data.secondary_color = secondaryColor;
94
+ await api.updateProjectConfig(projectId, data);
95
+ const parts: string[] = [];
96
+ if (primaryColor) parts.push(`primary → ${primaryColor}`);
97
+ if (secondaryColor) parts.push(`secondary → ${secondaryColor}`);
98
+ return {
99
+ content: [{
100
+ type: "text" as const,
101
+ text: `Brand colors updated: ${parts.join(", ")}.`,
102
+ }],
103
+ };
104
+ } catch (error: any) {
105
+ return {
106
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
107
+ isError: true,
108
+ };
109
+ }
110
+ }
111
+ );
112
+
113
+ // ─── 4. Update default locale ──────────────────────────
114
+
115
+ server.registerTool(
116
+ "update_default_locale",
117
+ {
118
+ description: "Change the default locale for the CMS (affects date formatting, etc.).",
119
+ inputSchema: {
120
+ projectId: z.string().describe("Firebase project ID"),
121
+ locale: z.string().describe("Locale code (e.g., 'en', 'es', 'de', 'fr', 'it')"),
122
+ },
123
+ },
124
+ async ({ projectId, locale }) => {
125
+ try {
126
+ await api.assertAdmin(projectId);
127
+ await api.updateProjectConfig(projectId, { default_locale: locale });
128
+ return {
129
+ content: [{
130
+ type: "text" as const,
131
+ text: `Default locale updated to "${locale}".`,
132
+ }],
133
+ };
134
+ } catch (error: any) {
135
+ return {
136
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
137
+ isError: true,
138
+ };
139
+ }
140
+ }
141
+ );
142
+
143
+ // ─── 5. Toggle text search ─────────────────────────────
144
+
145
+ server.registerTool(
146
+ "toggle_text_search",
147
+ {
148
+ description: "Enable or disable the local text search feature for a project.",
149
+ inputSchema: {
150
+ projectId: z.string().describe("Firebase project ID"),
151
+ enabled: z.boolean().describe("true to enable, false to disable"),
152
+ },
153
+ },
154
+ async ({ projectId, enabled }) => {
155
+ try {
156
+ await api.assertAdmin(projectId);
157
+ await api.updateProjectConfig(projectId, { local_text_search_enabled: enabled });
158
+ return {
159
+ content: [{
160
+ type: "text" as const,
161
+ text: `Local text search ${enabled ? "enabled" : "disabled"}.`,
162
+ }],
163
+ };
164
+ } catch (error: any) {
165
+ return {
166
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
167
+ isError: true,
168
+ };
169
+ }
170
+ }
171
+ );
172
+
173
+ // ─── 6. Toggle entity history ──────────────────────────
174
+
175
+ server.registerTool(
176
+ "toggle_entity_history",
177
+ {
178
+ description: "Enable or disable entity history tracking (audit log of document changes) for a project.",
179
+ inputSchema: {
180
+ projectId: z.string().describe("Firebase project ID"),
181
+ enabled: z.boolean().describe("true to enable, false to disable"),
182
+ },
183
+ },
184
+ async ({ projectId, enabled }) => {
185
+ try {
186
+ await api.assertAdmin(projectId);
187
+ await api.updateProjectConfig(projectId, { history_default_enabled: enabled });
188
+ return {
189
+ content: [{
190
+ type: "text" as const,
191
+ text: `Entity history tracking ${enabled ? "enabled" : "disabled"}.`,
192
+ }],
193
+ };
194
+ } catch (error: any) {
195
+ return {
196
+ content: [{ type: "text" as const, text: `Error: ${error.response?.data?.error ?? error.message}` }],
197
+ isError: true,
198
+ };
199
+ }
200
+ }
201
+ );
202
+ }
@@ -4,6 +4,8 @@ import { FireCMSApiClient } from "../api-client.js";
4
4
 
5
5
  /**
6
6
  * Register user management tools.
7
+ * Read operations are available to all authenticated users.
8
+ * Write operations (add, update, remove) are admin-only.
7
9
  */
8
10
  export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
9
11
 
@@ -14,6 +16,7 @@ export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
14
16
  inputSchema: {
15
17
  projectId: z.string().describe("The Firebase project ID"),
16
18
  },
19
+ annotations: { readOnlyHint: true },
17
20
  },
18
21
  async ({ projectId }) => {
19
22
  try {
@@ -28,7 +31,7 @@ export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
28
31
  server.registerTool(
29
32
  "add_user",
30
33
  {
31
- description: "Invite a new user to a FireCMS project. Sends an invitation email.",
34
+ description: "Invite a new user to a FireCMS project. Sends an invitation email. Admin-only.",
32
35
  inputSchema: {
33
36
  projectId: z.string().describe("The Firebase project ID"),
34
37
  email: z.string().email().describe("Email address of the user to invite"),
@@ -37,6 +40,7 @@ export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
37
40
  },
38
41
  async ({ projectId, email, roles }) => {
39
42
  try {
43
+ await api.assertAdmin(projectId);
40
44
  const result = await api.createUser(projectId, email, roles);
41
45
  return {
42
46
  content: [{ type: "text" as const, text: `Invited ${email} with roles: ${roles.join(", ")}\n\n${JSON.stringify(result, null, 2)}` }],
@@ -50,7 +54,7 @@ export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
50
54
  server.registerTool(
51
55
  "update_user_roles",
52
56
  {
53
- description: "Update the roles of an existing user in a FireCMS project",
57
+ description: "Update the roles of an existing user in a FireCMS project. Admin-only.",
54
58
  inputSchema: {
55
59
  projectId: z.string().describe("The Firebase project ID"),
56
60
  userId: z.string().describe("The user ID to update"),
@@ -59,6 +63,7 @@ export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
59
63
  },
60
64
  async ({ projectId, userId, roles }) => {
61
65
  try {
66
+ await api.assertAdmin(projectId);
62
67
  const result = await api.updateUser(projectId, userId, roles);
63
68
  return {
64
69
  content: [{ type: "text" as const, text: `Updated user ${userId} roles to: ${roles.join(", ")}\n\n${JSON.stringify(result, null, 2)}` }],
@@ -72,14 +77,16 @@ export function registerUserTools(server: McpServer, api: FireCMSApiClient) {
72
77
  server.registerTool(
73
78
  "remove_user",
74
79
  {
75
- description: "Remove a user from a FireCMS project, revoking their access",
80
+ description: "Remove a user from a FireCMS project, revoking their access. Admin-only.",
76
81
  inputSchema: {
77
82
  projectId: z.string().describe("The Firebase project ID"),
78
83
  userId: z.string().describe("The user ID to remove"),
79
84
  },
85
+ annotations: { destructiveHint: true },
80
86
  },
81
87
  async ({ projectId, userId }) => {
82
88
  try {
89
+ await api.assertAdmin(projectId);
83
90
  const result = await api.deleteUser(projectId, userId);
84
91
  return {
85
92
  content: [{ type: "text" as const, text: `Removed user ${userId}\n\n${JSON.stringify(result, null, 2)}` }],
package/LICENSE DELETED
@@ -1,6 +0,0 @@
1
- Source code in this repository is variously licensed under the Business Source
2
- License 1.1 (BSL), Apache version 2.0 and the MIT license. A copy of each
3
- license can be found in each one of the packages under the folder packages
4
- under a file called License. Source code in a given file is licensed under the
5
- BSL and the copyright belongs to Firecms Authors unless otherwise noted at the
6
- beginning of the file.