firebase-tools 14.18.0 → 14.19.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 (52) hide show
  1. package/README.md +11 -5
  2. package/lib/appUtils.js +230 -0
  3. package/lib/apphosting/localbuilds.js +23 -0
  4. package/lib/bin/mcp.js +16 -1
  5. package/lib/commands/index.js +8 -0
  6. package/lib/commands/init.js +0 -2
  7. package/lib/commands/remoteconfig-experiments-delete.js +32 -0
  8. package/lib/commands/remoteconfig-experiments-get.js +20 -0
  9. package/lib/commands/remoteconfig-experiments-list.js +38 -0
  10. package/lib/commands/remoteconfig-rollouts-delete.js +32 -0
  11. package/lib/commands/remoteconfig-rollouts-get.js +20 -0
  12. package/lib/commands/remoteconfig-rollouts-list.js +38 -0
  13. package/lib/config.js +4 -2
  14. package/lib/deploy/apphosting/deploy.js +26 -10
  15. package/lib/deploy/apphosting/prepare.js +23 -1
  16. package/lib/deploy/apphosting/release.js +5 -0
  17. package/lib/deploy/apphosting/util.js +4 -3
  18. package/lib/deploy/functions/deploy.js +5 -4
  19. package/lib/deploy/functions/params.js +2 -2
  20. package/lib/emulator/commandUtils.js +3 -3
  21. package/lib/emulator/controller.js +3 -2
  22. package/lib/gcp/cloudsql/cloudsqladmin.js +1 -1
  23. package/lib/gcp/storage.js +73 -20
  24. package/lib/init/features/dataconnect/index.js +18 -9
  25. package/lib/init/features/project.js +66 -75
  26. package/lib/management/projects.js +16 -4
  27. package/lib/mcp/index.js +9 -0
  28. package/lib/mcp/prompts/core/deploy.js +8 -8
  29. package/lib/mcp/prompts/core/init.js +15 -18
  30. package/lib/mcp/prompts/crashlytics/connect.js +2 -2
  31. package/lib/mcp/prompts/index.js +30 -9
  32. package/lib/mcp/resources/guides/init_ai.js +8 -0
  33. package/lib/mcp/resources/guides/init_auth.js +4 -0
  34. package/lib/mcp/resources/guides/init_firestore_rules.js +2 -0
  35. package/lib/mcp/resources/index.js +16 -1
  36. package/lib/mcp/tools/apphosting/list_backends.js +1 -1
  37. package/lib/mcp/tools/core/get_environment.js +34 -17
  38. package/lib/mcp/tools/core/init.js +2 -1
  39. package/lib/mcp/tools/core/logout.js +1 -2
  40. package/lib/mcp/tools/functions/get_logs.js +9 -7
  41. package/lib/mcp/tools/index.js +3 -2
  42. package/lib/remoteconfig/deleteExperiment.js +32 -0
  43. package/lib/remoteconfig/deleteRollout.js +33 -0
  44. package/lib/remoteconfig/getExperiment.js +52 -0
  45. package/lib/remoteconfig/getRollout.js +43 -0
  46. package/lib/remoteconfig/interfaces.js +3 -1
  47. package/lib/remoteconfig/listExperiments.js +72 -0
  48. package/lib/remoteconfig/listRollouts.js +72 -0
  49. package/lib/remoteconfig/options.js +4 -0
  50. package/lib/track.js +6 -4
  51. package/package.json +3 -1
  52. package/schema/firebase-config.json +6 -0
@@ -15,7 +15,7 @@ exports.deploy = (0, prompt_1.prompt)({
15
15
  annotations: {
16
16
  title: "Deploy to Firebase",
17
17
  },
18
- }, async ({ prompt }, { config, projectId, accountEmail }) => {
18
+ }, async ({ prompt }, { config, projectId, accountEmail, firebaseCliCommand }) => {
19
19
  return [
20
20
  {
21
21
  role: "user",
@@ -41,7 +41,7 @@ ${prompt || "<the user didn't supply specific instructions>"}
41
41
 
42
42
  Follow the steps below taking note of any user instructions provided above.
43
43
 
44
- 1. If there is no active user, prompt the user to run \`firebase login\` in an interactive terminal before continuing.
44
+ 1. If there is no active user, prompt the user to run \`${firebaseCliCommand} login\` in an interactive terminal before continuing.
45
45
  2. Analyze the source code in the current working directory to determine if this is a web app. If it isn't, end this process and tell the user "The /firebase:deploy command only works with web apps."
46
46
  3. Analyze the source code in the current working directory to determine if the app requires a server for Server-Side Rendering (SSR). This will determine whether or not to use Firebase App Hosting. Here are instructions to determine if the app needs a server:
47
47
  Objective: Analyze the provided codebase files to determine if the web application requires a backend for Server-Side Rendering (SSR). Your final output must be a clear "Yes" or "No" followed by a brief justification.
@@ -79,19 +79,19 @@ Follow the steps below taking note of any user instructions provided above.
79
79
  Example (No): "No, there are no dependencies or file structures that indicate the use of a server-side rendering framework."
80
80
  4. If there is no \`firebase.json\` file, manually create one based on whether the app requires SSR:
81
81
  4a. If the app requires SSR, configure Firebase App Hosting:
82
- Create \`firebase.json\ with an "apphosting" configuration, setting backendId to the app's name in package.json: \`{"apphosting": {"backendId": "<backendId>"}}\
82
+ Create \`firebase.json\ with an "apphosting" configuration, setting backendId to the app's name in package.json: \`{"apphosting": {"backendId": "<backendId>"}}\
83
83
  4b. If the app does NOT require SSR, configure Firebase Hosting:
84
84
  Create \`firebase.json\ with a "hosting" configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
85
85
  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.
86
86
  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).
87
87
  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).
88
- 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).
89
- If it doesn't exist, create one by running the \`firebase apphosting:backends:create --backend <backendId> --primary-region us-central1 --root-dir .\` shell.
90
- 7. Only after making sure Firebase has been initialized, run the \`firebase deploy\` shell command to perform the deploy. This may take a few minutes.
88
+ 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).
89
+ If it doesn't exist, create one by running the \`${firebaseCliCommand} apphosting:backends:create --backend <backendId> --primary-region us-central1 --root-dir .\` shell.
90
+ 7. Only after making sure Firebase has been initialized, run the \`${firebaseCliCommand} deploy\` shell command to perform the deploy. This may take a few minutes.
91
91
  7a. If deploying to apphosting, tell the user the deployment will take a few minutes, and they can monitor deployment progress in the Firebase console: \`https://console.firebase.google.com/project/<projectId>/apphosting\`
92
92
  8. If the deploy has errors, attempt to fix them and ask the user clarifying questions as needed.
93
- 9. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`firebase deploy --force\`.
94
- 10. If only one specific feature is failing, use command \`firebase deploy --only <feature>\` as you debug.
93
+ 9. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`${firebaseCliCommand} deploy --force\`.
94
+ 10. If only one specific feature is failing, use command \`${firebaseCliCommand} deploy --only <feature>\` as you debug.
95
95
  11. If the deploy succeeds, your job is finished.
96
96
  `.trim(),
97
97
  },
@@ -6,12 +6,12 @@ const types_1 = require("../../../dataconnect/types");
6
6
  const prompt_1 = require("../../prompt");
7
7
  exports.init = (0, prompt_1.prompt)({
8
8
  name: "init",
9
- description: "Use this command to setup Firebase for the current workspace.",
9
+ description: "Use this command to set up Firebase services, like backend and AI features.",
10
10
  annotations: {
11
11
  title: "Initialize Firebase",
12
12
  },
13
13
  }, async (_, mcp) => {
14
- const { config, projectId, accountEmail } = mcp;
14
+ const { config, projectId, accountEmail, firebaseCliCommand } = mcp;
15
15
  const platform = await (0, appFinder_1.getPlatformFromFolder)(config.projectDir);
16
16
  return [
17
17
  {
@@ -41,22 +41,17 @@ ${config.readProjectFile("firebase.json", { fallback: "<FILE DOES NOT EXIST>" })
41
41
 
42
42
 
43
43
  ## Steps
44
-
45
44
  Follow the steps below taking note of any user instructions provided above.
46
45
 
47
46
  1. If there is no active user, use the \`firebase_login\` tool to help them sign in.
48
- - If you run into issues logging the user in, suggest that they run \`npx firebase-tools login --reauth\` in a separate terminal
49
- 2.1 If the user supplied specific instructions, determine which of the services listed below are the best match for the user's needs. Skip to 2.2 if they didn't provide a prompt
50
- 1. Backend Services: Backend services for the user such as user authentication, database, or hosting.
51
- 2. Firebase AI Logic: Add AI features such as chat experiences, multimodal prompts, image generation and editing (via nano banana), etc.
52
- - UNAVAILABLE SERVICES: Analytics, Remote Config (feature flagging), A/B testing, Crashlytics (crash reporting), and Cloud Messaging (push notifications) are not yet available for setup via this command.
53
- 2.2 Start by listing out the existing init options that are available to the user. Ask the user which set of services they would like to add to their app. Always enumerate them and list the options out explicitly for the user.
54
- 1. Backend Services: Backend services for the user such as user authentication, database, or cloud file hosting.
55
- - IMPORTANT: The backend setup guide is for web apps only. If the user requests backend setup for a mobile app (iOS, Android, or Flutter), inform them that this is not supported and do not use the backend setup guide. You can still assist with other requests.
56
- 2. Firebase AI Logic: Add AI features such as chat experiences, multimodal prompts, image generation and editing (via nano banana), etc.
47
+ - If you run into issues logging the user in, suggest that they run \`${firebaseCliCommand} login --reauth\` in a separate terminal
48
+ 2. Start by listing out the existing init options that are available to the user. Ask the user which set of services they would like to add to their app. Always enumerate them and list the options out explicitly for the user:
49
+ - Backend Services: Backend services for the app, such as setting up a database, adding a user-authentication sign up and login page, and deploying a web app to a production URL.
50
+ - IMPORTANT: The backend setup guide is for web apps only. If the user requests backend setup for a mobile app (iOS, Android, or Flutter), inform them that this is not supported and do not use the backend setup guide. You can still assist with other requests.
51
+ - Firebase AI Logic: Add AI features such as chat experiences, multimodal prompts, image generation and editing (via nano banana), etc.
57
52
  - IMPORTANT: The Firebase AI Logic setup guide is for web, flutter, and android apps only. If the user requests firebase setup for unsupported platforms (iOS, Unity, or anything else), inform them that this is not supported and direct the user to Firebase Docs to learn how to set up AI Logic for their application (share this link with the user https://firebase.google.com/docs/ai-logic/get-started?api=dev). You can still assist with other requests.
58
- 3. After the user chooses an init option, create a plan based on the remaining steps in this guide, share it with the user, and give them an opportunity to accept or adjust it.
59
- 4. If there is no active Firebase project, ask the user if they would like to create a project, or use an existing one, and ask them for the project ID
53
+ 3. After the user chooses an init option, create a plan based on the remaining steps in this guide, share it with the user, and give them an opportunity to accept or adjust it.
54
+ 4. If there is no active Firebase project, ask the user if they want to create a new project or use an existing one. If using an existing project, ask for the project ID and explain how to find it: open the Firebase Console (http://console.firebase.google.com/), locate the project ID under the project name in the projects list, or open the project and go to Project Overview → Project Settings.
60
55
  - If they would like to create a project, use the firebase_create_project with the project ID
61
56
  - If they would like to use an existing project, use the firebase_update_environment tool with the active_project argument.
62
57
  - If you run into issues creating the firebase project, ask the user to go to the [Firebase Console](http://console.firebase.google.com/) and create a project. Wait for the user to report back before continuing.
@@ -65,11 +60,13 @@ Follow the steps below taking note of any user instructions provided above.
65
60
  - Run the \`firebase_list_apps\` tool to list their apps, and find an app that matches their "Workspace platform"
66
61
  - If there is no app that matches that criteria, use the \`firebase_create_app\` tool to create the app with the appropriate platform
67
62
  - Do the following only for Flutter apps
68
- - Install the Flutterfire CLI
69
- - Use the Flutterfire CLI tool to connect to the project
63
+ - Execute \`firebase --version\` to check if the Firebase CLI is installed
64
+ - If it isn't installed, run \`npm install -g firebase-tools\` to install it. If it is installed, skip to the next step.
65
+ - Install the Flutterfire CLI
66
+ - Use the Flutterfire CLI tool to connect to the project
70
67
  - Use the Flutterfire CLI to register the appropriate applications based on the user's input
71
68
  - Let the developer know that you currently only support configuring web, ios, and android targets together in a bundle. Each of those targets will have appropriate apps registered in the project using the flutterfire CLI
72
- - Execute flutterfire config using the following pattern: flutterfire config --yes --project=<aliasOrProjectId> --platforms=<platforms>
69
+ - Execute flutterfire config using the following pattern: flutterfire config --yes --project=<aliasOrProjectId> --platforms=<platforms>
73
70
  6. Now that we have a working environment, print out 1) Active user 2) Firebase Project and 3) Firebase App & platform they are using for this process.
74
71
  - Ask the user to confirm this is correct before continuing
75
72
  7. Set up the web Firebase SDK. Skip straight to #8 for Flutter and Android apps
@@ -88,7 +85,7 @@ Follow the steps below taking note of any user instructions provided above.
88
85
  '''
89
86
  8. Read the guide for the appropriate services and follow the instructions. If no guides match the user's need, inform the user.
90
87
  - Use the Firebase \`read_resources\` tool to load the instructions for the service the developer chose in step 2 of this guide
91
- - [Backend Services](firebase://guides/init/backend): Read this resource to setup backend services for the user such as user authentication, database, or hosting.
88
+ - [Backend Services](firebase://guides/init/backend): Read this resource to set up backend services for the app, such as setting up a database, adding a user-authentication sign up and login page, and deploying a web app to a production URL.
92
89
  - [Firebase AI Logic](firebase://guides/init/ai): Read this resource to add Gemini-powered AI features such as chat experiences, multimodal prompts, image generation, image editing (via nano banana), etc.
93
90
  `.trim(),
94
91
  },
@@ -9,7 +9,7 @@ exports.connect = (0, prompt_1.prompt)({
9
9
  annotations: {
10
10
  title: "Access Crashlytics data",
11
11
  },
12
- }, async (unused, { accountEmail }) => {
12
+ }, async (unused, { accountEmail, firebaseCliCommand }) => {
13
13
  return [
14
14
  {
15
15
  role: "user",
@@ -25,7 +25,7 @@ Active user: ${accountEmail || "<NONE>"}
25
25
 
26
26
  1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.**
27
27
  a. Use the \`firebase_get_environment\` tool to verify that the user is logged in.
28
- b. If the Firebase 'Active user' is set to <NONE>, instruct the user to run \`firebase login\`
28
+ b. If the Firebase 'Active user' is set to <NONE>, instruct the user to run \`${firebaseCliCommand} login\`
29
29
  before continuing. Ignore other fields that are set to <NONE>. We are just making sure the
30
30
  user is logged in.
31
31
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.availablePrompts = void 0;
3
+ exports.markdownDocsOfPrompts = exports.availablePrompts = void 0;
4
4
  const core_1 = require("./core");
5
5
  const dataconnect_1 = require("./dataconnect");
6
6
  const crashlytics_1 = require("./crashlytics");
@@ -33,16 +33,37 @@ function namespacePrompts(promptsToNamespace, feature) {
33
33
  return newPrompt;
34
34
  });
35
35
  }
36
- function availablePrompts(features) {
37
- const allPrompts = namespacePrompts(prompts["core"], "core");
38
- if (!features) {
39
- features = Object.keys(prompts).filter((f) => f !== "core");
36
+ function availablePrompts(activeFeatures) {
37
+ const allPrompts = [];
38
+ if (!(activeFeatures === null || activeFeatures === void 0 ? void 0 : activeFeatures.length)) {
39
+ activeFeatures = Object.keys(prompts);
40
40
  }
41
- for (const feature of features) {
42
- if (prompts[feature] && feature !== "core") {
43
- allPrompts.push(...namespacePrompts(prompts[feature], feature));
44
- }
41
+ if (!activeFeatures.includes("core")) {
42
+ activeFeatures = ["core", ...activeFeatures];
43
+ }
44
+ for (const feature of activeFeatures) {
45
+ allPrompts.push(...namespacePrompts(prompts[feature], feature));
45
46
  }
46
47
  return allPrompts;
47
48
  }
48
49
  exports.availablePrompts = availablePrompts;
50
+ function markdownDocsOfPrompts() {
51
+ var _a, _b;
52
+ const allPrompts = availablePrompts();
53
+ let doc = `
54
+ | Prompt Name | Feature Group | Description |
55
+ | ----------- | ------------- | ----------- |`;
56
+ for (const prompt of allPrompts) {
57
+ const feature = ((_a = prompt.mcp._meta) === null || _a === void 0 ? void 0 : _a.feature) || "";
58
+ let description = prompt.mcp.description || "";
59
+ if ((_b = prompt.mcp.arguments) === null || _b === void 0 ? void 0 : _b.length) {
60
+ const argsList = prompt.mcp.arguments.map((arg) => ` <br>&lt;${arg.name}&gt;${arg.required ? "" : " (optional)"}: ${arg.description || ""}`);
61
+ description += ` <br><br>Arguments:${argsList.join("")}`;
62
+ }
63
+ description = description.replaceAll("\n", "<br>");
64
+ doc += `
65
+ | ${prompt.mcp.name} | ${feature} | ${description} |`;
66
+ }
67
+ return doc;
68
+ }
69
+ exports.markdownDocsOfPrompts = markdownDocsOfPrompts;
@@ -60,6 +60,10 @@ Take the following actions depending on the language and platform or framework t
60
60
 
61
61
  - Use the firebase_init tool to set up ailogic
62
62
 
63
+ - For Android, the Google Services Gradle plugin is required to prevent the app from crashing. You must add it in two files:
64
+ - 1. In your project-level \`/build.gradle.kts\` file, add the plugin to the plugins block: id("com.google.gms.google-services") version "4.4.2" apply false
65
+ - 2. In your **app-level** \`/app/build.gradle.kts\` file, apply the plugin: id("com.google.gms.google-services")
66
+
63
67
  ### 3\. Implement AI Features
64
68
 
65
69
  #### Gather Building Blocks for Code Generation
@@ -77,6 +81,10 @@ Take the following actions depending on the language and platform or framework t
77
81
  - Kotlin Only
78
82
  - implementation(platform("com.google.firebase:firebase-bom:34.3.0")) or a higher bom version if it is available
79
83
  - implementation("com.google.firebase:firebase-ai")
84
+ - CRITICAL: When initializing the Firebase AI model in Kotlin, you must explicitly specify the Google AI backend by calling the googleAI() function. The correct syntax is GenerativeBackend.googleAI().
85
+ - Correct Example: val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(...)
86
+ - Incorrect: Do not use the invalid constant GenerativeBackend.GOOGLE_AI.
87
+ - The Kotlin SDK public API makes extensive use of suspend functions and coroutines. Make sure the code you generate is based on that paradigm and avoid using callbacks unless absolutely necessary in Kotlin
80
88
  - For Flutter apps, always include the following imports. do not forget or modify them
81
89
  - import 'package:firebase_core/firebase_core.dart';
82
90
  - import 'package:firebase_ai/firebase_ai.dart';
@@ -27,6 +27,10 @@ exports.init_auth = (0, resource_1.resource)({
27
27
  **Testing & Deployment:**
28
28
  - Test the complete sign-up and sign-in flow to verify authentication functionality
29
29
  - Deploy the application to production once authentication is verified and working properly
30
+
31
+ **Next Steps:**
32
+ - **Security Rules**: If an app uses *Cloud Firestore database*, *Cloud Storage for Firebase*, or *Firebase Realtime Database*, then please update user-based Security Rules that are structured according to the app's specific requirements.
33
+ - **App Deployment**: Deploy the app to production after Security Rules are verified to be working properly.
30
34
  `.trim(),
31
35
  },
32
36
  ],
@@ -30,6 +30,8 @@ ${config.readProjectFile("firestore.rules", { fallback: "<FILE DOES NOT EXIST>"
30
30
 
31
31
  For database entities that neatly fall into the "personal" and "public categories, you can use the personalData and publicData rules. Use the following firestore.rules file, and add a comment above 'personalData' and 'publicData' to note what entities apply to each rule.
32
32
 
33
+ **Next Steps:**
34
+ - **App Deployment**: Deploy the app to production after Security Rules are verified to be working properly.
33
35
  \`\`\`
34
36
  rules_version = '2';
35
37
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveResource = exports.resourceTemplates = exports.resources = void 0;
3
+ exports.markdownDocsOfResources = exports.resolveResource = exports.resourceTemplates = exports.resources = void 0;
4
4
  const docs_1 = require("./docs");
5
5
  const init_ai_1 = require("./guides/init_ai");
6
6
  const init_auth_1 = require("./guides/init_auth");
@@ -42,3 +42,18 @@ async function resolveResource(uri, ctx, track = true) {
42
42
  return null;
43
43
  }
44
44
  exports.resolveResource = resolveResource;
45
+ function markdownDocsOfResources() {
46
+ const allResources = [...exports.resources, ...exports.resourceTemplates];
47
+ const headings = `
48
+ | Resource Name | Description |
49
+ | ------------- | ----------- |`;
50
+ const resourceRows = allResources.map((res) => {
51
+ let desc = res.mcp.title ? `${res.mcp.title}: ` : "";
52
+ desc += res.mcp.description || "";
53
+ desc = desc.replaceAll("\n", "<br>");
54
+ return `
55
+ | ${res.mcp.name} | ${desc} |`;
56
+ });
57
+ return headings + resourceRows.join("");
58
+ }
59
+ exports.markdownDocsOfResources = markdownDocsOfResources;
@@ -13,7 +13,7 @@ exports.list_backends = (0, tool_1.tool)({
13
13
  "is the resource name of the Cloud Run service serving the App Hosting backend. The last segment of that name is the service ID. " +
14
14
  "`domains` is the list of domains that are associated with the backend. They either have type `CUSTOM` or `DEFAULT`. " +
15
15
  " Every backend should have a `DEFAULT` domain. " +
16
- " The actual domain that a user would use to conenct to the backend is the last parameter of the domain resource name. " +
16
+ " The actual domain that a user would use to connect to the backend is the last parameter of the domain resource name. " +
17
17
  " If a custom domain is correctly set up, it will have statuses ending in `ACTIVE`.",
18
18
  inputSchema: zod_1.z.object({
19
19
  location: zod_1.z
@@ -8,6 +8,7 @@ const projectUtils_1 = require("../../../projectUtils");
8
8
  const js_yaml_1 = require("js-yaml");
9
9
  const auth_1 = require("../../../auth");
10
10
  const configstore_1 = require("../../../configstore");
11
+ const appUtils_1 = require("../../../appUtils");
11
12
  exports.get_environment = (0, tool_1.tool)({
12
13
  name: "get_environment",
13
14
  description: "Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more.",
@@ -23,32 +24,48 @@ exports.get_environment = (0, tool_1.tool)({
23
24
  }, async (_, { projectId, host, accountEmail, rc, config }) => {
24
25
  const aliases = projectId ? (0, projectUtils_1.getAliases)({ rc }, projectId) : [];
25
26
  const geminiTosAccepted = !!configstore_1.configstore.get("gemini");
27
+ const projectFileExists = config.projectFileExists("firebase.json");
28
+ const detectedApps = await (0, appUtils_1.detectApps)(process.cwd());
29
+ const allAccounts = (0, auth_1.getAllAccounts)().map((account) => account.user.email);
30
+ const hasOtherAccounts = allAccounts.filter((email) => email !== accountEmail).length > 0;
31
+ const projectConfigPathString = projectFileExists
32
+ ? config.path("firebase.json")
33
+ : "<NO CONFIG PRESENT>";
34
+ const detectedAppsMap = detectedApps
35
+ .filter((app) => !!app.appId)
36
+ .reduce((map, app) => {
37
+ if (app.appId) {
38
+ map.set(app.appId, app.bundleId ? app.bundleId : "<UNKNOWN BUNDLE ID>");
39
+ }
40
+ return map;
41
+ }, new Map());
42
+ const activeProjectString = projectId
43
+ ? `${projectId}${aliases.length ? ` (alias: ${aliases.join(",")})` : ""}`
44
+ : "<NONE>";
45
+ const acceptedGeminiTosString = geminiTosAccepted ? "Accepted" : "<NOT ACCEPTED>";
26
46
  return (0, util_1.toContent)(`# Environment Information
27
47
 
28
48
  Project Directory: ${host.cachedProjectDir}
29
- Project Config Path: ${config.projectFileExists("firebase.json") ? config.path("firebase.json") : "<NO CONFIG PRESENT>"}
30
- Active Project ID: ${projectId ? `${projectId}${aliases.length ? ` (alias: ${aliases.join(",")})` : ""}` : "<NONE>"}
49
+ Project Config Path: ${projectConfigPathString}
50
+ Active Project ID: ${activeProjectString}
51
+ Gemini in Firebase Terms of Service: ${acceptedGeminiTosString}
31
52
  Authenticated User: ${accountEmail || "<NONE>"}
32
- Gemini in Firebase Terms of Service: ${geminiTosAccepted ? "Accepted" : "Not Accepted"}
33
-
34
- # Available Project Aliases (format: '[alias]: [projectId]')
35
-
36
- ${(0, js_yaml_1.dump)(rc.projects).trim()}
37
-
38
- # Available Accounts:
39
-
40
- ${(0, js_yaml_1.dump)((0, auth_1.getAllAccounts)().map((account) => account.user.email)).trim()}
41
- ${config.projectFileExists("firebase.json")
42
- ? `
43
- # firebase.json contents:
53
+ Detected App IDs: ${detectedAppsMap.size > 0 ? `\n\n${(0, js_yaml_1.dump)(Object.fromEntries(detectedAppsMap)).trim()}\n` : "<NONE>"}
54
+ Available Project Aliases (format: '[alias]: [projectId]'): ${Object.entries(rc.projects).length > 0 ? `\n\n${(0, js_yaml_1.dump)(rc.projects).trim()}\n` : "<NONE>"}${hasOtherAccounts ? `\nAvailable Accounts: \n\n${(0, js_yaml_1.dump)(allAccounts).trim()}` : ""}
55
+ ${projectFileExists
56
+ ? `\nfirebase.json contents:
44
57
 
45
58
  \`\`\`json
46
59
  ${config.readProjectFile("firebase.json")}
47
60
  \`\`\``
48
- : `\n# Empty Environment
61
+ : `\nNo firebase.json file was found.
62
+
63
+ If this project does not use Firebase services that require a firebase.json file, no action is necessary.
49
64
 
50
- It looks like the current directory is not initialized as a Firebase project. The user will most likely want to:
65
+ If this project uses Firebase services that require a firebase.json file, the user will most likely want to:
51
66
 
52
67
  a) Change the project directory using the 'firebase_update_environment' tool to select a directory with a 'firebase.json' file in it, or
53
- b) Initialize a new Firebase project directory using the 'firebase_init' tool.`}`);
68
+ b) Initialize a new Firebase project directory using the 'firebase_init' tool.
69
+
70
+ Confirm with the user before taking action.`}`);
54
71
  });
@@ -11,6 +11,7 @@ const errors_1 = require("../../errors");
11
11
  const error_1 = require("../../../error");
12
12
  const utils_1 = require("../../../init/features/ailogic/utils");
13
13
  const projects_1 = require("../../../management/projects");
14
+ const dataconnect_1 = require("../../../init/features/dataconnect");
14
15
  exports.init = (0, tool_1.tool)({
15
16
  name: "init",
16
17
  description: "Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. " +
@@ -70,7 +71,7 @@ exports.init = (0, tool_1.tool)({
70
71
  location_id: zod_1.z
71
72
  .string()
72
73
  .optional()
73
- .default("us-central1")
74
+ .default(dataconnect_1.FDC_DEFAULT_REGION)
74
75
  .describe("The GCP region ID to set up the Firebase Data Connect service."),
75
76
  cloudsql_instance_id: zod_1.z
76
77
  .string()
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logout = void 0;
4
4
  const zod_1 = require("zod");
5
- const clc = require("colorette");
6
5
  const tool_1 = require("../../tool");
7
6
  const util_1 = require("../../util");
8
7
  const auth_1 = require("../../../auth");
@@ -44,7 +43,7 @@ exports.logout = (0, tool_1.tool)({
44
43
  if (token) {
45
44
  try {
46
45
  await (0, auth_1.logout)(token);
47
- logoutMessages.push(`Logged out from ${clc.bold(account.user.email)}`);
46
+ logoutMessages.push(`Logged out from ${account.user.email}`);
48
47
  }
49
48
  catch (e) {
50
49
  if (e instanceof Error) {
@@ -6,6 +6,7 @@ const tool_1 = require("../../tool");
6
6
  const util_1 = require("../../util");
7
7
  const functionslog_1 = require("../../../functions/functionslog");
8
8
  const cloudlogging_1 = require("../../../gcp/cloudlogging");
9
+ const error_1 = require("../../../error");
9
10
  const SEVERITY_LEVELS = [
10
11
  "DEFAULT",
11
12
  "DEBUG",
@@ -39,12 +40,13 @@ function validateTimestamp(label, value) {
39
40
  }
40
41
  exports.get_logs = (0, tool_1.tool)({
41
42
  name: "get_logs",
42
- description: "Retrieves a page of Cloud Functions log entries using Google Cloud Logging advanced filters.",
43
+ description: "Use this to retrieve a page of Cloud Functions log entries using Google Cloud Logging advanced filters.",
43
44
  inputSchema: zod_1.z.object({
44
45
  function_names: zod_1.z
45
- .union([zod_1.z.string(), zod_1.z.array(zod_1.z.string()).min(1)])
46
+ .array(zod_1.z.string())
47
+ .min(1)
46
48
  .optional()
47
- .describe("Optional list of deployed Cloud Function names to filter logs (string or array)."),
49
+ .describe("Optional list of deployed Cloud Function IDs to filter logs (e.g. ['fnA','fnB'])."),
48
50
  page_size: zod_1.z
49
51
  .number()
50
52
  .int()
@@ -84,8 +86,8 @@ exports.get_logs = (0, tool_1.tool)({
84
86
  requiresProject: true,
85
87
  },
86
88
  }, async ({ function_names, page_size, order, page_token, min_severity, start_time, end_time, filter }, { projectId }) => {
87
- const resolvedOrder = order;
88
- const resolvedPageSize = page_size;
89
+ const resolvedOrder = (order === null || order === void 0 ? void 0 : order.toLowerCase()) === "asc" ? "asc" : "desc";
90
+ const resolvedPageSize = page_size !== null && page_size !== void 0 ? page_size : 50;
89
91
  const normalizedSelectors = normalizeFunctionSelectors(function_names);
90
92
  const filterParts = [(0, functionslog_1.getApiFilter)(normalizedSelectors)];
91
93
  if (min_severity) {
@@ -143,7 +145,7 @@ exports.get_logs = (0, tool_1.tool)({
143
145
  return (0, util_1.toContent)(response);
144
146
  }
145
147
  catch (err) {
146
- const message = err instanceof Error ? err.message : "Failed to retrieve Cloud Logging entries.";
147
- return (0, util_1.mcpError)(message);
148
+ const errMsg = (0, error_1.getErrMsg)((err === null || err === void 0 ? void 0 : err.original) || err, "Failed to retrieve Cloud Logging entries.");
149
+ return (0, util_1.mcpError)(errMsg);
148
150
  }
149
151
  });
@@ -18,7 +18,7 @@ function availableTools(activeFeatures) {
18
18
  activeFeatures = Object.keys(tools);
19
19
  }
20
20
  if (!activeFeatures.includes("core")) {
21
- activeFeatures.unshift("core");
21
+ activeFeatures = ["core", ...activeFeatures];
22
22
  }
23
23
  for (const key of activeFeatures) {
24
24
  toolDefs.push(...tools[key]);
@@ -53,8 +53,9 @@ function markdownDocsOfTools() {
53
53
  if (feature === "firebase") {
54
54
  feature = "core";
55
55
  }
56
+ const description = (((_c = tool.mcp) === null || _c === void 0 ? void 0 : _c.description) || "").replaceAll("\n", "<br>");
56
57
  doc += `
57
- | ${tool.mcp.name} | ${feature} | ${((_c = tool.mcp) === null || _c === void 0 ? void 0 : _c.description) || ""} |`;
58
+ | ${tool.mcp.name} | ${feature} | ${description} |`;
58
59
  }
59
60
  return doc;
60
61
  }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deleteExperiment = void 0;
4
+ const clc = require("colorette");
5
+ const api_1 = require("../api");
6
+ const apiv2_1 = require("../apiv2");
7
+ const error_1 = require("../error");
8
+ const utils_1 = require("../utils");
9
+ const TIMEOUT = 30000;
10
+ const apiClient = new apiv2_1.Client({
11
+ urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
12
+ apiVersion: "v1",
13
+ });
14
+ async function deleteExperiment(projectId, namespace, experimentId) {
15
+ try {
16
+ await apiClient.request({
17
+ method: "DELETE",
18
+ path: `projects/${projectId}/namespaces/${namespace}/experiments/${experimentId}`,
19
+ timeout: TIMEOUT,
20
+ });
21
+ return clc.bold(`Successfully deleted experiment ${clc.yellow(experimentId)}`);
22
+ }
23
+ catch (err) {
24
+ const error = (0, error_1.getError)(err);
25
+ if (error.message.includes("is running and cannot be deleted")) {
26
+ const rcConsoleUrl = (0, utils_1.consoleUrl)(projectId, `/config/experiment/results/${experimentId}`);
27
+ throw new error_1.FirebaseError(`Experiment ${experimentId} is currently running and cannot be deleted. If you want to delete this experiment, stop it at ${rcConsoleUrl}`, { original: error });
28
+ }
29
+ throw new error_1.FirebaseError(`Failed to delete Remote Config experiment with ID ${experimentId} for project ${projectId}. Error: ${(0, error_1.getErrMsg)(err)}`, { original: error });
30
+ }
31
+ }
32
+ exports.deleteExperiment = deleteExperiment;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deleteRollout = void 0;
4
+ const api_1 = require("../api");
5
+ const apiv2_1 = require("../apiv2");
6
+ const error_1 = require("../error");
7
+ const utils_1 = require("../utils");
8
+ const clc = require("colorette");
9
+ const TIMEOUT = 30000;
10
+ const apiClient = new apiv2_1.Client({
11
+ urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
12
+ apiVersion: "v1",
13
+ });
14
+ async function deleteRollout(projectId, namespace, rolloutId) {
15
+ try {
16
+ await apiClient.request({
17
+ method: "DELETE",
18
+ path: `/projects/${projectId}/namespaces/${namespace}/rollouts/${rolloutId}`,
19
+ timeout: TIMEOUT,
20
+ });
21
+ return clc.bold(`Successfully deleted rollout ${clc.yellow(rolloutId)}`);
22
+ }
23
+ catch (err) {
24
+ const originalError = (0, error_1.getError)(err);
25
+ const errorMessage = (0, error_1.getErrMsg)(err);
26
+ if (errorMessage.includes("is running and cannot be deleted")) {
27
+ const rcConsoleUrl = (0, utils_1.consoleUrl)(projectId, `/config/env/firebase/rollout/${rolloutId}`);
28
+ throw new error_1.FirebaseError(`Rollout '${rolloutId}' is currently running and cannot be deleted. If you want to delete this rollout, stop it at ${rcConsoleUrl}`, { original: originalError });
29
+ }
30
+ throw new error_1.FirebaseError(`Failed to delete Remote Config rollout with ID ${rolloutId} for project ${projectId}. Error: ${errorMessage}`, { original: originalError });
31
+ }
32
+ }
33
+ exports.deleteRollout = deleteRollout;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getExperiment = exports.parseExperiment = void 0;
4
+ const Table = require("cli-table3");
5
+ const util = require("util");
6
+ const api_1 = require("../api");
7
+ const apiv2_1 = require("../apiv2");
8
+ const logger_1 = require("../logger");
9
+ const error_1 = require("../error");
10
+ const TIMEOUT = 30000;
11
+ const TABLE_HEAD = ["Entry Name", "Value"];
12
+ const apiClient = new apiv2_1.Client({
13
+ urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
14
+ apiVersion: "v1",
15
+ });
16
+ const parseExperiment = (experiment) => {
17
+ const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
18
+ table.push(["Name", experiment.name]);
19
+ table.push(["Display Name", experiment.definition.displayName]);
20
+ table.push(["Service", experiment.definition.service]);
21
+ table.push([
22
+ "Objectives",
23
+ util.inspect(experiment.definition.objectives, { showHidden: false, depth: null }),
24
+ ]);
25
+ table.push([
26
+ "Variants",
27
+ util.inspect(experiment.definition.variants, { showHidden: false, depth: null }),
28
+ ]);
29
+ table.push(["State", experiment.state]);
30
+ table.push(["Start Time", experiment.startTime]);
31
+ table.push(["End Time", experiment.endTime]);
32
+ table.push(["Last Update Time", experiment.lastUpdateTime]);
33
+ table.push(["etag", experiment.etag]);
34
+ return table.toString();
35
+ };
36
+ exports.parseExperiment = parseExperiment;
37
+ async function getExperiment(projectId, namespace, experimentId) {
38
+ try {
39
+ const res = await apiClient.request({
40
+ method: "GET",
41
+ path: `projects/${projectId}/namespaces/${namespace}/experiments/${experimentId}`,
42
+ timeout: TIMEOUT,
43
+ });
44
+ return res.body;
45
+ }
46
+ catch (err) {
47
+ const error = (0, error_1.getError)(err);
48
+ logger_1.logger.debug(error.message);
49
+ throw new error_1.FirebaseError(`Failed to get Remote Config experiment with ID ${experimentId} for project ${projectId}. Error: ${error.message}`, { original: error });
50
+ }
51
+ }
52
+ exports.getExperiment = getExperiment;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRollout = exports.parseRolloutIntoTable = void 0;
4
+ const api_1 = require("../api");
5
+ const apiv2_1 = require("../apiv2");
6
+ const logger_1 = require("../logger");
7
+ const error_1 = require("../error");
8
+ const Table = require("cli-table3");
9
+ const util = require("util");
10
+ const TIMEOUT = 30000;
11
+ const TABLE_HEAD = ["Entry Name", "Value"];
12
+ const apiClient = new apiv2_1.Client({
13
+ urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
14
+ apiVersion: "v1",
15
+ });
16
+ const parseRolloutIntoTable = (rollout) => {
17
+ const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
18
+ table.push(["Name", rollout.name], ["Display Name", rollout.definition.displayName], ["Description", rollout.definition.description], ["State", rollout.state], ["Create Time", rollout.createTime], ["Start Time", rollout.startTime], ["End Time", rollout.endTime], ["Last Update Time", rollout.lastUpdateTime], [
19
+ "Control Variant",
20
+ util.inspect(rollout.definition.controlVariant, { showHidden: false, depth: null }),
21
+ ], [
22
+ "Enabled Variant",
23
+ util.inspect(rollout.definition.enabledVariant, { showHidden: false, depth: null }),
24
+ ], ["ETag", rollout.etag]);
25
+ return table.toString();
26
+ };
27
+ exports.parseRolloutIntoTable = parseRolloutIntoTable;
28
+ async function getRollout(projectId, namespace, rolloutId) {
29
+ try {
30
+ const res = await apiClient.request({
31
+ method: "GET",
32
+ path: `/projects/${projectId}/namespaces/${namespace}/rollouts/${rolloutId}`,
33
+ timeout: TIMEOUT,
34
+ });
35
+ return res.body;
36
+ }
37
+ catch (err) {
38
+ const error = (0, error_1.getError)(err);
39
+ logger_1.logger.debug(error.message);
40
+ throw new error_1.FirebaseError(`Failed to get Remote Config Rollout with ID ${rolloutId} for project ${projectId}. Error: ${error.message}`, { original: error });
41
+ }
42
+ }
43
+ exports.getRollout = getRollout;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TagColor = void 0;
3
+ exports.TagColor = exports.DEFAULT_PAGE_SIZE = exports.NAMESPACE_FIREBASE = void 0;
4
+ exports.NAMESPACE_FIREBASE = "firebase";
5
+ exports.DEFAULT_PAGE_SIZE = "10";
4
6
  var TagColor;
5
7
  (function (TagColor) {
6
8
  TagColor["BLUE"] = "Blue";