firebase-tools 14.15.1 → 14.16.0

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 (63) hide show
  1. package/lib/commands/dataconnect-sdk-generate.js +28 -24
  2. package/lib/commands/firestore-bulkdelete.js +73 -0
  3. package/lib/commands/firestore-operations-cancel.js +44 -0
  4. package/lib/commands/firestore-operations-describe.js +29 -0
  5. package/lib/commands/firestore-operations-list.js +29 -0
  6. package/lib/commands/firestore-utils.js +15 -0
  7. package/lib/commands/functions-config-export.js +5 -2
  8. package/lib/commands/index.js +5 -0
  9. package/lib/config.js +16 -4
  10. package/lib/dataconnect/ensureApis.js +3 -3
  11. package/lib/deploy/functions/build.js +2 -13
  12. package/lib/deploy/functions/deploy.js +4 -3
  13. package/lib/deploy/functions/prepare.js +10 -7
  14. package/lib/deploy/functions/runtimes/discovery/index.js +1 -1
  15. package/lib/emulator/auth/operations.js +10 -1
  16. package/lib/emulator/commandUtils.js +7 -1
  17. package/lib/emulator/controller.js +15 -31
  18. package/lib/emulator/dataconnectEmulator.js +27 -24
  19. package/lib/emulator/downloadableEmulatorInfo.json +18 -18
  20. package/lib/emulator/functionsEmulator.js +1 -1
  21. package/lib/emulator/hub.js +9 -5
  22. package/lib/extensions/runtimes/common.js +3 -2
  23. package/lib/firestore/api.js +45 -0
  24. package/lib/firestore/pretty-print.js +23 -0
  25. package/lib/functions/env.js +12 -1
  26. package/lib/functions/projectConfig.js +69 -9
  27. package/lib/gcp/cloudfunctions.js +1 -6
  28. package/lib/gcp/cloudfunctionsv2.js +1 -9
  29. package/lib/gcp/cloudsql/cloudsqladmin.js +2 -2
  30. package/lib/init/features/dataconnect/create_app.js +7 -2
  31. package/lib/init/features/dataconnect/index.js +72 -56
  32. package/lib/init/features/dataconnect/sdk.js +23 -11
  33. package/lib/mcp/errors.js +2 -10
  34. package/lib/mcp/index.js +1 -4
  35. package/lib/mcp/prompts/core/deploy.js +1 -1
  36. package/lib/mcp/prompts/crashlytics/connect.js +114 -0
  37. package/lib/mcp/prompts/crashlytics/index.js +2 -3
  38. package/lib/mcp/tools/auth/disable_user.js +1 -1
  39. package/lib/mcp/tools/auth/get_user.js +9 -2
  40. package/lib/mcp/tools/core/index.js +4 -0
  41. package/lib/mcp/tools/core/init.js +11 -2
  42. package/lib/mcp/tools/core/login.js +46 -0
  43. package/lib/mcp/tools/core/logout.js +62 -0
  44. package/lib/mcp/tools/dataconnect/execute.js +71 -0
  45. package/lib/mcp/tools/dataconnect/index.js +3 -13
  46. package/lib/mcp/tools/dataconnect/list_services.js +104 -7
  47. package/lib/mcp/util.js +1 -17
  48. package/lib/serve/functions.js +4 -3
  49. package/lib/track.js +16 -0
  50. package/lib/unzip.js +13 -0
  51. package/lib/utils.js +17 -1
  52. package/package.json +1 -1
  53. package/schema/firebase-config.json +160 -59
  54. package/lib/mcp/prompts/crashlytics/common.js +0 -10
  55. package/lib/mcp/prompts/crashlytics/fix_issue.js +0 -89
  56. package/lib/mcp/prompts/crashlytics/prioritize_issues.js +0 -79
  57. package/lib/mcp/tools/database/set_rules.js +0 -41
  58. package/lib/mcp/tools/dataconnect/execute_graphql.js +0 -48
  59. package/lib/mcp/tools/dataconnect/execute_graphql_read.js +0 -48
  60. package/lib/mcp/tools/dataconnect/execute_mutation.js +0 -62
  61. package/lib/mcp/tools/dataconnect/execute_query.js +0 -62
  62. package/lib/mcp/tools/dataconnect/get_connector.js +0 -31
  63. package/lib/mcp/tools/dataconnect/get_schema.js +0 -31
@@ -26,26 +26,25 @@ const CONNECTOR_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconn
26
26
  const SCHEMA_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/schema.gql");
27
27
  const QUERIES_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/queries.gql");
28
28
  const MUTATIONS_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/mutations.gql");
29
- const emptyConnector = {
30
- id: "example",
31
- path: "./example",
32
- files: [],
33
- };
34
- const defaultConnector = {
35
- id: "example",
36
- path: "./example",
37
- files: [
38
- {
39
- path: "queries.gql",
40
- content: QUERIES_TEMPLATE,
41
- },
29
+ const templateServiceInfo = {
30
+ schemaGql: [{ path: "schema.gql", content: SCHEMA_TEMPLATE }],
31
+ connectors: [
42
32
  {
43
- path: "mutations.gql",
44
- content: MUTATIONS_TEMPLATE,
33
+ id: "example",
34
+ path: "./example",
35
+ files: [
36
+ {
37
+ path: "queries.gql",
38
+ content: QUERIES_TEMPLATE,
39
+ },
40
+ {
41
+ path: "mutations.gql",
42
+ content: MUTATIONS_TEMPLATE,
43
+ },
44
+ ],
45
45
  },
46
46
  ],
47
47
  };
48
- const defaultSchema = { path: "schema.gql", content: SCHEMA_TEMPLATE };
49
48
  async function askQuestions(setup) {
50
49
  const info = {
51
50
  analyticsFlow: "cli",
@@ -54,6 +53,7 @@ async function askQuestions(setup) {
54
53
  locationId: "",
55
54
  cloudSqlInstanceId: "",
56
55
  cloudSqlDatabase: "",
56
+ shouldProvisionCSQL: false,
57
57
  };
58
58
  if (setup.projectId) {
59
59
  const hasBilling = await (0, cloudbilling_1.isBillingEnabled)(setup);
@@ -101,6 +101,7 @@ async function actuate(setup, config, options) {
101
101
  void (0, track_1.trackGA4)("dataconnect_init", {
102
102
  project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing",
103
103
  flow: info.analyticsFlow,
104
+ provision_cloud_sql: String(info.shouldProvisionCSQL),
104
105
  });
105
106
  }
106
107
  if (info.appDescription) {
@@ -118,10 +119,11 @@ async function actuateWithInfo(setup, config, info, options) {
118
119
  const projectId = setup.projectId;
119
120
  if (!projectId) {
120
121
  info.analyticsFlow += "_save_template";
121
- return await writeFiles(config, info, { schemaGql: [defaultSchema], connectors: [defaultConnector] }, options);
122
+ return await writeFiles(config, info, templateServiceInfo, options);
122
123
  }
123
- const hasBilling = await (0, cloudbilling_1.isBillingEnabled)(setup);
124
- if (hasBilling) {
124
+ await (0, ensureApis_1.ensureApis)(projectId, true);
125
+ const provisionCSQL = info.shouldProvisionCSQL && (await (0, cloudbilling_1.isBillingEnabled)(setup));
126
+ if (provisionCSQL) {
125
127
  await (0, provisionCloudSql_1.setupCloudSql)({
126
128
  projectId: projectId,
127
129
  location: info.locationId,
@@ -130,15 +132,18 @@ async function actuateWithInfo(setup, config, info, options) {
130
132
  requireGoogleMlIntegration: false,
131
133
  });
132
134
  }
135
+ const serviceName = `projects/${projectId}/locations/${info.locationId}/services/${info.serviceId}`;
133
136
  if (!info.appDescription) {
137
+ if (!info.serviceGql) {
138
+ await downloadService(info, serviceName);
139
+ }
134
140
  if (info.serviceGql) {
135
141
  info.analyticsFlow += "_save_downloaded";
136
142
  return await writeFiles(config, info, info.serviceGql, options);
137
143
  }
138
144
  info.analyticsFlow += "_save_template";
139
- return await writeFiles(config, info, { schemaGql: [defaultSchema], connectors: [defaultConnector] }, options);
145
+ return await writeFiles(config, info, templateServiceInfo, options);
140
146
  }
141
- const serviceName = `projects/${projectId}/locations/${info.locationId}/services/${info.serviceId}`;
142
147
  const serviceAlreadyExists = !(await (0, client_1.createService)(projectId, info.locationId, info.serviceId));
143
148
  const schemaGql = await (0, utils_1.promiseWithSpinner)(() => (0, fdcExperience_1.generateSchema)(info.appDescription, projectId), "Generating the Data Connect Schema...");
144
149
  const schemaFiles = [{ path: "schema.gql", content: schemaGql }];
@@ -148,7 +153,7 @@ async function actuateWithInfo(setup, config, info, options) {
148
153
  return await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
149
154
  }
150
155
  await (0, utils_1.promiseWithSpinner)(async () => {
151
- const [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence(projectId, info, schemaFiles, hasBilling);
156
+ const [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence(projectId, info, schemaFiles, provisionCSQL);
152
157
  await (0, client_1.upsertSchema)(saveSchemaGql);
153
158
  if (waitForCloudSQLProvision) {
154
159
  void (0, client_1.upsertSchema)(waitForCloudSQLProvision);
@@ -284,7 +289,6 @@ function subConnectorYamlValues(replacementValues) {
284
289
  return replaced;
285
290
  }
286
291
  async function promptForExistingServices(setup, info) {
287
- var _a, _b, _c, _d, _e;
288
292
  if (!setup.projectId) {
289
293
  return;
290
294
  }
@@ -292,10 +296,7 @@ async function promptForExistingServices(setup, info) {
292
296
  if (!existingServices.length) {
293
297
  return;
294
298
  }
295
- const existingServicesAndSchemas = await Promise.all(existingServices.map(async (s) => {
296
- return { service: s, schema: await (0, client_1.getSchema)(s.name) };
297
- }));
298
- const choice = await chooseExistingService(existingServicesAndSchemas);
299
+ const choice = await chooseExistingService(existingServices);
299
300
  if (!choice) {
300
301
  const existingServiceIds = existingServices.map((s) => s.name.split("/").pop());
301
302
  info.serviceId = (0, utils_1.newUniqueId)(defaultServiceId(), existingServiceIds);
@@ -303,39 +304,50 @@ async function promptForExistingServices(setup, info) {
303
304
  return;
304
305
  }
305
306
  info.analyticsFlow += "_pick_existing_service";
306
- const serviceName = (0, names_1.parseServiceName)(choice.service.name);
307
+ const serviceName = (0, names_1.parseServiceName)(choice.name);
307
308
  info.serviceId = serviceName.serviceId;
308
309
  info.locationId = serviceName.location;
310
+ await downloadService(info, choice.name);
311
+ }
312
+ async function downloadService(info, serviceName) {
313
+ var _a, _b, _c, _d, _e;
314
+ const schema = await (0, client_1.getSchema)(serviceName);
315
+ if (!schema) {
316
+ return;
317
+ }
309
318
  info.serviceGql = {
310
319
  schemaGql: [],
311
- connectors: [emptyConnector],
320
+ connectors: [
321
+ {
322
+ id: "example",
323
+ path: "./example",
324
+ files: [],
325
+ },
326
+ ],
312
327
  };
313
- if (choice.schema) {
314
- const primaryDatasource = choice.schema.datasources.find((d) => d.postgresql);
315
- if ((_b = (_a = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql) === null || _b === void 0 ? void 0 : _b.instance) {
316
- const instanceName = (0, names_1.parseCloudSQLInstanceName)(primaryDatasource.postgresql.cloudSql.instance);
317
- info.cloudSqlInstanceId = instanceName.instanceId;
318
- }
319
- if ((_c = choice.schema.source.files) === null || _c === void 0 ? void 0 : _c.length) {
320
- info.serviceGql.schemaGql = choice.schema.source.files;
321
- }
322
- info.cloudSqlDatabase = (_e = (_d = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _d === void 0 ? void 0 : _d.database) !== null && _e !== void 0 ? _e : "";
323
- const connectors = await (0, client_1.listConnectors)(choice.service.name, [
324
- "connectors.name",
325
- "connectors.source.files",
326
- ]);
327
- if (connectors.length) {
328
- info.serviceGql.connectors = connectors.map((c) => {
329
- const id = c.name.split("/").pop();
330
- return {
331
- id,
332
- path: connectors.length === 1 ? "./example" : `./${id}`,
333
- files: c.source.files || [],
334
- };
335
- });
336
- }
328
+ const primaryDatasource = schema.datasources.find((d) => d.postgresql);
329
+ if ((_b = (_a = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql) === null || _b === void 0 ? void 0 : _b.instance) {
330
+ const instanceName = (0, names_1.parseCloudSQLInstanceName)(primaryDatasource.postgresql.cloudSql.instance);
331
+ info.cloudSqlInstanceId = instanceName.instanceId;
332
+ }
333
+ if ((_c = schema.source.files) === null || _c === void 0 ? void 0 : _c.length) {
334
+ info.serviceGql.schemaGql = schema.source.files;
335
+ }
336
+ info.cloudSqlDatabase = (_e = (_d = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _d === void 0 ? void 0 : _d.database) !== null && _e !== void 0 ? _e : "";
337
+ const connectors = await (0, client_1.listConnectors)(serviceName, [
338
+ "connectors.name",
339
+ "connectors.source.files",
340
+ ]);
341
+ if (connectors.length) {
342
+ info.serviceGql.connectors = connectors.map((c) => {
343
+ const id = c.name.split("/").pop();
344
+ return {
345
+ id,
346
+ path: connectors.length === 1 ? "./example" : `./${id}`,
347
+ files: c.source.files || [],
348
+ };
349
+ });
337
350
  }
338
- return;
339
351
  }
340
352
  async function chooseExistingService(existing) {
341
353
  const fdcConnector = (0, utils_1.envOverride)("FDC_CONNECTOR", "");
@@ -344,7 +356,7 @@ async function chooseExistingService(existing) {
344
356
  if (serviceEnvVar) {
345
357
  const [serviceLocationFromEnvVar, serviceIdFromEnvVar] = serviceEnvVar.split("/");
346
358
  const serviceFromEnvVar = existing.find((s) => {
347
- const serviceName = (0, names_1.parseServiceName)(s.service.name);
359
+ const serviceName = (0, names_1.parseServiceName)(s.name);
348
360
  return (serviceName.serviceId === serviceIdFromEnvVar &&
349
361
  serviceName.location === serviceLocationFromEnvVar);
350
362
  });
@@ -356,7 +368,7 @@ async function chooseExistingService(existing) {
356
368
  (0, utils_1.logWarning)(`Unable to pick up an existing service based on ${envVarName}=${serviceEnvVar}.`);
357
369
  }
358
370
  const choices = existing.map((s) => {
359
- const serviceName = (0, names_1.parseServiceName)(s.service.name);
371
+ const serviceName = (0, names_1.parseServiceName)(s.name);
360
372
  return {
361
373
  name: `${serviceName.location}/${serviceName.serviceId}`,
362
374
  value: s,
@@ -414,6 +426,10 @@ async function promptForCloudSQL(setup, info) {
414
426
  choices,
415
427
  default: "us-central1",
416
428
  });
429
+ info.shouldProvisionCSQL = await (0, prompt_1.confirm)({
430
+ message: `Would you like to provision your Cloud SQL instance and database now?`,
431
+ default: true,
432
+ });
417
433
  }
418
434
  if (info.cloudSqlInstanceId !== "" && info.cloudSqlDatabase === "") {
419
435
  try {
@@ -26,22 +26,30 @@ async function askQuestions(setup) {
26
26
  };
27
27
  info.apps = await chooseApp();
28
28
  if (!info.apps.length) {
29
- const existingFilesAndDirs = (0, fsutils_1.listFiles)(cwd);
30
- const webAppId = (0, utils_1.newUniqueId)("web-app", existingFilesAndDirs);
29
+ const npxMissingWarning = (0, utils_1.commandExistsSync)("npx")
30
+ ? ""
31
+ : clc.yellow(" (you need to install Node.js first)");
32
+ const flutterMissingWarning = (0, utils_1.commandExistsSync)("flutter")
33
+ ? ""
34
+ : clc.yellow(" (you need to install Flutter first)");
31
35
  const choice = await (0, prompt_1.select)({
32
36
  message: `Do you want to create an app template?`,
33
37
  choices: [
34
- { name: "React", value: "react" },
35
- { name: "Next.JS", value: "next" },
38
+ { name: `React${npxMissingWarning}`, value: "react" },
39
+ { name: `Next.JS${npxMissingWarning}`, value: "next" },
40
+ { name: `Flutter${flutterMissingWarning}`, value: "flutter" },
36
41
  { name: "no", value: "no" },
37
42
  ],
38
43
  });
39
44
  switch (choice) {
40
45
  case "react":
41
- await (0, create_app_1.createReactApp)(webAppId);
46
+ await (0, create_app_1.createReactApp)((0, utils_1.newUniqueId)("web-app", (0, fsutils_1.listFiles)(cwd)));
42
47
  break;
43
48
  case "next":
44
- await (0, create_app_1.createNextApp)(webAppId);
49
+ await (0, create_app_1.createNextApp)((0, utils_1.newUniqueId)("web-app", (0, fsutils_1.listFiles)(cwd)));
50
+ break;
51
+ case "flutter":
52
+ await (0, create_app_1.createFlutterApp)((0, utils_1.newUniqueId)("flutter_app", (0, fsutils_1.listFiles)(cwd)));
45
53
  break;
46
54
  case "no":
47
55
  break;
@@ -151,11 +159,15 @@ async function actuateWithInfo(setup, config, info) {
151
159
  config.writeProjectFile(path.relative(config.projectDir, connectorYamlPath), connectorYamlContents);
152
160
  (0, utils_1.logLabeledBullet)("dataconnect", `Installing the generated SDKs ...`);
153
161
  const account = (0, auth_1.getGlobalDefaultAccount)();
154
- await dataconnectEmulator_1.DataConnectEmulator.generate({
155
- configDir: connectorInfo.directory,
156
- connectorId: connectorInfo.connectorYaml.connectorId,
157
- account,
158
- });
162
+ try {
163
+ await dataconnectEmulator_1.DataConnectEmulator.generate({
164
+ configDir: connectorInfo.directory,
165
+ account,
166
+ });
167
+ }
168
+ catch (e) {
169
+ (0, utils_1.logLabeledError)("dataconnect", `Failed to generate Data Connect SDKs\n${e === null || e === void 0 ? void 0 : e.message}`);
170
+ }
159
171
  (0, utils_1.logLabeledSuccess)("dataconnect", `Installed generated SDKs for ${clc.bold(apps.map((a) => (0, appFinder_1.appDescription)(a)).join(", "))}`);
160
172
  if (apps.some((a) => a.platform === types_1.Platform.IOS)) {
161
173
  (0, utils_1.logBullet)(clc.bold("Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/ios-sdk#set-client"));
package/lib/mcp/errors.js CHANGED
@@ -4,18 +4,10 @@ exports.mcpGeminiError = exports.mcpAuthError = exports.NO_PROJECT_ERROR = void
4
4
  const util_1 = require("./util");
5
5
  exports.NO_PROJECT_ERROR = (0, util_1.mcpError)('No active project was found. Use the `firebase_update_environment` tool to set the project directory to an absolute folder location containing a firebase.json config file. Alternatively, change the MCP server config to add [...,"--dir","/absolute/path/to/project/directory"] in its command-line arguments.', "PRECONDITION_FAILED");
6
6
  function mcpAuthError(skipADC) {
7
- const cmd = (0, util_1.commandExistsSync)("firebase") ? "firebase" : "npx -y firebase-tools";
8
7
  if (skipADC) {
9
- return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please instruct the user to execute this shell command to sign in.
10
- \`\`\`sh
11
- ${cmd} login
12
- \`\`\``);
8
+ return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please run the 'firebase_login' tool to log in.`);
13
9
  }
14
- return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please instruct the user to execute this shell command to sign in or to configure [Application Default Credentials][ADC] on their machine.
15
- \`\`\`sh
16
- ${cmd} login
17
- \`\`\`
18
-
10
+ return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please run the 'firebase_login' tool to log in, or instruct the user to configure [Application Default Credentials][ADC] on their machine.
19
11
  [ADC]: https://cloud.google.com/docs/authentication/application-default-credentials`);
20
12
  }
21
13
  exports.mcpAuthError = mcpAuthError;
package/lib/mcp/index.js CHANGED
@@ -140,9 +140,6 @@ class FirebaseMcpServer {
140
140
  return this.emulatorHubClient;
141
141
  }
142
142
  const projectId = await this.getProjectId();
143
- if (!projectId) {
144
- return;
145
- }
146
143
  this.emulatorHubClient = new hubClient_1.EmulatorHubClient(projectId);
147
144
  return this.emulatorHubClient;
148
145
  }
@@ -154,7 +151,7 @@ class FirebaseMcpServer {
154
151
  const emulators = await hubClient.getEmulators();
155
152
  const emulatorInfo = emulators[emulatorType];
156
153
  if (!emulatorInfo) {
157
- throw Error("No Firestore Emulator found running. Make sure your project firebase.json file includes firestore and then rerun emulator using `firebase emulators:start` from your project directory.");
154
+ throw Error(`No ${emulatorType} Emulator found running. Make sure your project firebase.json file includes ${emulatorType} and then rerun emulator using \`firebase emulators:start\` from your project directory.`);
158
155
  }
159
156
  const host = emulatorInfo.host.includes(":") ? `[${emulatorInfo.host}]` : emulatorInfo.host;
160
157
  return `http://${host}:${emulatorInfo.port}`;
@@ -83,7 +83,7 @@ Follow the steps below taking note of any user instructions provided above.
83
83
  Create \`firebase.json\ with an "apphosting" configuration, setting backendId to the app's name in package.json: \`{"apphosting": {"backendId": "<backendId>"}}\
84
84
  4b. If the app does NOT require SSR, configure Firebase Hosting:
85
85
  Create \`firebase.json\ with a "hosting" configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
86
- 5. Check if there is an active Firebase project for this environment (the \`firebase_get_environment\` tool may be helpful). If there is, proceed using that project. If there is not an active project, give the user two options: Use an existing Firebase project or Create a new one. Wait for their response before proceeding.
86
+ 5. Check if there is an active Firebase project for this environment (the \`firebase_get_environment\` tool may be helpful). If there is, provide the active project ID to the user and ask them if they want to proceed using that project. If there is not an active project, give the user two options: Provide an existing project ID or create a new project. Only use the list_projects tool on user request. Wait for their response before proceeding.
87
87
  5a. If the user chooses to use an existing Firebase project, the \`firebase_list_projects\` tool may be helpful. Set the selected project as the active project (the \`firebase_update_environment\` tool may be helpful).
88
88
  5b. If the user chooses to create a new project, use the \`firebase_create_project \` tool. Then set the new project as the active project (the \`firebase_update_environment\` tool may be helpful).
89
89
  6. If firebase.json contains an "apphosting" configuration, check if a backend exists matching the provided backendId (the \`apphosting_list_backends\` tool may be helpful).
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.connect = void 0;
4
+ const prompt_1 = require("../../prompt");
5
+ exports.connect = (0, prompt_1.prompt)({
6
+ name: "connect",
7
+ omitPrefix: false,
8
+ description: "Access a Firebase application's Crashlytics data.",
9
+ annotations: {
10
+ title: "Access Crashlytics data",
11
+ },
12
+ }, async (unused, { accountEmail }) => {
13
+ return [
14
+ {
15
+ role: "user",
16
+ content: {
17
+ type: "text",
18
+ text: `
19
+ You are going to help a developer prioritize and fix issues in their
20
+ mobile application by accessing their Firebase Crashlytics data.
21
+
22
+ Active user: ${accountEmail || "<NONE>"}
23
+
24
+ ## Required first steps! Absolutely required! Incredibly important!
25
+
26
+ 1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.**
27
+ a. Use the \`firebase_get_environment\` tool to verify that the user is logged in,
28
+ and find the active Firebase project.
29
+ b. If the Firebase 'Active user' is set to <NONE>, instruct the user to run \`firebase login\`
30
+ before continuing.
31
+
32
+ 2. **Get the app_id for the Firebase application.**
33
+ a. If this is an Android app, read the mobilesdk_app_id value specified in the
34
+ google-services.json file. If there are multiple files or multiple app ids in a
35
+ single file, ask the user to choose one by providing a numbered list of all the package names.
36
+ b. If this is an iOS app, read the GOOGLE_APP_ID from GoogleService-Info.plist file.
37
+ If there are multiple files or multiple app ids in single file, ask the user to
38
+ choose one by providing a numbered list of all the bundle names.
39
+ c. If you can't find either of the above, just ask the user for the app id.
40
+
41
+ ## Next steps
42
+
43
+ Once you have confirmed that the user is logged in to Firebase, and confirmed the
44
+ id for the application that they want to access, then you can ask the user what actions
45
+ they would like to perform. Here are some possibilities and instructions follow below:
46
+
47
+ 1. Prioritize the most impactful stability issues
48
+ 2. Diagnose and propose a fix for a crash
49
+
50
+ ## Instructions for Using Crashlytics Data
51
+
52
+ ### How to prioritize issues
53
+
54
+ Follow these steps to fetch issues and prioritize them.
55
+
56
+ 1. Use the 'crashlytics_list_top_issues' tool to fetch up to 20 issues.
57
+ 2. Use the 'crashlytics_list_top_versions' tool to fetch the top versions for this app.
58
+ 3. If the user instructions include statements about prioritization, use those instructions.
59
+ 4. If the user instructions do not include statements about prioritization,
60
+ then prioritize the returned issues using the following criteria:
61
+ 4a. The app versions for the issue include the most recent version of the app.
62
+ 4b. The number of users experiencing the issue across variants
63
+ 4c. The volume of crashes
64
+ 5. Return the top 5 issues, with a brief description each in a numerical list with the following format:
65
+ 1. Issue <issue id>
66
+ * <the issue title>
67
+ * <the issue subtitle>
68
+ * **Description:** <a discription of the issue based on information from the tool response>
69
+ * **Rationale:** <the reason this issue was prioritized in the way it was>
70
+
71
+ ### How to diagnose and fix issues
72
+
73
+ Follow these steps to diagnose and fix issues.
74
+
75
+ 1. Make sure you have a good understanding of the code structure and where different functionality exists
76
+ 2. Use the 'crashlytics_get_issue_details' tool to get more context on the issue.
77
+ 3. Use the 'crashlytics_get_sample_crash_for_issue' tool to get 3 example crashes for this issue.
78
+ 4. Read the files that exist in the stack trace of the issue to understand the crash deeply.
79
+ 5. Determine the root cause of the crash.
80
+ 6. Write out a plan using the following criteria:
81
+ 6a. Write out a description of the issue and including
82
+ * A brief description of the cause of the issue
83
+ * A determination of your level of confidence in the cause of the issue
84
+ * A determination of which library is at fault, this codebase or a dependent library
85
+ * A determination for how complex the fix will be
86
+ 6b. The plan should include relevant files to change
87
+ 6c. The plan should include a test plan to verify the fix
88
+ 6d. Use the following format for the plan:
89
+
90
+ ## Cause
91
+ <A description of the root cause leading to the issue>
92
+ - **Fault**: <a determination of whether this code base is at fault or a dependent library is at fault>
93
+ - **Complexity**: <one of "simple", "moderately simple", "moderately hard", "hard", "oof, I don't know where to start">
94
+
95
+ ## Fix
96
+ <A description of the fix for this issue and a break down of the changes.>
97
+ 1. <Step 1>
98
+ 2. <Step 2>
99
+
100
+ ## Test
101
+ <A plan for how to test that the issue has been fixed and protect against regressions>
102
+ 1. <Test case 1>
103
+ 2. <Test case 2>
104
+
105
+ 7. Present the plan to the user and get approval before making the change.
106
+ 8. Fix the issue.
107
+ 8a. Be mindful of API contracts and do not add fields to resources without a clear way to populate those fields
108
+ 8b. If there is not enough information in the crash report to find a root cause, describe why you cannot fix the issue instead of making a guess.
109
+ 9. Ask the developer if they would like you to test the fix for them.
110
+ `.trim(),
111
+ },
112
+ },
113
+ ];
114
+ });
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.crashlyticsPrompts = void 0;
4
- const fix_issue_1 = require("./fix_issue");
5
- const prioritize_issues_1 = require("./prioritize_issues");
6
- exports.crashlyticsPrompts = [fix_issue_1.fix_issue, prioritize_issues_1.prioritize_issues];
4
+ const connect_1 = require("./connect");
5
+ exports.crashlyticsPrompts = [connect_1.connect];
@@ -24,7 +24,7 @@ exports.disable_user = (0, tool_1.tool)({
24
24
  }, async ({ uid, disabled }, { projectId }) => {
25
25
  const res = await (0, auth_1.disableUser)(projectId, uid, disabled);
26
26
  if (res) {
27
- return (0, util_1.toContent)(`User ${uid} as been ${disabled ? "disabled" : "enabled"}`);
27
+ return (0, util_1.toContent)(`User ${uid} has been ${disabled ? "disabled" : "enabled"}`);
28
28
  }
29
29
  return (0, util_1.toContent)(`Failed to ${disabled ? "disable" : "enable"} user ${uid}`);
30
30
  });
@@ -32,7 +32,14 @@ exports.get_user = (0, tool_1.tool)({
32
32
  },
33
33
  }, async ({ email, phone_number, uid }, { projectId }) => {
34
34
  if (email === undefined && phone_number === undefined && uid === undefined) {
35
- return (0, util_1.mcpError)(`No user identifier supplied in auth_get_user tool`);
35
+ return (0, util_1.mcpError)("No user identifier supplied in auth_get_user tool");
36
36
  }
37
- return (0, util_1.toContent)(await (0, auth_1.findUser)(projectId, email, phone_number, uid));
37
+ let user;
38
+ try {
39
+ user = await (0, auth_1.findUser)(projectId, email, phone_number, uid);
40
+ }
41
+ catch (err) {
42
+ return (0, util_1.mcpError)("Unable to find user");
43
+ }
44
+ return (0, util_1.toContent)(user);
38
45
  });
@@ -13,7 +13,11 @@ const get_environment_1 = require("./get_environment");
13
13
  const update_environment_1 = require("./update_environment");
14
14
  const list_projects_1 = require("./list_projects");
15
15
  const consult_assistant_1 = require("./consult_assistant");
16
+ const login_1 = require("./login");
17
+ const logout_1 = require("./logout");
16
18
  exports.coreTools = [
19
+ login_1.login,
20
+ logout_1.logout,
17
21
  get_project_1.get_project,
18
22
  list_apps_1.list_apps,
19
23
  get_admin_sdk_config_1.get_admin_sdk_config,
@@ -6,6 +6,7 @@ const tool_1 = require("../../tool");
6
6
  const util_1 = require("../../util");
7
7
  const database_1 = require("../../../init/features/database");
8
8
  const index_1 = require("../../../init/index");
9
+ const freeTrial_1 = require("../../../dataconnect/freeTrial");
9
10
  exports.init = (0, tool_1.tool)({
10
11
  name: "init",
11
12
  description: "Initializes selected Firebase features in the workspace (Firestore, Data Connect, Realtime Database). All features are optional; provide only the products you wish to set up. " +
@@ -70,12 +71,19 @@ exports.init = (0, tool_1.tool)({
70
71
  cloudsql_instance_id: zod_1.z
71
72
  .string()
72
73
  .optional()
73
- .describe("The GCP Cloud SQL instance ID to use in the Firebase Data Connect service. By default, use <serviceId>-fdc."),
74
+ .describe("The GCP Cloud SQL instance ID to use in the Firebase Data Connect service. By default, use <serviceId>-fdc. " +
75
+ "\nSet `provision_cloudsql` to true to start Cloud SQL provisioning."),
74
76
  cloudsql_database: zod_1.z
75
77
  .string()
76
78
  .optional()
77
79
  .default("fdcdb")
78
80
  .describe("The Postgres database ID to use in the Firebase Data Connect service."),
81
+ provision_cloudsql: zod_1.z
82
+ .boolean()
83
+ .optional()
84
+ .default(false)
85
+ .describe("If true, provision the Cloud SQL instance if `cloudsql_instance_id` does not exist already. " +
86
+ `\nThe first Cloud SQL instance in the project will use the Data Connect no-cost trial. See its terms of service: ${(0, freeTrial_1.freeTrialTermsLink)()}.`),
79
87
  })
80
88
  .optional()
81
89
  .describe("Provide this object to initialize Firebase Data Connect with Cloud SQL Postgres in this project directory.\n" +
@@ -138,6 +146,7 @@ exports.init = (0, tool_1.tool)({
138
146
  locationId: features.dataconnect.location_id || "",
139
147
  cloudSqlInstanceId: features.dataconnect.cloudsql_instance_id || "",
140
148
  cloudSqlDatabase: features.dataconnect.cloudsql_database || "",
149
+ shouldProvisionCSQL: !!features.dataconnect.provision_cloudsql,
141
150
  };
142
151
  featureInfo.dataconnectSdk = {
143
152
  apps: [],
@@ -155,7 +164,7 @@ exports.init = (0, tool_1.tool)({
155
164
  config.writeProjectFile("firebase.json", setup.config);
156
165
  config.writeProjectFile(".firebaserc", setup.rcfile);
157
166
  if (featureInfo.dataconnectSdk && !featureInfo.dataconnectSdk.apps.length) {
158
- setup.instructions.push(`No app is found in the current folder. We recommend you create an app (web, ios, android) first, then re-run the 'firebase_init' MCP tool to add Data Connect SDKs to your apps.
167
+ setup.instructions.push(`No app is found in the current folder. We recommend you create an app (web, ios, android) first, then re-run the 'firebase_init' MCP tool with the same input without app_description to add Data Connect SDKs to your apps.
159
168
  Consider popular commands like 'npx create-react-app my-app', 'npx create-next-app my-app', 'flutter create my-app', etc`);
160
169
  }
161
170
  return (0, util_1.toContent)(`Successfully setup those features: ${featuresList.join(", ")}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.login = void 0;
4
+ const zod_1 = require("zod");
5
+ const tool_1 = require("../../tool");
6
+ const auth_1 = require("../../../auth");
7
+ const util_1 = require("../../util");
8
+ const LoginInputSchema = zod_1.z.object({
9
+ authCode: zod_1.z.string().optional().describe("The authorization code from the login flow"),
10
+ });
11
+ exports.login = (0, tool_1.tool)({
12
+ name: "login",
13
+ description: "Logs the user into the Firebase CLI and MCP server.",
14
+ inputSchema: LoginInputSchema,
15
+ _meta: {
16
+ requiresAuth: false,
17
+ },
18
+ }, async (input, ctx) => {
19
+ const { authCode } = input;
20
+ const serverWithState = ctx.host;
21
+ if (authCode) {
22
+ if (!serverWithState.authorize) {
23
+ return (0, util_1.mcpError)("Login flow not started. Please call this tool without the authCode argument first to get a login URI.");
24
+ }
25
+ try {
26
+ const creds = await serverWithState.authorize(authCode);
27
+ delete serverWithState.authorize;
28
+ const user = creds.user;
29
+ return (0, util_1.toContent)(`Successfully logged in as ${user.email}`);
30
+ }
31
+ catch (e) {
32
+ delete serverWithState.authorize;
33
+ return (0, util_1.mcpError)(`Login failed: ${e.message}`);
34
+ }
35
+ }
36
+ else {
37
+ const prototyper = await (0, auth_1.loginPrototyper)();
38
+ serverWithState.authorize = prototyper.authorize;
39
+ const result = {
40
+ uri: prototyper.uri,
41
+ sessionId: prototyper.sessionId,
42
+ };
43
+ const humanReadable = `Please visit this URL to login: ${result.uri}\nYour session ID is: ${result.sessionId}\nInstruct the use to copy the authorization code from that link, and paste it into chat.\nThen, run this tool again with that as the authCode argument to complete the login.`;
44
+ return (0, util_1.toContent)(humanReadable);
45
+ }
46
+ });