firebase-tools 14.11.2 → 14.12.1
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/lib/api.js +3 -1
- package/lib/command.js +9 -3
- package/lib/commands/firestore-databases-create.js +11 -0
- package/lib/commands/init.js +7 -5
- package/lib/commands/internaltesting-functions-discover.js +20 -5
- package/lib/commands/use.js +5 -0
- package/lib/crashlytics/buildToolsJarHelper.js +1 -2
- package/lib/dataconnect/ensureApis.js +1 -0
- package/lib/deploy/dataconnect/prepare.js +2 -2
- package/lib/deploy/dataconnect/release.js +2 -2
- package/lib/deploy/firestore/deploy.js +10 -0
- package/lib/deploy/functions/prepare.js +5 -5
- package/lib/deploy/functions/prepareFunctionsUpload.js +3 -1
- package/lib/emulator/downloadableEmulatorInfo.json +18 -18
- package/lib/firestore/api-sort.js +96 -3
- package/lib/firestore/api-types.js +20 -1
- package/lib/firestore/api.js +68 -1
- package/lib/firestore/pretty-print.js +5 -1
- package/lib/firestore/validator.js +1 -1
- package/lib/functions/deprecationWarnings.js +4 -4
- package/lib/gcp/cloudsql/connect.js +1 -1
- package/lib/init/features/aitools/claude.js +7 -7
- package/lib/init/features/dataconnect/index.js +1 -1
- package/lib/init/features/dataconnect/sdk.js +2 -3
- package/lib/init/features/index.js +3 -1
- package/lib/init/index.js +8 -0
- package/lib/management/studio.js +120 -0
- package/lib/mcp/index.js +75 -2
- package/lib/mcp/prompt.js +10 -0
- package/lib/mcp/prompts/core/deploy.js +58 -0
- package/lib/mcp/prompts/core/index.js +5 -0
- package/lib/mcp/prompts/index.js +45 -0
- package/lib/mcp/tools/core/consult_assistant.js +7 -2
- package/lib/mcp/tools/core/get_sdk_config.js +10 -0
- package/lib/mcp/tools/core/init.js +1 -1
- package/lib/mcp/tools/database/get_data.js +49 -0
- package/lib/mcp/tools/database/get_rules.js +39 -0
- package/lib/mcp/tools/database/index.js +8 -0
- package/lib/mcp/tools/database/set_data.js +57 -0
- package/lib/mcp/tools/database/set_rules.js +41 -0
- package/lib/mcp/tools/database/validate_rules.js +41 -0
- package/lib/mcp/tools/index.js +4 -1
- package/lib/mcp/tools/rules/get_rules.js +1 -1
- package/lib/mcp/types.js +2 -0
- package/lib/mcp/util.js +2 -0
- package/lib/requireAuth.js +5 -1
- package/lib/rtdb.js +10 -6
- package/lib/scopes.js +2 -1
- package/lib/utils.js +24 -1
- package/package.json +1 -1
- package/prompts/FIREBASE.md +1 -2
- package/schema/firebase-config.json +3 -0
- package/templates/init/firestore/firestore.indexes.json +26 -1
package/lib/firestore/api.js
CHANGED
|
@@ -160,6 +160,10 @@ class FirestoreApi {
|
|
|
160
160
|
collectionGroup: util.parseIndexName(index.name).collectionGroupId,
|
|
161
161
|
queryScope: index.queryScope,
|
|
162
162
|
fields: index.fields,
|
|
163
|
+
apiScope: index.apiScope,
|
|
164
|
+
density: index.density,
|
|
165
|
+
multikey: index.multikey,
|
|
166
|
+
unique: index.unique,
|
|
163
167
|
};
|
|
164
168
|
});
|
|
165
169
|
if (!fields) {
|
|
@@ -179,6 +183,10 @@ class FirestoreApi {
|
|
|
179
183
|
order: firstField.order,
|
|
180
184
|
arrayConfig: firstField.arrayConfig,
|
|
181
185
|
queryScope: index.queryScope,
|
|
186
|
+
apiScope: index.apiScope,
|
|
187
|
+
density: index.density,
|
|
188
|
+
multikey: index.multikey,
|
|
189
|
+
unique: index.unique,
|
|
182
190
|
};
|
|
183
191
|
}),
|
|
184
192
|
};
|
|
@@ -205,6 +213,18 @@ class FirestoreApi {
|
|
|
205
213
|
validator.assertHas(index, "collectionGroup");
|
|
206
214
|
validator.assertHas(index, "queryScope");
|
|
207
215
|
validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope));
|
|
216
|
+
if (index.apiScope) {
|
|
217
|
+
validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope));
|
|
218
|
+
}
|
|
219
|
+
if (index.density) {
|
|
220
|
+
validator.assertEnum(index, "density", Object.keys(types.Density));
|
|
221
|
+
}
|
|
222
|
+
if (index.multikey) {
|
|
223
|
+
validator.assertType("multikey", index.multikey, "boolean");
|
|
224
|
+
}
|
|
225
|
+
if (index.unique) {
|
|
226
|
+
validator.assertType("unique", index.unique, "boolean");
|
|
227
|
+
}
|
|
208
228
|
validator.assertHas(index, "fields");
|
|
209
229
|
index.fields.forEach((field) => {
|
|
210
230
|
validator.assertHas(field, "fieldPath");
|
|
@@ -239,6 +259,18 @@ class FirestoreApi {
|
|
|
239
259
|
if (index.queryScope) {
|
|
240
260
|
validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope));
|
|
241
261
|
}
|
|
262
|
+
if (index.apiScope) {
|
|
263
|
+
validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope));
|
|
264
|
+
}
|
|
265
|
+
if (index.density) {
|
|
266
|
+
validator.assertEnum(index, "density", Object.keys(types.Density));
|
|
267
|
+
}
|
|
268
|
+
if (index.multikey) {
|
|
269
|
+
validator.assertType("multikey", index.multikey, "boolean");
|
|
270
|
+
}
|
|
271
|
+
if (index.unique) {
|
|
272
|
+
validator.assertType("unique", index.unique, "boolean");
|
|
273
|
+
}
|
|
242
274
|
});
|
|
243
275
|
}
|
|
244
276
|
async patchField(project, spec, databaseId = "(default)") {
|
|
@@ -246,6 +278,10 @@ class FirestoreApi {
|
|
|
246
278
|
const indexes = spec.indexes.map((index) => {
|
|
247
279
|
return {
|
|
248
280
|
queryScope: index.queryScope,
|
|
281
|
+
apiScope: index.apiScope,
|
|
282
|
+
density: index.density,
|
|
283
|
+
multikey: index.multikey,
|
|
284
|
+
unique: index.unique,
|
|
249
285
|
fields: [
|
|
250
286
|
{
|
|
251
287
|
fieldPath: spec.fieldPath,
|
|
@@ -282,6 +318,10 @@ class FirestoreApi {
|
|
|
282
318
|
return this.apiClient.post(url, {
|
|
283
319
|
fields: index.fields,
|
|
284
320
|
queryScope: index.queryScope,
|
|
321
|
+
apiScope: index.apiScope,
|
|
322
|
+
density: index.density,
|
|
323
|
+
multikey: index.multikey,
|
|
324
|
+
unique: index.unique,
|
|
285
325
|
});
|
|
286
326
|
}
|
|
287
327
|
deleteIndex(index) {
|
|
@@ -296,6 +336,18 @@ class FirestoreApi {
|
|
|
296
336
|
if (index.queryScope !== spec.queryScope) {
|
|
297
337
|
return false;
|
|
298
338
|
}
|
|
339
|
+
if (index.apiScope !== spec.apiScope) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (index.density !== spec.density) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
if (index.multikey !== spec.multikey) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (index.unique !== spec.unique) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
299
351
|
if (index.fields.length !== spec.fields.length) {
|
|
300
352
|
return false;
|
|
301
353
|
}
|
|
@@ -312,6 +364,9 @@ class FirestoreApi {
|
|
|
312
364
|
if (iField.arrayConfig !== sField.arrayConfig) {
|
|
313
365
|
return false;
|
|
314
366
|
}
|
|
367
|
+
if (iField.vectorConfig !== sField.vectorConfig) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
315
370
|
i++;
|
|
316
371
|
}
|
|
317
372
|
return true;
|
|
@@ -368,8 +423,19 @@ class FirestoreApi {
|
|
|
368
423
|
const i = {
|
|
369
424
|
collectionGroup: index.collectionGroup || index.collectionId,
|
|
370
425
|
queryScope: index.queryScope || types.QueryScope.COLLECTION,
|
|
371
|
-
fields: [],
|
|
372
426
|
};
|
|
427
|
+
if (index.apiScope) {
|
|
428
|
+
i.apiScope = index.apiScope;
|
|
429
|
+
}
|
|
430
|
+
if (index.density) {
|
|
431
|
+
i.density = index.density;
|
|
432
|
+
}
|
|
433
|
+
if (index.multikey !== undefined) {
|
|
434
|
+
i.multikey = index.multikey;
|
|
435
|
+
}
|
|
436
|
+
if (index.unique !== undefined) {
|
|
437
|
+
i.unique = index.unique;
|
|
438
|
+
}
|
|
373
439
|
if (index.fields) {
|
|
374
440
|
i.fields = index.fields.map((field) => {
|
|
375
441
|
const f = {
|
|
@@ -429,6 +495,7 @@ class FirestoreApi {
|
|
|
429
495
|
const payload = {
|
|
430
496
|
locationId: req.locationId,
|
|
431
497
|
type: req.type,
|
|
498
|
+
databaseEdition: req.databaseEdition,
|
|
432
499
|
deleteProtectionState: req.deleteProtectionState,
|
|
433
500
|
pointInTimeRecoveryEnablement: req.pointInTimeRecoveryEnablement,
|
|
434
501
|
cmekConfig: req.cmekConfig,
|
|
@@ -41,7 +41,11 @@ class PrettyPrint {
|
|
|
41
41
|
head: ["Field", "Value"],
|
|
42
42
|
colWidths: [30, colValueWidth],
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
const edition = !database.databaseEdition ||
|
|
45
|
+
database.databaseEdition === types.DatabaseEdition.DATABASE_EDITION_UNSPECIFIED
|
|
46
|
+
? types.DatabaseEdition.STANDARD
|
|
47
|
+
: database.databaseEdition;
|
|
48
|
+
table.push(["Name", clc.yellow(database.name)], ["Create Time", clc.yellow(database.createTime)], ["Last Update Time", clc.yellow(database.updateTime)], ["Type", clc.yellow(database.type)], ["Edition", clc.yellow(edition)], ["Location", clc.yellow(database.locationId)], ["Delete Protection State", clc.yellow(database.deleteProtectionState)], ["Point In Time Recovery", clc.yellow(database.pointInTimeRecoveryEnablement)], ["Earliest Version Time", clc.yellow(database.earliestVersionTime)], ["Version Retention Period", clc.yellow(database.versionRetentionPeriod)]);
|
|
45
49
|
if (database.cmekConfig) {
|
|
46
50
|
table.push(["KMS Key Name", clc.yellow(database.cmekConfig.kmsKeyName)]);
|
|
47
51
|
if (database.cmekConfig.activeKeyVersion) {
|
|
@@ -26,7 +26,7 @@ exports.assertHasOneOf = assertHasOneOf;
|
|
|
26
26
|
function assertEnum(obj, prop, valid) {
|
|
27
27
|
const objString = clc.cyan(JSON.stringify(obj));
|
|
28
28
|
if (valid.indexOf(obj[prop]) < 0) {
|
|
29
|
-
throw new error_1.FirebaseError(`Field "${prop}" must be one of
|
|
29
|
+
throw new error_1.FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
exports.assertEnum = assertEnum;
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.logFunctionsConfigDeprecationWarning = void 0;
|
|
4
4
|
const utils_1 = require("../utils");
|
|
5
|
-
const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required to deploy after
|
|
5
|
+
const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required to deploy after March 2026
|
|
6
6
|
|
|
7
7
|
functions.config() API is deprecated.
|
|
8
|
-
Cloud Runtime Configuration API, the Google Cloud service used to store function configuration data, will be shut down
|
|
8
|
+
Cloud Runtime Configuration API, the Google Cloud service used to store function configuration data, will be shut down in March 2026. As a result, you must migrate away from using functions.config() to continue deploying your functions after March 2026.
|
|
9
9
|
|
|
10
10
|
What this means for you:
|
|
11
11
|
|
|
12
|
-
- The Firebase CLI commands for managing this configuration (functions:config:set, get, unset, clone, and export) are deprecated. These commands no longer work after
|
|
13
|
-
- firebase deploy command will fail for functions that use the legacy functions.config() API after
|
|
12
|
+
- The Firebase CLI commands for managing this configuration (functions:config:set, get, unset, clone, and export) are deprecated. These commands will no longer work after March 2026.
|
|
13
|
+
- firebase deploy command will fail for functions that use the legacy functions.config() API after March 2026.
|
|
14
14
|
|
|
15
15
|
Existing deployments will continue to work with their current configuration.
|
|
16
16
|
|
|
@@ -103,7 +103,7 @@ exports.executeSqlCmdsAsIamUser = executeSqlCmdsAsIamUser;
|
|
|
103
103
|
async function executeSqlCmdsAsSuperUser(options, instanceId, databaseId, cmds, silent = false, transaction = false) {
|
|
104
104
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
105
105
|
const superuser = "firebasesuperuser";
|
|
106
|
-
const temporaryPassword = utils.
|
|
106
|
+
const temporaryPassword = utils.generatePassword(20);
|
|
107
107
|
await cloudSqlAdminClient.createUser(projectId, instanceId, "BUILT_IN", superuser, temporaryPassword);
|
|
108
108
|
return await execute([`SET ROLE = '${superuser}'`, ...cmds], {
|
|
109
109
|
projectId,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.claude = void 0;
|
|
4
4
|
const promptUpdater_1 = require("./promptUpdater");
|
|
5
|
-
const
|
|
6
|
-
const CLAUDE_PROMPT_PATH = "CLAUDE.
|
|
5
|
+
const MCP_CONFIG_PATH = ".mcp.json";
|
|
6
|
+
const CLAUDE_PROMPT_PATH = "CLAUDE.md";
|
|
7
7
|
exports.claude = {
|
|
8
8
|
name: "claude",
|
|
9
9
|
displayName: "Claude Code",
|
|
@@ -11,9 +11,9 @@ exports.claude = {
|
|
|
11
11
|
var _a;
|
|
12
12
|
const files = [];
|
|
13
13
|
let existingConfig = {};
|
|
14
|
-
let
|
|
14
|
+
let mcpUpdated = false;
|
|
15
15
|
try {
|
|
16
|
-
const existingContent = config.readProjectFile(
|
|
16
|
+
const existingContent = config.readProjectFile(MCP_CONFIG_PATH);
|
|
17
17
|
if (existingContent) {
|
|
18
18
|
existingConfig = JSON.parse(existingContent);
|
|
19
19
|
}
|
|
@@ -28,10 +28,10 @@ exports.claude = {
|
|
|
28
28
|
command: "npx",
|
|
29
29
|
args: ["-y", "firebase-tools", "experimental:mcp", "--dir", projectPath],
|
|
30
30
|
};
|
|
31
|
-
config.writeProjectFile(
|
|
32
|
-
|
|
31
|
+
config.writeProjectFile(MCP_CONFIG_PATH, JSON.stringify(existingConfig, null, 2));
|
|
32
|
+
mcpUpdated = true;
|
|
33
33
|
}
|
|
34
|
-
files.push({ path:
|
|
34
|
+
files.push({ path: MCP_CONFIG_PATH, updated: mcpUpdated });
|
|
35
35
|
const { updated } = await (0, promptUpdater_1.updateFirebaseSection)(config, CLAUDE_PROMPT_PATH, enabledFeatures, {
|
|
36
36
|
interactive: true,
|
|
37
37
|
});
|
|
@@ -107,7 +107,7 @@ async function actuate(setup, config, options) {
|
|
|
107
107
|
info.connectors = [defaultConnector];
|
|
108
108
|
}
|
|
109
109
|
await writeFiles(config, info, options);
|
|
110
|
-
if (setup.projectId && info.shouldProvisionCSQL) {
|
|
110
|
+
if (setup.projectId && info.shouldProvisionCSQL && (await (0, cloudbilling_1.isBillingEnabled)(setup))) {
|
|
111
111
|
await (0, provisionCloudSql_1.provisionCloudSql)({
|
|
112
112
|
projectId: setup.projectId,
|
|
113
113
|
location: info.locationId,
|
|
@@ -75,8 +75,7 @@ async function askQuestions(setup, config) {
|
|
|
75
75
|
const unusedFrameworks = fileUtils_1.SUPPORTED_FRAMEWORKS.filter((framework) => { var _a; return !((_a = newConnectorYaml.generate) === null || _a === void 0 ? void 0 : _a.javascriptSdk[framework]); });
|
|
76
76
|
if (unusedFrameworks.length > 0) {
|
|
77
77
|
const additionalFrameworks = await (0, prompt_1.checkbox)({
|
|
78
|
-
message: "Which frameworks would you like to generate SDKs for? "
|
|
79
|
-
"Press Space to select features, then Enter to confirm your choices.",
|
|
78
|
+
message: "Which frameworks would you like to generate SDKs for in addition to the TypeScript SDK? Press Enter to skip.\n",
|
|
80
79
|
choices: fileUtils_1.SUPPORTED_FRAMEWORKS.map((frameworkStr) => {
|
|
81
80
|
var _a, _b;
|
|
82
81
|
return ({
|
|
@@ -170,7 +169,7 @@ async function actuate(sdkInfo, config) {
|
|
|
170
169
|
var _a, _b;
|
|
171
170
|
const connectorYamlPath = `${sdkInfo.connectorInfo.directory}/connector.yaml`;
|
|
172
171
|
(0, utils_1.logBullet)(`Writing your new SDK configuration to ${connectorYamlPath}`);
|
|
173
|
-
|
|
172
|
+
config.writeProjectFile(path.relative(config.projectDir, connectorYamlPath), sdkInfo.connectorYamlContents);
|
|
174
173
|
const account = (0, auth_1.getGlobalDefaultAccount)();
|
|
175
174
|
await dataconnectEmulator_1.DataConnectEmulator.generate({
|
|
176
175
|
configDir: sdkInfo.connectorInfo.directory,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.apptestingAcutate = exports.apptestingAskQuestions = exports.genkit = exports.apphosting = exports.dataconnectSdk = exports.dataconnectPostSetup = exports.dataconnectActuate = exports.dataconnectAskQuestions = exports.hostingGithub = exports.remoteconfig = exports.project = exports.extensions = exports.emulators = exports.storageActuate = exports.storageAskQuestions = exports.hosting = exports.functions = exports.firestoreActuate = exports.firestoreAskQuestions = exports.databaseActuate = exports.databaseAskQuestions = exports.account = void 0;
|
|
3
|
+
exports.aitools = exports.apptestingAcutate = exports.apptestingAskQuestions = exports.genkit = exports.apphosting = exports.dataconnectSdk = exports.dataconnectPostSetup = exports.dataconnectActuate = exports.dataconnectAskQuestions = exports.hostingGithub = exports.remoteconfig = exports.project = exports.extensions = exports.emulators = exports.storageActuate = exports.storageAskQuestions = exports.hosting = exports.functions = exports.firestoreActuate = exports.firestoreAskQuestions = exports.databaseActuate = exports.databaseAskQuestions = exports.account = void 0;
|
|
4
4
|
var account_1 = require("./account");
|
|
5
5
|
Object.defineProperty(exports, "account", { enumerable: true, get: function () { return account_1.doSetup; } });
|
|
6
6
|
var database_1 = require("./database");
|
|
@@ -39,3 +39,5 @@ Object.defineProperty(exports, "genkit", { enumerable: true, get: function () {
|
|
|
39
39
|
var apptesting_1 = require("./apptesting");
|
|
40
40
|
Object.defineProperty(exports, "apptestingAskQuestions", { enumerable: true, get: function () { return apptesting_1.askQuestions; } });
|
|
41
41
|
Object.defineProperty(exports, "apptestingAcutate", { enumerable: true, get: function () { return apptesting_1.actuate; } });
|
|
42
|
+
var aitools_1 = require("./aitools");
|
|
43
|
+
Object.defineProperty(exports, "aitools", { enumerable: true, get: function () { return aitools_1.doSetup; } });
|
package/lib/init/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const clc = require("colorette");
|
|
|
6
6
|
const error_1 = require("../error");
|
|
7
7
|
const logger_1 = require("../logger");
|
|
8
8
|
const features = require("./features");
|
|
9
|
+
const track_1 = require("../track");
|
|
9
10
|
const featuresList = [
|
|
10
11
|
{ name: "account", doSetup: features.account },
|
|
11
12
|
{
|
|
@@ -44,12 +45,14 @@ const featuresList = [
|
|
|
44
45
|
askQuestions: features.apptestingAskQuestions,
|
|
45
46
|
actuate: features.apptestingAcutate,
|
|
46
47
|
},
|
|
48
|
+
{ name: "aitools", displayName: "AI Tools", doSetup: features.aitools },
|
|
47
49
|
];
|
|
48
50
|
const featureMap = new Map(featuresList.map((feature) => [feature.name, feature]));
|
|
49
51
|
async function init(setup, config, options) {
|
|
50
52
|
var _a;
|
|
51
53
|
const nextFeature = (_a = setup.features) === null || _a === void 0 ? void 0 : _a.shift();
|
|
52
54
|
if (nextFeature) {
|
|
55
|
+
const start = process.uptime();
|
|
53
56
|
const f = featureMap.get(nextFeature);
|
|
54
57
|
if (!f) {
|
|
55
58
|
const availableFeatures = Object.keys(features)
|
|
@@ -72,6 +75,8 @@ async function init(setup, config, options) {
|
|
|
72
75
|
if (f.postSetup) {
|
|
73
76
|
await f.postSetup(setup, config, options);
|
|
74
77
|
}
|
|
78
|
+
const duration = Math.floor((process.uptime() - start) * 1000);
|
|
79
|
+
await (0, track_1.trackGA4)("product_init", { feature: nextFeature }, duration);
|
|
75
80
|
return init(setup, config, options);
|
|
76
81
|
}
|
|
77
82
|
}
|
|
@@ -80,6 +85,7 @@ async function actuate(setup, config, options) {
|
|
|
80
85
|
var _a;
|
|
81
86
|
const nextFeature = (_a = setup.features) === null || _a === void 0 ? void 0 : _a.shift();
|
|
82
87
|
if (nextFeature) {
|
|
88
|
+
const start = process.uptime();
|
|
83
89
|
const f = lookupFeature(nextFeature);
|
|
84
90
|
logger_1.logger.info(clc.bold(`\n${clc.white("===")} ${(0, lodash_1.capitalize)(nextFeature)} Setup Actuation`));
|
|
85
91
|
if (f.doSetup) {
|
|
@@ -90,6 +96,8 @@ async function actuate(setup, config, options) {
|
|
|
90
96
|
await f.actuate(setup, config, options);
|
|
91
97
|
}
|
|
92
98
|
}
|
|
99
|
+
const duration = Math.floor((process.uptime() - start) * 1000);
|
|
100
|
+
await (0, track_1.trackGA4)("product_init_mcp", { feature: nextFeature }, duration);
|
|
93
101
|
return actuate(setup, config, options);
|
|
94
102
|
}
|
|
95
103
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateStudioFirebaseProject = exports.reconcileStudioFirebaseProject = void 0;
|
|
4
|
+
const apiv2_1 = require("../apiv2");
|
|
5
|
+
const prompt = require("../prompt");
|
|
6
|
+
const api = require("../api");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const utils = require("../utils");
|
|
9
|
+
const configstore_1 = require("../configstore");
|
|
10
|
+
const TIMEOUT_MILLIS = 30000;
|
|
11
|
+
const studioClient = new apiv2_1.Client({
|
|
12
|
+
urlPrefix: api.studioApiOrigin(),
|
|
13
|
+
apiVersion: "v1",
|
|
14
|
+
});
|
|
15
|
+
async function reconcileStudioFirebaseProject(options, activeProjectFromConfig) {
|
|
16
|
+
const studioWorkspace = await getStudioWorkspace();
|
|
17
|
+
if (!studioWorkspace) {
|
|
18
|
+
return activeProjectFromConfig;
|
|
19
|
+
}
|
|
20
|
+
if (!studioWorkspace.firebaseProjectId) {
|
|
21
|
+
if (activeProjectFromConfig) {
|
|
22
|
+
await updateStudioFirebaseProject(activeProjectFromConfig);
|
|
23
|
+
}
|
|
24
|
+
return activeProjectFromConfig;
|
|
25
|
+
}
|
|
26
|
+
if (!activeProjectFromConfig) {
|
|
27
|
+
await writeStudioProjectToConfigStore(options, studioWorkspace.firebaseProjectId);
|
|
28
|
+
return studioWorkspace.firebaseProjectId;
|
|
29
|
+
}
|
|
30
|
+
if (studioWorkspace.firebaseProjectId !== activeProjectFromConfig && !options.nonInteractive) {
|
|
31
|
+
const choices = [
|
|
32
|
+
{
|
|
33
|
+
name: `Set ${studioWorkspace.firebaseProjectId} from Firebase Studio as my active project in both places`,
|
|
34
|
+
value: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: `Set ${activeProjectFromConfig} from Firebase CLI as my active project in both places`,
|
|
38
|
+
value: true,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
const useCliProject = await prompt.select({
|
|
42
|
+
message: "Found different active Firebase Projects in the Firebase CLI and your Firebase Studio Workspace. Which project would you like to set as your active project?",
|
|
43
|
+
choices,
|
|
44
|
+
});
|
|
45
|
+
if (useCliProject) {
|
|
46
|
+
await updateStudioFirebaseProject(activeProjectFromConfig);
|
|
47
|
+
return activeProjectFromConfig;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
await writeStudioProjectToConfigStore(options, studioWorkspace.firebaseProjectId);
|
|
51
|
+
return studioWorkspace.firebaseProjectId;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return studioWorkspace.firebaseProjectId;
|
|
55
|
+
}
|
|
56
|
+
exports.reconcileStudioFirebaseProject = reconcileStudioFirebaseProject;
|
|
57
|
+
async function getStudioWorkspace() {
|
|
58
|
+
const workspaceId = process.env.WORKSPACE_SLUG;
|
|
59
|
+
if (!workspaceId) {
|
|
60
|
+
logger_1.logger.error(`Failed to fetch Firebase Project from Studio Workspace because WORKSPACE_SLUG environment variable is empty`);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const res = await studioClient.request({
|
|
65
|
+
method: "GET",
|
|
66
|
+
path: `/workspaces/${workspaceId}`,
|
|
67
|
+
timeout: TIMEOUT_MILLIS,
|
|
68
|
+
});
|
|
69
|
+
return res.body;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
let message = err.message;
|
|
73
|
+
if (err.original) {
|
|
74
|
+
message += ` (original: ${err.original.message})`;
|
|
75
|
+
}
|
|
76
|
+
logger_1.logger.error(`Failed to fetch Firebase Project from current Studio Workspace: ${message}`);
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function writeStudioProjectToConfigStore(options, studioProjectId) {
|
|
81
|
+
if (options.projectRoot) {
|
|
82
|
+
logger_1.logger.info(`Updating Firebase CLI active project to match Studio Workspace '${studioProjectId}'`);
|
|
83
|
+
utils.makeActiveProject(options.projectRoot, studioProjectId);
|
|
84
|
+
recordStudioProjectSyncTime();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function updateStudioFirebaseProject(projectId) {
|
|
88
|
+
logger_1.logger.info(`Updating Studio Workspace active project to match Firebase CLI '${projectId}'`);
|
|
89
|
+
const workspaceId = process.env.WORKSPACE_SLUG;
|
|
90
|
+
if (!workspaceId) {
|
|
91
|
+
logger_1.logger.error(`Failed to update Firebase Project for Studio Workspace because WORKSPACE_SLUG environment variable is empty`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await studioClient.request({
|
|
96
|
+
method: "PATCH",
|
|
97
|
+
path: `/workspaces/${workspaceId}`,
|
|
98
|
+
responseType: "json",
|
|
99
|
+
body: {
|
|
100
|
+
firebaseProjectId: projectId,
|
|
101
|
+
},
|
|
102
|
+
queryParams: {
|
|
103
|
+
updateMask: "workspace.firebaseProjectId",
|
|
104
|
+
},
|
|
105
|
+
timeout: TIMEOUT_MILLIS,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
let message = err.message;
|
|
110
|
+
if (err.original) {
|
|
111
|
+
message += ` (original: ${err.original.message})`;
|
|
112
|
+
}
|
|
113
|
+
logger_1.logger.warn(`Failed to update active Firebase Project for current Studio Workspace: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
recordStudioProjectSyncTime();
|
|
116
|
+
}
|
|
117
|
+
exports.updateStudioFirebaseProject = updateStudioFirebaseProject;
|
|
118
|
+
function recordStudioProjectSyncTime() {
|
|
119
|
+
configstore_1.configstore.set("firebaseStudioProjectLastSynced", Date.now());
|
|
120
|
+
}
|
package/lib/mcp/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
|
7
7
|
const util_1 = require("./util");
|
|
8
8
|
const types_1 = require("./types");
|
|
9
9
|
const index_1 = require("./tools/index");
|
|
10
|
+
const index_2 = require("./prompts/index");
|
|
10
11
|
const configstore_1 = require("../configstore");
|
|
11
12
|
const command_1 = require("../command");
|
|
12
13
|
const requireAuth_1 = require("../requireAuth");
|
|
@@ -22,7 +23,7 @@ const api = require("../api");
|
|
|
22
23
|
const logging_transport_1 = require("./logging-transport");
|
|
23
24
|
const env_1 = require("../env");
|
|
24
25
|
const timeout_1 = require("../timeout");
|
|
25
|
-
const SERVER_VERSION = "0.
|
|
26
|
+
const SERVER_VERSION = "0.3.0";
|
|
26
27
|
const cmd = new command_1.Command("experimental:mcp");
|
|
27
28
|
const orderedLogLevels = [
|
|
28
29
|
"debug",
|
|
@@ -56,9 +57,15 @@ class FirebaseMcpServer {
|
|
|
56
57
|
this.activeFeatures = options.activeFeatures;
|
|
57
58
|
this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT;
|
|
58
59
|
this.server = new index_js_1.Server({ name: "firebase", version: SERVER_VERSION });
|
|
59
|
-
this.server.registerCapabilities({
|
|
60
|
+
this.server.registerCapabilities({
|
|
61
|
+
tools: { listChanged: true },
|
|
62
|
+
logging: {},
|
|
63
|
+
prompts: { listChanged: true },
|
|
64
|
+
});
|
|
60
65
|
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, this.mcpListTools.bind(this));
|
|
61
66
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, this.mcpCallTool.bind(this));
|
|
67
|
+
this.server.setRequestHandler(types_js_1.ListPromptsRequestSchema, this.mcpListPrompts.bind(this));
|
|
68
|
+
this.server.setRequestHandler(types_js_1.GetPromptRequestSchema, this.mcpGetPrompt.bind(this));
|
|
62
69
|
this.server.oninitialized = async () => {
|
|
63
70
|
var _a, _b;
|
|
64
71
|
const clientInfo = this.server.getClientVersion();
|
|
@@ -159,11 +166,19 @@ class FirebaseMcpServer {
|
|
|
159
166
|
getTool(name) {
|
|
160
167
|
return this.availableTools.find((t) => t.mcp.name === name) || null;
|
|
161
168
|
}
|
|
169
|
+
get availablePrompts() {
|
|
170
|
+
var _a;
|
|
171
|
+
return (0, index_2.availablePrompts)(((_a = this.activeFeatures) === null || _a === void 0 ? void 0 : _a.length) ? this.activeFeatures : this.detectedFeatures);
|
|
172
|
+
}
|
|
173
|
+
getPrompt(name) {
|
|
174
|
+
return this.availablePrompts.find((p) => p.mcp.name === name) || null;
|
|
175
|
+
}
|
|
162
176
|
setProjectRoot(newRoot) {
|
|
163
177
|
this.updateStoredClientConfig({ projectRoot: newRoot });
|
|
164
178
|
this.cachedProjectRoot = newRoot || undefined;
|
|
165
179
|
this.detectedFeatures = undefined;
|
|
166
180
|
void this.server.sendToolListChanged();
|
|
181
|
+
void this.server.sendPromptListChanged();
|
|
167
182
|
}
|
|
168
183
|
async resolveOptions() {
|
|
169
184
|
const options = { cwd: this.cachedProjectRoot, isMCP: true };
|
|
@@ -258,6 +273,64 @@ class FirebaseMcpServer {
|
|
|
258
273
|
return (0, util_1.mcpError)(err);
|
|
259
274
|
}
|
|
260
275
|
}
|
|
276
|
+
async mcpListPrompts() {
|
|
277
|
+
await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]);
|
|
278
|
+
const hasActiveProject = !!(await this.getProjectId());
|
|
279
|
+
await this.trackGA4("mcp_list_prompts");
|
|
280
|
+
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
|
|
281
|
+
return {
|
|
282
|
+
prompts: this.availablePrompts.map((p) => ({
|
|
283
|
+
name: p.mcp.name,
|
|
284
|
+
description: p.mcp.description,
|
|
285
|
+
annotations: p.mcp.annotations,
|
|
286
|
+
arguments: p.mcp.arguments,
|
|
287
|
+
})),
|
|
288
|
+
_meta: {
|
|
289
|
+
projectRoot: this.cachedProjectRoot,
|
|
290
|
+
projectDetected: hasActiveProject,
|
|
291
|
+
authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio),
|
|
292
|
+
activeFeatures: this.activeFeatures,
|
|
293
|
+
detectedFeatures: this.detectedFeatures,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async mcpGetPrompt(req) {
|
|
298
|
+
await this.detectProjectRoot();
|
|
299
|
+
const promptName = req.params.name;
|
|
300
|
+
const promptArgs = req.params.arguments || {};
|
|
301
|
+
const prompt = this.getPrompt(promptName);
|
|
302
|
+
if (!prompt) {
|
|
303
|
+
throw new Error(`Prompt '${promptName}' could not be found.`);
|
|
304
|
+
}
|
|
305
|
+
let projectId = await this.getProjectId();
|
|
306
|
+
projectId = projectId || "";
|
|
307
|
+
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
|
|
308
|
+
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
|
|
309
|
+
const options = { projectDir: this.cachedProjectRoot, cwd: this.cachedProjectRoot };
|
|
310
|
+
const promptsCtx = {
|
|
311
|
+
projectId: projectId,
|
|
312
|
+
host: this,
|
|
313
|
+
config: config_1.Config.load(options, true) || new config_1.Config({}, options),
|
|
314
|
+
rc: (0, rc_1.loadRC)(options),
|
|
315
|
+
accountEmail,
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
const messages = await prompt.fn(promptArgs, promptsCtx);
|
|
319
|
+
await this.trackGA4("mcp_get_prompt", {
|
|
320
|
+
tool_name: promptName,
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
messages,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
await this.trackGA4("mcp_get_prompt", {
|
|
328
|
+
tool_name: promptName,
|
|
329
|
+
error: 1,
|
|
330
|
+
});
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
261
334
|
async start() {
|
|
262
335
|
const transport = process.env.FIREBASE_MCP_DEBUG_LOG
|
|
263
336
|
? new logging_transport_1.LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deploy = void 0;
|
|
4
|
+
const prompt_1 = require("../../prompt");
|
|
5
|
+
exports.deploy = (0, prompt_1.prompt)({
|
|
6
|
+
name: "deploy",
|
|
7
|
+
omitPrefix: true,
|
|
8
|
+
description: "Use this command to deploy resources to Firebase.",
|
|
9
|
+
arguments: [
|
|
10
|
+
{
|
|
11
|
+
name: "prompt",
|
|
12
|
+
description: "any specific instructions you wish to provide about deploying",
|
|
13
|
+
required: false,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
annotations: {
|
|
17
|
+
title: "Deploy to Firebase",
|
|
18
|
+
},
|
|
19
|
+
}, async ({ prompt }, { config, projectId, accountEmail }) => {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
role: "user",
|
|
23
|
+
content: {
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `
|
|
26
|
+
Your goal is to deploy resources from the current project to Firebase.
|
|
27
|
+
|
|
28
|
+
Active user: ${accountEmail || "<NONE>"}
|
|
29
|
+
Active project: ${projectId || "<NONE>"}
|
|
30
|
+
|
|
31
|
+
Contents of \`firebase.json\` config file:
|
|
32
|
+
|
|
33
|
+
\`\`\`json
|
|
34
|
+
${config.readProjectFile("firebase.json", { fallback: "<FILE DOES NOT EXIST>" })}
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
## User Instructions
|
|
38
|
+
|
|
39
|
+
${prompt || "<the user didn't supply specific instructions>"}
|
|
40
|
+
|
|
41
|
+
## Steps
|
|
42
|
+
|
|
43
|
+
Follow the steps below taking note of any user instructions provided above.
|
|
44
|
+
|
|
45
|
+
1. If there is no active user, prompt the user to run \`firebase login\` in an interactive terminal before continuing.
|
|
46
|
+
2. If there is no \`firebase.json\` file and the current workspace is a static web application, manually create a \`firebase.json\` with \`"hosting"\` configuration based on the current directory's web app configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
|
|
47
|
+
3. If there is no active project, ask the user if they want to use an existing project or create a new one.
|
|
48
|
+
3a. If create a new one, use the \`firebase_create_project\` tool.
|
|
49
|
+
3b. If they want to use an existing one, ask them for a project id (the \`firebase_list_projects\` tool may be helpful).
|
|
50
|
+
4. Only after making sure Firebase has been initialized, run the \`firebase deploy\` shell command to perform the deploy. This may take a few minutes.
|
|
51
|
+
5. If the deploy has errors, attempt to fix them and ask the user clarifying questions as needed.
|
|
52
|
+
6. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`firebase deploy --force\`.
|
|
53
|
+
7. If only one specific feature is failing, use command \`firebase deploy --only <feature>\` as you debug.
|
|
54
|
+
`.trim(),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
});
|