@firecms/mcp-server 3.3.0-canary.451aa49 → 3.3.0-canary.7e3431b
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 +86 -7
- package/dist/api-client.d.ts +70 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +207 -3
- package/dist/api-client.js.map +1 -1
- package/dist/resources/project.d.ts +1 -0
- package/dist/resources/project.d.ts.map +1 -1
- package/dist/resources/project.js +55 -0
- package/dist/resources/project.js.map +1 -1
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +23 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/collection-schemas.d.ts +8 -0
- package/dist/tools/collection-schemas.d.ts.map +1 -0
- package/dist/tools/collection-schemas.js +278 -0
- package/dist/tools/collection-schemas.js.map +1 -0
- package/dist/tools/import.d.ts +8 -0
- package/dist/tools/import.d.ts.map +1 -0
- package/dist/tools/import.js +57 -0
- package/dist/tools/import.js.map +1 -0
- package/dist/tools/project-config.d.ts +8 -0
- package/dist/tools/project-config.d.ts.map +1 -0
- package/dist/tools/project-config.js +174 -0
- package/dist/tools/project-config.js.map +1 -0
- package/dist/tools/users.d.ts +2 -0
- package/dist/tools/users.d.ts.map +1 -1
- package/dist/tools/users.js +10 -3
- package/dist/tools/users.js.map +1 -1
- package/package.json +6 -6
- package/src/api-client.ts +242 -3
- package/src/resources/project.ts +69 -0
- package/src/server.ts +26 -2
- package/src/tools/collection-schemas.ts +316 -0
- package/src/tools/import.ts +65 -0
- package/src/tools/project-config.ts +202 -0
- package/src/tools/users.ts +10 -3
- package/LICENSE +0 -6
package/dist/tools/users.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/tools/users.js
CHANGED
|
@@ -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)}` }],
|
package/dist/tools/users.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/tools/users.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB
|
|
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.3.0-canary.
|
|
3
|
+
"version": "3.3.0-canary.7e3431b",
|
|
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.3.0-canary.
|
|
21
|
+
"@firecms/cli": "^3.3.0-canary.7e3431b",
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
23
|
-
"axios": "^1.
|
|
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
|
-
|
|
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
|
-
// ───
|
|
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
|
}
|
package/src/resources/project.ts
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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;
|