Skip to content

Commit 5bddb93

Browse files
committed
Performance improvements and new billing info for get_environment
1 parent 7b35263 commit 5bddb93

File tree

8 files changed

+54
-27
lines changed

8 files changed

+54
-27
lines changed

src/appUtils.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,29 +59,30 @@ export async function getPlatformsFromFolder(dirPath: string): Promise<Platform[
5959
* @return A list of apps detected.
6060
*/
6161
export async function detectApps(dirPath: string): Promise<App[]> {
62-
const packageJsonFiles = await detectFiles(dirPath, "package.json");
63-
const pubSpecYamlFiles = await detectFiles(dirPath, "pubspec.yaml");
64-
const srcMainFolders = await detectFiles(dirPath, "src/main/");
65-
const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/");
66-
const adminAndWebApps = (
67-
await Promise.all(packageJsonFiles.map((p) => packageJsonToAdminOrWebApp(dirPath, p)))
68-
).flat();
69-
const flutterAppPromises = await Promise.all(
70-
pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)),
62+
const [packageJsonFiles, pubSpecYamlFiles, srcMainFolders, xCodeProjects] = await Promise.all([
63+
detectFiles(dirPath, "package.json"),
64+
detectFiles(dirPath, "pubspec.yaml"),
65+
detectFiles(dirPath, "src/main/"),
66+
detectFiles(dirPath, "*.xcodeproj/"),
67+
]);
68+
69+
const [adminAndWebApps, flutterApps, androidAppsRaw, iosAppsRaw] = await Promise.all([
70+
Promise.all(packageJsonFiles.map((p) => packageJsonToAdminOrWebApp(dirPath, p))).then((r) =>
71+
r.flat(),
72+
),
73+
Promise.all(pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f))).then((r) => r.flat()),
74+
Promise.all(srcMainFolders.map((f) => processAndroidDir(dirPath, f))).then((r) => r.flat()),
75+
Promise.all(xCodeProjects.map((f) => processIosDir(dirPath, f))).then((r) => r.flat()),
76+
]);
77+
78+
const androidApps = androidAppsRaw.filter(
79+
(a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory)),
7180
);
72-
const flutterApps = flutterAppPromises.flat();
7381

74-
const androidAppPromises = await Promise.all(
75-
srcMainFolders.map((f) => processAndroidDir(dirPath, f)),
82+
const iosApps = iosAppsRaw.filter(
83+
(a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory)),
7684
);
77-
const androidApps = androidAppPromises
78-
.flat()
79-
.filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory)));
8085

81-
const iosAppPromises = await Promise.all(xCodeProjects.map((f) => processIosDir(dirPath, f)));
82-
const iosApps = iosAppPromises
83-
.flat()
84-
.filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory)));
8586
return [...flutterApps, ...androidApps, ...iosApps, ...adminAndWebApps];
8687
}
8788

src/mcp/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { timeoutFallback } from "../timeout";
4747
import { resolveResource, resources, resourceTemplates } from "./resources";
4848
import * as crossSpawn from "cross-spawn";
4949
import { getDefaultFeatureAvailabilityCheck } from "./util/availability";
50+
import { checkBillingEnabled } from "../gcp/cloudbilling";
5051

5152
const SERVER_VERSION = "0.3.0";
5253

@@ -193,7 +194,8 @@ export class FirebaseMcpServer {
193194
this.logger.debug("detecting active features of Firebase MCP server...");
194195
const projectId = (await this.getProjectId()) || "";
195196
const accountEmail = await this.getAuthenticatedUser();
196-
const ctx = this._createMcpContext(projectId, accountEmail);
197+
const isBillingEnabled = projectId ? await checkBillingEnabled(projectId) : false;
198+
const ctx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
197199
const detected = await Promise.all(
198200
SERVER_FEATURES.map(async (f) => {
199201
const availabilityCheck = getDefaultFeatureAvailabilityCheck(f);
@@ -244,7 +246,8 @@ export class FirebaseMcpServer {
244246
// We need a project ID and user for the context, but it's ok if they're empty.
245247
const projectId = (await this.getProjectId()) || "";
246248
const accountEmail = await this.getAuthenticatedUser();
247-
const ctx = this._createMcpContext(projectId, accountEmail);
249+
const isBillingEnabled = projectId ? await checkBillingEnabled(projectId) : false;
250+
const ctx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
248251
return availableTools(ctx, features);
249252
}
250253

@@ -258,7 +261,8 @@ export class FirebaseMcpServer {
258261
// We need a project ID and user for the context, but it's ok if they're empty.
259262
const projectId = (await this.getProjectId()) || "";
260263
const accountEmail = await this.getAuthenticatedUser();
261-
const ctx = this._createMcpContext(projectId, accountEmail);
264+
const isBillingEnabled = projectId ? await checkBillingEnabled(projectId) : false;
265+
const ctx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
262266
return availablePrompts(ctx, features);
263267
}
264268

@@ -297,7 +301,11 @@ export class FirebaseMcpServer {
297301
}
298302
}
299303

300-
private _createMcpContext(projectId: string, accountEmail: string | null): McpContext {
304+
private _createMcpContext(
305+
projectId: string,
306+
accountEmail: string | null,
307+
isBillingEnabled: boolean,
308+
): McpContext {
301309
const options = { projectDir: this.cachedProjectDir, cwd: this.cachedProjectDir };
302310
return {
303311
projectId: projectId,
@@ -306,6 +314,7 @@ export class FirebaseMcpServer {
306314
rc: loadRC(options),
307315
accountEmail,
308316
firebaseCliCommand: this._getFirebaseCliCommand(),
317+
isBillingEnabled,
309318
};
310319
}
311320

@@ -370,7 +379,8 @@ export class FirebaseMcpServer {
370379
if (err) return err;
371380
}
372381

373-
const toolsCtx = this._createMcpContext(projectId, accountEmail);
382+
const isBillingEnabled = projectId ? await checkBillingEnabled(projectId) : false;
383+
const toolsCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
374384
try {
375385
const res = await tool.fn(toolArgs, toolsCtx);
376386
await this.trackGA4("mcp_tool_call", {
@@ -424,7 +434,8 @@ export class FirebaseMcpServer {
424434
const skipAutoAuthForStudio = isFirebaseStudio();
425435
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
426436

427-
const promptsCtx = this._createMcpContext(projectId, accountEmail);
437+
const isBillingEnabled = projectId ? await checkBillingEnabled(projectId) : false;
438+
const promptsCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
428439

429440
try {
430441
const messages = await prompt.fn(promptArgs, promptsCtx);
@@ -464,7 +475,8 @@ export class FirebaseMcpServer {
464475
const skipAutoAuthForStudio = isFirebaseStudio();
465476
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
466477

467-
const resourceCtx = this._createMcpContext(projectId, accountEmail);
478+
const isBillingEnabled = projectId ? await checkBillingEnabled(projectId) : false;
479+
const resourceCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
468480

469481
const resolved = await resolveResource(req.params.uri, resourceCtx);
470482
if (!resolved) {

src/mcp/tools/core/get_environment.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe("get_environment tool", () => {
5454
rc,
5555
config,
5656
firebaseCliCommand: "firebase",
57+
isBillingEnabled: false,
5758
};
5859
};
5960

@@ -72,6 +73,7 @@ Project Directory: /test-dir
7273
Project Config Path: <NO CONFIG PRESENT>
7374
Active Project ID: <NONE>
7475
Gemini in Firebase Terms of Service: <NOT ACCEPTED>
76+
Billing Enabled: No
7577
Authenticated User: <NONE>
7678
Detected App IDs: <NONE>
7779
Available Project Aliases (format: '[alias]: [projectId]'): <NONE>
@@ -120,6 +122,7 @@ Project Directory: /test-dir
120122
Project Config Path: /test-dir/firebase.json
121123
Active Project ID: test-project (alias: my-alias)
122124
Gemini in Firebase Terms of Service: Accepted
125+
Billing Enabled: No
123126
Authenticated User: test@example.com
124127
Detected App IDs:
125128

src/mcp/tools/core/get_environment.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ interface EnvironmentTemplateValues {
2323
// Whether the user has accepted Gemini in Firebase TOS
2424
geminiTosAccepted: boolean;
2525

26+
// Whether billing is enabled for the project
27+
isBillingEnabled: boolean;
28+
2629
// The authenticated user identifier
2730
authenticatedUser?: string;
2831

@@ -48,6 +51,7 @@ export function hydrateTemplate(config: EnvironmentTemplateValues): string {
4851
: "<NONE>";
4952
const projectConfigPath = config.projectConfigPath || "<NO CONFIG PRESENT>";
5053
const geminiTosAccepted = config.geminiTosAccepted ? "Accepted" : "<NOT ACCEPTED>";
54+
const billingEnabled = config.projectId ? (config.isBillingEnabled ? "Yes" : "No") : "N/A";
5155
const authenticatedUser = config.authenticatedUser || "<NONE>";
5256
const detectedApps =
5357
Object.entries(config.detectedAppIds).length > 0
@@ -67,6 +71,7 @@ Project Directory: ${config.projectDir}
6771
Project Config Path: ${projectConfigPath}
6872
Active Project ID: ${activeProject}
6973
Gemini in Firebase Terms of Service: ${geminiTosAccepted}
74+
Billing Enabled: ${billingEnabled}
7075
Authenticated User: ${authenticatedUser}
7176
Detected App IDs: ${detectedApps}
7277
Available Project Aliases (format: '[alias]: [projectId]'): ${availableProjects}${hasOtherAccounts ? `\nAvailable Accounts: \n\n${availableAccounts}` : ""}
@@ -106,9 +111,10 @@ export const get_environment = tool(
106111
requiresProject: false,
107112
},
108113
},
109-
async (_, { projectId, host, accountEmail, rc, config }) => {
114+
async (_, { projectId, host, accountEmail, rc, config, isBillingEnabled }) => {
110115
const aliases = projectId ? getAliases({ rc }, projectId) : [];
111116
const geminiTosAccepted = !!configstore.get("gemini");
117+
const billingEnabled = isBillingEnabled;
112118
const projectFileExists = config.projectFileExists("firebase.json");
113119
const detectedApps = await detectApps(process.cwd());
114120
const allAccounts = getAllAccounts().map((account) => account.user.email);
@@ -129,6 +135,7 @@ export const get_environment = tool(
129135
projectDir: host.cachedProjectDir,
130136
projectConfigPath: projectFileExists ? config.path("firebase.json") : undefined,
131137
geminiTosAccepted,
138+
isBillingEnabled: billingEnabled,
132139
authenticatedUser: accountEmail || undefined,
133140
projectAliasMap: rc.projects,
134141
allAccounts,

src/mcp/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ export interface McpContext {
3030
host: FirebaseMcpServer;
3131
rc: RC;
3232
firebaseCliCommand: string;
33+
isBillingEnabled: boolean;
3334
}

src/mcp/util/apptesting/availability.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe("isAppTestingAvailable", () => {
3131
host: new FirebaseMcpServer({}),
3232
rc: {} as RC,
3333
firebaseCliCommand: "firebase",
34+
isBillingEnabled: false,
3435
});
3536

3637
it("returns false for non mobile project", async () => {

src/mcp/util/availability.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe("getDefaultFeatureAvailabilityCheck", () => {
1717
host: {} as any,
1818
rc: {} as any,
1919
firebaseCliCommand: "firebase",
20+
isBillingEnabled: false,
2021
});
2122

2223
beforeEach(() => {

src/mcp/util/crashlytics/availability.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("isCrashlyticsAvailable", () => {
2020
host: new FirebaseMcpServer({}),
2121
rc: {} as RC,
2222
firebaseCliCommand: "firebase",
23+
isBillingEnabled: false,
2324
});
2425

2526
it("should return true for an Android project with Crashlytics in build.gradle", async () => {

0 commit comments

Comments
 (0)