@firecms/mcp-server 3.2.0 → 3.3.0-canary.102f274

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
@@ -2,6 +2,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { FireCMSApiClient } from "../api-client.js";
3
3
  /**
4
4
  * Register user management tools.
5
+ * Read operations are available to all authenticated users.
6
+ * Write operations (add, update, remove) are admin-only.
5
7
  */
6
8
  export declare function registerUserTools(server: McpServer, api: FireCMSApiClient): void;
7
9
  //# sourceMappingURL=users.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/tools/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,QAoFzE"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/tools/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,QAyFzE"}
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  /**
3
3
  * Register user management tools.
4
+ * Read operations are available to all authenticated users.
5
+ * Write operations (add, update, remove) are admin-only.
4
6
  */
5
7
  export function registerUserTools(server, api) {
6
8
  server.registerTool("list_users", {
@@ -8,6 +10,7 @@ export function registerUserTools(server, api) {
8
10
  inputSchema: {
9
11
  projectId: z.string().describe("The Firebase project ID"),
10
12
  },
13
+ annotations: { readOnlyHint: true },
11
14
  }, async ({ projectId }) => {
12
15
  try {
13
16
  const result = await api.listUsers(projectId);
@@ -18,7 +21,7 @@ export function registerUserTools(server, api) {
18
21
  }
19
22
  });
20
23
  server.registerTool("add_user", {
21
- description: "Invite a new user to a FireCMS project. Sends an invitation email.",
24
+ description: "Invite a new user to a FireCMS project. Sends an invitation email. Admin-only.",
22
25
  inputSchema: {
23
26
  projectId: z.string().describe("The Firebase project ID"),
24
27
  email: z.string().email().describe("Email address of the user to invite"),
@@ -26,6 +29,7 @@ export function registerUserTools(server, api) {
26
29
  },
27
30
  }, async ({ projectId, email, roles }) => {
28
31
  try {
32
+ await api.assertAdmin(projectId);
29
33
  const result = await api.createUser(projectId, email, roles);
30
34
  return {
31
35
  content: [{ type: "text", text: `Invited ${email} with roles: ${roles.join(", ")}\n\n${JSON.stringify(result, null, 2)}` }],
@@ -36,7 +40,7 @@ export function registerUserTools(server, api) {
36
40
  }
37
41
  });
38
42
  server.registerTool("update_user_roles", {
39
- description: "Update the roles of an existing user in a FireCMS project",
43
+ description: "Update the roles of an existing user in a FireCMS project. Admin-only.",
40
44
  inputSchema: {
41
45
  projectId: z.string().describe("The Firebase project ID"),
42
46
  userId: z.string().describe("The user ID to update"),
@@ -44,6 +48,7 @@ export function registerUserTools(server, api) {
44
48
  },
45
49
  }, async ({ projectId, userId, roles }) => {
46
50
  try {
51
+ await api.assertAdmin(projectId);
47
52
  const result = await api.updateUser(projectId, userId, roles);
48
53
  return {
49
54
  content: [{ type: "text", text: `Updated user ${userId} roles to: ${roles.join(", ")}\n\n${JSON.stringify(result, null, 2)}` }],
@@ -54,13 +59,15 @@ export function registerUserTools(server, api) {
54
59
  }
55
60
  });
56
61
  server.registerTool("remove_user", {
57
- description: "Remove a user from a FireCMS project, revoking their access",
62
+ description: "Remove a user from a FireCMS project, revoking their access. Admin-only.",
58
63
  inputSchema: {
59
64
  projectId: z.string().describe("The Firebase project ID"),
60
65
  userId: z.string().describe("The user ID to remove"),
61
66
  },
67
+ annotations: { destructiveHint: true },
62
68
  }, async ({ projectId, userId }) => {
63
69
  try {
70
+ await api.assertAdmin(projectId);
64
71
  const result = await api.deleteUser(projectId, userId);
65
72
  return {
66
73
  content: [{ type: "text", text: `Removed user ${userId}\n\n${JSON.stringify(result, null, 2)}` }],
@@ -1 +1 @@
1
- {"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/tools/users.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/tools/users.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firecms/mcp-server",
3
- "version": "3.2.0",
3
+ "version": "3.3.0-canary.102f274",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -18,9 +18,9 @@
18
18
  "clean": "rm -rf dist"
19
19
  },
20
20
  "dependencies": {
21
- "@firecms/cli": "^3.2.0",
21
+ "@firecms/cli": "^3.3.0-canary.102f274",
22
22
  "@modelcontextprotocol/sdk": "^1.12.1",
23
- "axios": "^1.7.0",
23
+ "axios": "^1.16.1",
24
24
  "zod": "^3.24.0"
25
25
  },
26
26
  "devDependencies": {
@@ -42,7 +42,7 @@
42
42
  "license": "MIT",
43
43
  "repository": {
44
44
  "type": "git",
45
- "url": "https://github.com/FireCMSco/firecms"
46
- },
47
- "gitHead": "44dc65b20680ae61964608d755584620abcf8127"
45
+ "url": "https://github.com/FireCMSco/firecms.git",
46
+ "directory": "packages/mcp_server"
47
+ }
48
48
  }
package/src/api-client.ts CHANGED
@@ -1,14 +1,34 @@
1
1
  import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
2
- import { getValidTokens } from "./auth.js";
2
+ import { getValidTokens, getCurrentUserEmail } from "./auth.js";
3
3
 
4
4
  const API_URL = "https://api.firecms.co";
5
5
 
6
+ /**
7
+ * The Firestore path where collection schemas are persisted in the CLIENT's
8
+ * Firestore (not the SaaS backend). The backend's document CRUD endpoints
9
+ * proxy operations into the client's project Firestore.
10
+ */
11
+ const COLLECTIONS_CONFIG_PATH = "__FIRECMS/config/collections";
12
+
6
13
  /**
7
14
  * Typed HTTP client for the FireCMS Cloud backend REST API.
8
15
  * Uses the same tokens as the FireCMS CLI (from ~/.firecms/tokens.json).
16
+ *
17
+ * Architecture notes:
18
+ * - Collection schemas are stored in the CLIENT's Firestore at
19
+ * `__FIRECMS/config/collections/{collectionId}`. We use the existing
20
+ * document CRUD proxy endpoints to read/write them.
21
+ * - Project config (name, colors, locale) is stored in the SaaS backend's
22
+ * Firestore at `projects/{projectId}`. Dedicated `/config` endpoints
23
+ * handle this.
24
+ * - Bulk import uses the admin `batch_write` endpoint.
9
25
  */
10
26
  export class FireCMSApiClient {
11
27
  private client: AxiosInstance;
28
+ private adminCache: Map<string, { isAdmin: boolean; checkedAt: number }> = new Map();
29
+
30
+ /** Cache admin checks for 5 minutes */
31
+ private static ADMIN_CACHE_TTL_MS = 5 * 60 * 1000;
12
32
 
13
33
  constructor() {
14
34
  this.client = axios.create({
@@ -38,6 +58,44 @@ export class FireCMSApiClient {
38
58
  return response.data;
39
59
  }
40
60
 
61
+ // ─── Admin guard ──────────────────────────────────────────
62
+
63
+ /**
64
+ * Verify that the current user is an admin of the given project.
65
+ * Results are cached for 5 minutes per project.
66
+ * @throws Error if the user is not an admin.
67
+ */
68
+ async assertAdmin(projectId: string): Promise<void> {
69
+ const cached = this.adminCache.get(projectId);
70
+ if (cached && (Date.now() - cached.checkedAt) < FireCMSApiClient.ADMIN_CACHE_TTL_MS) {
71
+ if (!cached.isAdmin) {
72
+ throw this.notAdminError(projectId);
73
+ }
74
+ return;
75
+ }
76
+
77
+ const users = await this.listUsers(projectId);
78
+ const currentEmail = getCurrentUserEmail();
79
+ const me = (users as any[]).find((u: any) =>
80
+ u.email?.toLowerCase() === currentEmail?.toLowerCase()
81
+ );
82
+ const isAdmin = me?.roles?.includes("admin") ?? false;
83
+
84
+ this.adminCache.set(projectId, { isAdmin, checkedAt: Date.now() });
85
+
86
+ if (!isAdmin) {
87
+ throw this.notAdminError(projectId);
88
+ }
89
+ }
90
+
91
+ private notAdminError(projectId: string): Error {
92
+ const email = getCurrentUserEmail() ?? "unknown";
93
+ return new Error(
94
+ `Access denied: ${email} is not an admin of project "${projectId}". ` +
95
+ `The FireCMS MCP server requires admin access for this operation.`
96
+ );
97
+ }
98
+
41
99
  // ─── Projects ──────────────────────────────────────────
42
100
 
43
101
  async listProjects(): Promise<any> {
@@ -51,6 +109,23 @@ export class FireCMSApiClient {
51
109
  });
52
110
  }
53
111
 
112
+ // ─── Project Config (SaaS backend Firestore) ──────────
113
+
114
+ async getProjectConfig(projectId: string): Promise<any> {
115
+ return this.request({
116
+ method: "GET",
117
+ url: `/projects/${projectId}/config`,
118
+ });
119
+ }
120
+
121
+ async updateProjectConfig(projectId: string, data: Record<string, any>): Promise<any> {
122
+ return this.request({
123
+ method: "PATCH",
124
+ url: `/projects/${projectId}/config`,
125
+ data,
126
+ });
127
+ }
128
+
54
129
  // ─── Users ─────────────────────────────────────────────
55
130
 
56
131
  async listUsers(projectId: string): Promise<any> {
@@ -77,7 +152,129 @@ export class FireCMSApiClient {
77
152
  return this.request({ method: "DELETE", url: `/projects/${projectId}/users/${userId}` });
78
153
  }
79
154
 
80
- // ─── Collections ───────────────────────────────────────
155
+ // ─── Collection Schemas (client Firestore via document proxy) ───────
156
+
157
+ /**
158
+ * List all persisted collection schemas.
159
+ * Uses the document list endpoint to read from `__FIRECMS/config/collections`.
160
+ */
161
+ async listCollectionSchemas(projectId: string): Promise<any[]> {
162
+ const response: any = await this.request({
163
+ method: "POST",
164
+ url: `/projects/${projectId}/documents/list`,
165
+ data: { path: COLLECTIONS_CONFIG_PATH, limit: 500 },
166
+ });
167
+ // The document list endpoint returns { data: Document[] } or { documents: Document[] }
168
+ return response?.data ?? response?.documents ?? [];
169
+ }
170
+
171
+ /**
172
+ * Get a single collection schema by its document ID.
173
+ */
174
+ async getCollectionSchema(projectId: string, collectionId: string): Promise<any> {
175
+ return this.request({
176
+ method: "POST",
177
+ url: `/projects/${projectId}/documents/get`,
178
+ data: { path: COLLECTIONS_CONFIG_PATH, documentId: collectionId },
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Create or fully replace a collection schema.
184
+ * Uses the document create/update endpoints to write to `__FIRECMS/config/collections/{id}`.
185
+ */
186
+ async saveCollectionSchema(projectId: string, collectionId: string, schema: Record<string, any>): Promise<any> {
187
+ // Try update first (if doc exists), fall back to create
188
+ try {
189
+ return await this.request({
190
+ method: "POST",
191
+ url: `/projects/${projectId}/documents/update`,
192
+ data: {
193
+ path: COLLECTIONS_CONFIG_PATH,
194
+ documentId: collectionId,
195
+ data: { ...schema, id: collectionId },
196
+ },
197
+ });
198
+ } catch (error: any) {
199
+ // If the document doesn't exist, create it
200
+ if (error.response?.status === 404) {
201
+ return this.request({
202
+ method: "POST",
203
+ url: `/projects/${projectId}/documents/create`,
204
+ data: {
205
+ path: COLLECTIONS_CONFIG_PATH,
206
+ documentId: collectionId,
207
+ data: { ...schema, id: collectionId },
208
+ },
209
+ });
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Partially update an existing collection schema (merge).
217
+ */
218
+ async updateCollectionSchema(projectId: string, collectionId: string, data: Record<string, any>): Promise<any> {
219
+ return this.request({
220
+ method: "POST",
221
+ url: `/projects/${projectId}/documents/update`,
222
+ data: {
223
+ path: COLLECTIONS_CONFIG_PATH,
224
+ documentId: collectionId,
225
+ data,
226
+ },
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Delete a collection schema.
232
+ */
233
+ async deleteCollectionSchema(projectId: string, collectionId: string): Promise<any> {
234
+ return this.request({
235
+ method: "POST",
236
+ url: `/projects/${projectId}/documents/delete`,
237
+ data: {
238
+ path: COLLECTIONS_CONFIG_PATH,
239
+ documentId: collectionId,
240
+ },
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Add or update a single property within a collection schema.
246
+ * Reads the current schema, modifies the property, and writes back.
247
+ */
248
+ async saveProperty(projectId: string, collectionId: string, propertyKey: string, property: Record<string, any>, namespace?: string): Promise<any> {
249
+ // Get current schema
250
+ const current: any = await this.getCollectionSchema(projectId, collectionId);
251
+ const schemaData = current?.data ?? current ?? {};
252
+
253
+ // Build the properties map
254
+ const properties = schemaData.properties ?? {};
255
+ const key = namespace ? `${namespace}:${propertyKey}` : propertyKey;
256
+ properties[key] = property;
257
+
258
+ // Write back
259
+ return this.updateCollectionSchema(projectId, collectionId, { properties });
260
+ }
261
+
262
+ /**
263
+ * Delete a property from a collection schema.
264
+ * Reads the current schema, removes the property, and writes back.
265
+ */
266
+ async deleteProperty(projectId: string, collectionId: string, propertyKey: string, namespace?: string): Promise<any> {
267
+ const current: any = await this.getCollectionSchema(projectId, collectionId);
268
+ const schemaData = current?.data ?? current ?? {};
269
+
270
+ const properties = schemaData.properties ?? {};
271
+ const key = namespace ? `${namespace}:${propertyKey}` : propertyKey;
272
+ delete properties[key];
273
+
274
+ return this.updateCollectionSchema(projectId, collectionId, { properties });
275
+ }
276
+
277
+ // ─── AI Collection Generation ──────────────────────────
81
278
 
82
279
  async generateCollection(prompt: string, existingCollections: any[] = [], existingCollection?: any): Promise<any> {
83
280
  return this.request({
@@ -87,7 +284,7 @@ export class FireCMSApiClient {
87
284
  });
88
285
  }
89
286
 
90
- // ─── Documents (Firestore CRUD via backend) ─────────────
287
+ // ─── Documents (Firestore CRUD via backend proxy) ──────
91
288
 
92
289
  async listDocuments(projectId: string, body: {
93
290
  path: string;
@@ -143,4 +340,46 @@ export class FireCMSApiClient {
143
340
  data: { path, databaseId },
144
341
  });
145
342
  }
343
+
344
+ // ─── Data Import (admin batch_write) ────────────────────
345
+
346
+ /**
347
+ * Bulk import documents into a collection using the admin batch_write endpoint.
348
+ * This writes directly to the client's Firestore via the delegated service account.
349
+ */
350
+ async importDocuments(projectId: string, body: {
351
+ path: string;
352
+ documents: Array<{ id?: string; data: Record<string, any> }>;
353
+ merge?: boolean;
354
+ databaseId?: string;
355
+ }): Promise<any> {
356
+ // Transform documents into BatchOperation format expected by the backend
357
+ const operations = body.documents.map(doc => ({
358
+ type: (body.merge ? "update" : "set") as "set" | "update",
359
+ path: body.path,
360
+ documentId: doc.id ?? this.generateId(),
361
+ data: doc.data,
362
+ }));
363
+
364
+ return this.request({
365
+ method: "POST",
366
+ url: `/projects/${projectId}/admin/documents/batch_write`,
367
+ data: {
368
+ operations,
369
+ databaseId: body.databaseId,
370
+ },
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Generate a random Firestore-style document ID.
376
+ */
377
+ private generateId(): string {
378
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
379
+ let id = "";
380
+ for (let i = 0; i < 20; i++) {
381
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
382
+ }
383
+ return id;
384
+ }
146
385
  }
@@ -3,9 +3,12 @@ import { FireCMSApiClient } from "../api-client.js";
3
3
 
4
4
  /**
5
5
  * Register MCP resources — read-only contextual data about FireCMS projects.
6
+ * Resources provide ambient context to the LLM without requiring explicit tool calls.
6
7
  */
7
8
  export function registerProjectResources(server: McpServer, api: FireCMSApiClient) {
8
9
 
10
+ // ─── Root collections ──────────────────────────────────
11
+
9
12
  server.registerResource(
10
13
  "project-collections",
11
14
  new ResourceTemplate("firecms://projects/{projectId}/collections", { list: undefined }),
@@ -36,6 +39,8 @@ export function registerProjectResources(server: McpServer, api: FireCMSApiClien
36
39
  }
37
40
  );
38
41
 
42
+ // ─── Project users ─────────────────────────────────────
43
+
39
44
  server.registerResource(
40
45
  "project-users",
41
46
  new ResourceTemplate("firecms://projects/{projectId}/users", { list: undefined }),
@@ -65,4 +70,68 @@ export function registerProjectResources(server: McpServer, api: FireCMSApiClien
65
70
  }
66
71
  }
67
72
  );
73
+
74
+ // ─── Collection schemas (full schema tree) ─────────────
75
+
76
+ server.registerResource(
77
+ "project-schemas",
78
+ new ResourceTemplate("firecms://projects/{projectId}/schemas", { list: undefined }),
79
+ {
80
+ description: "All persisted collection schemas for a FireCMS project — the full configuration tree including properties, validation rules, and display settings",
81
+ mimeType: "application/json",
82
+ },
83
+ async (uri, variables) => {
84
+ const projectId = variables.projectId as string;
85
+ try {
86
+ const schemas = await api.listCollectionSchemas(projectId);
87
+ return {
88
+ contents: [{
89
+ uri: uri.href,
90
+ mimeType: "application/json",
91
+ text: JSON.stringify(schemas, null, 2),
92
+ }],
93
+ };
94
+ } catch (error: any) {
95
+ return {
96
+ contents: [{
97
+ uri: uri.href,
98
+ mimeType: "application/json",
99
+ text: JSON.stringify({ error: error.message }),
100
+ }],
101
+ };
102
+ }
103
+ }
104
+ );
105
+
106
+ // ─── Project configuration ─────────────────────────────
107
+
108
+ server.registerResource(
109
+ "project-config",
110
+ new ResourceTemplate("firecms://projects/{projectId}/config", { list: undefined }),
111
+ {
112
+ description: "Project configuration — name, colors, subscription plan, feature toggles, and locale settings",
113
+ mimeType: "application/json",
114
+ },
115
+ async (uri, variables) => {
116
+ const projectId = variables.projectId as string;
117
+ try {
118
+ const config = await api.getProjectConfig(projectId);
119
+ return {
120
+ contents: [{
121
+ uri: uri.href,
122
+ mimeType: "application/json",
123
+ text: JSON.stringify(config, null, 2),
124
+ }],
125
+ };
126
+ } catch (error: any) {
127
+ return {
128
+ contents: [{
129
+ uri: uri.href,
130
+ mimeType: "application/json",
131
+ text: JSON.stringify({ error: error.message }),
132
+ }],
133
+ };
134
+ }
135
+ }
136
+ );
68
137
  }
package/src/server.ts CHANGED
@@ -4,17 +4,32 @@ import { registerAuthTools } from "./tools/auth.js";
4
4
  import { registerProjectTools } from "./tools/projects.js";
5
5
  import { registerUserTools } from "./tools/users.js";
6
6
  import { registerCollectionTools } from "./tools/collections.js";
7
+ import { registerCollectionSchemaTools } from "./tools/collection-schemas.js";
7
8
  import { registerDocumentTools } from "./tools/documents.js";
8
9
  import { registerExportTools } from "./tools/export.js";
10
+ import { registerImportTools } from "./tools/import.js";
11
+ import { registerProjectConfigTools } from "./tools/project-config.js";
9
12
  import { registerProjectResources } from "./resources/project.js";
10
13
 
11
14
  /**
12
15
  * Create and configure the FireCMS MCP server with all tools and resources.
16
+ *
17
+ * Tool categories:
18
+ * - Auth: Login/logout/current user
19
+ * - Projects: List projects, root collections
20
+ * - Project Config: Name, colors, locale, feature toggles (admin-only)
21
+ * - Users: User management (invite, roles, remove)
22
+ * - Collection Schemas: CRUD for collection configurations (admin-only)
23
+ * - AI Collections: AI-powered schema generation/modification
24
+ * - Documents: Firestore CRUD (list, get, create, update, delete, count)
25
+ * - Export: Data export as JSON
26
+ * - Import: Bulk data import (admin-only)
27
+ * - Resources: Read-only context (collections, users, schemas, config)
13
28
  */
14
29
  export function createFireCMSMcpServer(): McpServer {
15
30
  const server = new McpServer({
16
31
  name: "FireCMS Cloud",
17
- version: "0.1.0",
32
+ version: "0.2.0",
18
33
  });
19
34
 
20
35
  const api = new FireCMSApiClient();
@@ -26,6 +41,12 @@ export function createFireCMSMcpServer(): McpServer {
26
41
  registerProjectTools(server, api);
27
42
  registerUserTools(server, api);
28
43
 
44
+ // Project configuration (admin-only)
45
+ registerProjectConfigTools(server, api);
46
+
47
+ // Collection schema CRUD (admin-only — the core feature for agent-driven CMS management)
48
+ registerCollectionSchemaTools(server, api);
49
+
29
50
  // Collection schema AI tools (via backend API)
30
51
  registerCollectionTools(server, api);
31
52
 
@@ -35,7 +56,10 @@ export function createFireCMSMcpServer(): McpServer {
35
56
  // Data export (via backend API)
36
57
  registerExportTools(server, api);
37
58
 
38
- // Resources
59
+ // Data import (admin-only)
60
+ registerImportTools(server, api);
61
+
62
+ // Resources (read-only contextual data)
39
63
  registerProjectResources(server, api);
40
64
 
41
65
  return server;