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.
Files changed (53) hide show
  1. package/lib/api.js +3 -1
  2. package/lib/command.js +9 -3
  3. package/lib/commands/firestore-databases-create.js +11 -0
  4. package/lib/commands/init.js +7 -5
  5. package/lib/commands/internaltesting-functions-discover.js +20 -5
  6. package/lib/commands/use.js +5 -0
  7. package/lib/crashlytics/buildToolsJarHelper.js +1 -2
  8. package/lib/dataconnect/ensureApis.js +1 -0
  9. package/lib/deploy/dataconnect/prepare.js +2 -2
  10. package/lib/deploy/dataconnect/release.js +2 -2
  11. package/lib/deploy/firestore/deploy.js +10 -0
  12. package/lib/deploy/functions/prepare.js +5 -5
  13. package/lib/deploy/functions/prepareFunctionsUpload.js +3 -1
  14. package/lib/emulator/downloadableEmulatorInfo.json +18 -18
  15. package/lib/firestore/api-sort.js +96 -3
  16. package/lib/firestore/api-types.js +20 -1
  17. package/lib/firestore/api.js +68 -1
  18. package/lib/firestore/pretty-print.js +5 -1
  19. package/lib/firestore/validator.js +1 -1
  20. package/lib/functions/deprecationWarnings.js +4 -4
  21. package/lib/gcp/cloudsql/connect.js +1 -1
  22. package/lib/init/features/aitools/claude.js +7 -7
  23. package/lib/init/features/dataconnect/index.js +1 -1
  24. package/lib/init/features/dataconnect/sdk.js +2 -3
  25. package/lib/init/features/index.js +3 -1
  26. package/lib/init/index.js +8 -0
  27. package/lib/management/studio.js +120 -0
  28. package/lib/mcp/index.js +75 -2
  29. package/lib/mcp/prompt.js +10 -0
  30. package/lib/mcp/prompts/core/deploy.js +58 -0
  31. package/lib/mcp/prompts/core/index.js +5 -0
  32. package/lib/mcp/prompts/index.js +45 -0
  33. package/lib/mcp/tools/core/consult_assistant.js +7 -2
  34. package/lib/mcp/tools/core/get_sdk_config.js +10 -0
  35. package/lib/mcp/tools/core/init.js +1 -1
  36. package/lib/mcp/tools/database/get_data.js +49 -0
  37. package/lib/mcp/tools/database/get_rules.js +39 -0
  38. package/lib/mcp/tools/database/index.js +8 -0
  39. package/lib/mcp/tools/database/set_data.js +57 -0
  40. package/lib/mcp/tools/database/set_rules.js +41 -0
  41. package/lib/mcp/tools/database/validate_rules.js +41 -0
  42. package/lib/mcp/tools/index.js +4 -1
  43. package/lib/mcp/tools/rules/get_rules.js +1 -1
  44. package/lib/mcp/types.js +2 -0
  45. package/lib/mcp/util.js +2 -0
  46. package/lib/requireAuth.js +5 -1
  47. package/lib/rtdb.js +10 -6
  48. package/lib/scopes.js +2 -1
  49. package/lib/utils.js +24 -1
  50. package/package.json +1 -1
  51. package/prompts/FIREBASE.md +1 -2
  52. package/schema/firebase-config.json +3 -0
  53. package/templates/init/firestore/firestore.indexes.json +26 -1
@@ -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
- 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)], ["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)]);
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 ${valid.join(", ")}: ${objString}`);
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 Dec 31, 2025
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 on December 31, 2025. As a result, you must migrate away from using functions.config() to continue deploying your functions after December 31, 2025.
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 December 31, 2025.
13
- - firebase deploy command will fail for functions that use the legacy functions.config() API after December 31, 2025.
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.generateId(20);
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 CLAUDE_SETTINGS_PATH = ".claude/settings.local.json";
6
- const CLAUDE_PROMPT_PATH = "CLAUDE.local.md";
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 settingsUpdated = false;
14
+ let mcpUpdated = false;
15
15
  try {
16
- const existingContent = config.readProjectFile(CLAUDE_SETTINGS_PATH);
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(CLAUDE_SETTINGS_PATH, JSON.stringify(existingConfig, null, 2));
32
- settingsUpdated = true;
31
+ config.writeProjectFile(MCP_CONFIG_PATH, JSON.stringify(existingConfig, null, 2));
32
+ mcpUpdated = true;
33
33
  }
34
- files.push({ path: CLAUDE_SETTINGS_PATH, updated: settingsUpdated });
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
- await config.askWriteProjectFile(path.relative(config.projectDir, connectorYamlPath), sdkInfo.connectorYamlContents, false, true);
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.2.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({ tools: { listChanged: true }, logging: {} });
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,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prompt = void 0;
4
+ function prompt(options, fn) {
5
+ return {
6
+ mcp: options,
7
+ fn,
8
+ };
9
+ }
10
+ exports.prompt = prompt;
@@ -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
+ });
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.corePrompts = void 0;
4
+ const deploy_1 = require("./deploy");
5
+ exports.corePrompts = [deploy_1.deploy];