Skip to content

Commit 3b874f7

Browse files
[MCP] firebase_update_environment tool can be used to accept Gemini in Firebase ToS (#9143)
* update_environment can be used to accept Gemini ToS * changelog * Update src/mcp/tools/core/update_environment.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * m * m * m * m * m --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent b1bdeca commit 3b874f7

File tree

8 files changed

+81
-46
lines changed

8 files changed

+81
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `firebase_update_environment` MCP tool supports accepting Gemini in Firebase Terms of Service.

src/mcp/errors.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,43 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { mcpError } from "./util";
3+
import { configstore } from "../configstore";
4+
import { check, ensure } from "../ensureApiEnabled";
5+
import { cloudAiCompanionOrigin } from "../api";
36

47
export const NO_PROJECT_ERROR = mcpError(
5-
'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.',
8+
"This tool requires an active project. Use the `firebase_update_environment` tool to set a project ID",
69
"PRECONDITION_FAILED",
710
);
811

12+
const GEMINI_TOS_ERROR = mcpError(
13+
"This tool requires the Gemini in Firebase API, please review the terms of service and accept it using `firebase_update_environment`.\n" +
14+
"Learn more about Gemini in Firebase and how it uses your data: https://firebase.google.com/docs/gemini-in-firebase#how-gemini-in-firebase-uses-your-data",
15+
"PRECONDITION_FAILED",
16+
);
17+
18+
/** Enable the Gemini in Firebase API or return an error to accept it */
19+
export async function requireGeminiToS(projectId: string): Promise<CallToolResult | undefined> {
20+
if (!projectId) {
21+
return NO_PROJECT_ERROR;
22+
}
23+
if (configstore.get("gemini")) {
24+
await ensure(projectId, cloudAiCompanionOrigin(), "");
25+
} else {
26+
if (!(await check(projectId, cloudAiCompanionOrigin(), ""))) {
27+
return GEMINI_TOS_ERROR;
28+
}
29+
}
30+
return undefined;
31+
}
32+
33+
export function noProjectDirectory(projectRoot: string | undefined): CallToolResult {
34+
return mcpError(
35+
`The current project directory '${
36+
projectRoot || "<NO PROJECT DIRECTORY FOUND>"
37+
}' does not exist. Please use the 'update_firebase_environment' tool to target a different project directory.`,
38+
);
39+
}
40+
941
export function mcpAuthError(skipADC: boolean): CallToolResult {
1042
if (skipADC) {
1143
return mcpError(
@@ -15,11 +47,3 @@ export function mcpAuthError(skipADC: boolean): CallToolResult {
1547
return 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.
1648
[ADC]: https://cloud.google.com/docs/authentication/application-default-credentials`);
1749
}
18-
19-
export function mcpGeminiError(projectId: string) {
20-
const consoleUrl = `https://firebase.corp.google.com/project/${projectId}/overview`;
21-
return mcpError(
22-
`This tool uses the Gemini in Firebase API. Visit Firebase Console to enable the Gemini in Firebase API ${consoleUrl} and try again.`,
23-
"PRECONDITION_FAILED",
24-
);
25-
}

src/mcp/index.ts

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ import { Command } from "../command";
2525
import { requireAuth } from "../requireAuth";
2626
import { Options } from "../options";
2727
import { getProjectId } from "../projectUtils";
28-
import { mcpAuthError, NO_PROJECT_ERROR, mcpGeminiError } from "./errors";
28+
import { mcpAuthError, noProjectDirectory, NO_PROJECT_ERROR, requireGeminiToS } from "./errors";
2929
import { trackGA4 } from "../track";
3030
import { Config } from "../config";
3131
import { loadRC } from "../rc";
3232
import { EmulatorHubClient } from "../emulator/hubClient";
3333
import { Emulators } from "../emulator/types";
3434
import { existsSync } from "node:fs";
35-
import { ensure, check } from "../ensureApiEnabled";
36-
import * as api from "../api";
3735
import { LoggingStdioServerTransport } from "./logging-transport";
3836
import { isFirebaseStudio } from "../env";
3937
import { timeoutFallback } from "../timeout";
@@ -57,7 +55,7 @@ export class FirebaseMcpServer {
5755
private _ready: boolean = false;
5856
private _readyPromises: { resolve: () => void; reject: (err: unknown) => void }[] = [];
5957
startupRoot?: string;
60-
cachedProjectRoot?: string;
58+
cachedProjectDir?: string;
6159
server: Server;
6260
activeFeatures?: ServerFeature[];
6361
detectedFeatures?: ServerFeature[];
@@ -156,11 +154,11 @@ export class FirebaseMcpServer {
156154

157155
async detectProjectRoot(): Promise<string> {
158156
await timeoutFallback(this.ready(), null, 2000);
159-
if (this.cachedProjectRoot) return this.cachedProjectRoot;
157+
if (this.cachedProjectDir) return this.cachedProjectDir;
160158
const storedRoot = this.getStoredClientConfig().projectRoot;
161-
this.cachedProjectRoot = storedRoot || this.startupRoot || process.cwd();
162-
this.log("debug", "detected and cached project root: " + this.cachedProjectRoot);
163-
return this.cachedProjectRoot;
159+
this.cachedProjectDir = storedRoot || this.startupRoot || process.cwd();
160+
this.log("debug", "detected and cached project root: " + this.cachedProjectDir);
161+
return this.cachedProjectDir;
164162
}
165163

166164
async detectActiveFeatures(): Promise<ServerFeature[]> {
@@ -235,14 +233,14 @@ export class FirebaseMcpServer {
235233

236234
setProjectRoot(newRoot: string | null): void {
237235
this.updateStoredClientConfig({ projectRoot: newRoot });
238-
this.cachedProjectRoot = newRoot || undefined;
236+
this.cachedProjectDir = newRoot || undefined;
239237
this.detectedFeatures = undefined; // reset detected features
240238
void this.server.sendToolListChanged();
241239
void this.server.sendPromptListChanged();
242240
}
243241

244242
async resolveOptions(): Promise<Partial<Options>> {
245-
const options: Partial<Options> = { cwd: this.cachedProjectRoot, isMCP: true };
243+
const options: Partial<Options> = { cwd: this.cachedProjectDir, isMCP: true };
246244
await cmd.prepare(options);
247245
return options;
248246
}
@@ -272,7 +270,7 @@ export class FirebaseMcpServer {
272270
return {
273271
tools: this.availableTools.map((t) => t.mcp),
274272
_meta: {
275-
projectRoot: this.cachedProjectRoot,
273+
projectRoot: this.cachedProjectDir,
276274
projectDetected: hasActiveProject,
277275
authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio),
278276
activeFeatures: this.activeFeatures,
@@ -289,15 +287,10 @@ export class FirebaseMcpServer {
289287
if (!tool) throw new Error(`Tool '${toolName}' could not be found.`);
290288

291289
// Check if the current project directory exists.
292-
if (
293-
tool.mcp.name !== "firebase_update_environment" && // allow this tool only, to fix the issue
294-
(!this.cachedProjectRoot || !existsSync(this.cachedProjectRoot))
295-
) {
296-
return mcpError(
297-
`The current project directory '${
298-
this.cachedProjectRoot || "<NO PROJECT DIRECTORY FOUND>"
299-
}' does not exist. Please use the 'update_firebase_environment' tool to target a different project directory.`,
300-
);
290+
if (!tool.mcp._meta?.optionalProjectDir) {
291+
if (!this.cachedProjectDir || !existsSync(this.cachedProjectDir)) {
292+
return noProjectDirectory(this.cachedProjectDir);
293+
}
301294
}
302295

303296
// Check if the project ID is set.
@@ -316,16 +309,11 @@ export class FirebaseMcpServer {
316309

317310
// Check if the tool requires Gemini in Firebase API.
318311
if (tool.mcp._meta?.requiresGemini) {
319-
if (configstore.get("gemini")) {
320-
await ensure(projectId, api.cloudAiCompanionOrigin(), "");
321-
} else {
322-
if (!(await check(projectId, api.cloudAiCompanionOrigin(), ""))) {
323-
return mcpGeminiError(projectId);
324-
}
325-
}
312+
const err = await requireGeminiToS(projectId);
313+
if (err) return err;
326314
}
327315

328-
const options = { projectDir: this.cachedProjectRoot, cwd: this.cachedProjectRoot };
316+
const options = { projectDir: this.cachedProjectDir, cwd: this.cachedProjectDir };
329317
const toolsCtx: ServerToolContext = {
330318
projectId: projectId,
331319
host: this,
@@ -362,7 +350,7 @@ export class FirebaseMcpServer {
362350
arguments: p.mcp.arguments,
363351
})),
364352
_meta: {
365-
projectRoot: this.cachedProjectRoot,
353+
projectRoot: this.cachedProjectDir,
366354
projectDetected: hasActiveProject,
367355
authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio),
368356
activeFeatures: this.activeFeatures,
@@ -386,7 +374,7 @@ export class FirebaseMcpServer {
386374
const skipAutoAuthForStudio = isFirebaseStudio();
387375
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
388376

389-
const options = { projectDir: this.cachedProjectRoot, cwd: this.cachedProjectRoot };
377+
const options = { projectDir: this.cachedProjectDir, cwd: this.cachedProjectDir };
390378
const promptsCtx: ServerPromptContext = {
391379
projectId: projectId,
392380
host: this,

src/mcp/tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export interface ServerTool<InputSchema extends ZodTypeAny = ZodTypeAny> {
3737
openWorldHint?: boolean;
3838
};
3939
_meta?: {
40+
/** Set this on a tool if it cannot work without a Firebase project directory. */
41+
optionalProjectDir?: boolean;
4042
/** Set this on a tool if it *always* requires a project to work. */
4143
requiresProject?: boolean;
4244
/** Set this on a tool if it *always* requires a signed-in user to work. */

src/mcp/tools/core/get_environment.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { toContent } from "../../util";
44
import { getAliases } from "../../../projectUtils";
55
import { dump } from "js-yaml";
66
import { getAllAccounts } from "../../../auth";
7+
import { configstore } from "../../../configstore";
78

89
export const get_environment = tool(
910
{
@@ -22,14 +23,16 @@ export const get_environment = tool(
2223
},
2324
async (_, { projectId, host, accountEmail, rc, config }) => {
2425
const aliases = projectId ? getAliases({ rc }, projectId) : [];
26+
const geminiTosAccepted = !!configstore.get("gemini");
2527
return toContent(`# Environment Information
2628
27-
Project Directory: ${host.cachedProjectRoot}
29+
Project Directory: ${host.cachedProjectDir}
2830
Project Config Path: ${config.projectFileExists("firebase.json") ? config.path("firebase.json") : "<NO CONFIG PRESENT>"}
2931
Active Project ID: ${
3032
projectId ? `${projectId}${aliases.length ? ` (alias: ${aliases.join(",")})` : ""}` : "<NONE>"
3133
}
3234
Authenticated User: ${accountEmail || "<NONE>"}
35+
Gemini in Firebase Terms of Service: ${geminiTosAccepted ? "Accepted" : "Not Accepted"}
3336
3437
# Available Project Aliases (format: '[alias]: [projectId]')
3538

src/mcp/tools/core/init.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { toContent } from "../../util";
44
import { DEFAULT_RULES } from "../../../init/features/database";
55
import { actuate, Setup, SetupInfo } from "../../../init/index";
66
import { freeTrialTermsLink } from "../../../dataconnect/freeTrial";
7+
import { requireGeminiToS } from "../../errors";
78

89
export const init = tool(
910
{
@@ -157,6 +158,11 @@ export const init = tool(
157158
};
158159
}
159160
if (features.dataconnect) {
161+
if (features.dataconnect.app_description) {
162+
// If app description is provided, ensure the Gemini in Firebase API is enabled.
163+
const err = await requireGeminiToS(projectId);
164+
if (err) return err;
165+
}
160166
featuresList.push("dataconnect");
161167
featureInfo.dataconnect = {
162168
analyticsFlow: "mcp",

src/mcp/tools/core/update_environment.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { mcpError, toContent } from "../../util";
44
import { setNewActive } from "../../../commands/use";
55
import { assertAccount, setProjectAccount } from "../../../auth";
66
import { existsSync } from "node:fs";
7+
import { configstore } from "../../../configstore";
78

89
export const update_environment = tool(
910
{
1011
name: "update_environment",
1112
description:
12-
"Updates Firebase environment config such as project directory, active project, active user account, and more. Use `firebase_get_environment` to see the currently configured environment.",
13+
"Updates Firebase environment config such as project directory, active project, active user account, accept terms of service, and more. Use `firebase_get_environment` to see the currently configured environment.",
1314
inputSchema: z.object({
1415
project_dir: z
1516
.string()
@@ -29,17 +30,25 @@ export const update_environment = tool(
2930
.describe(
3031
"The email address of the signed-in user to authenticate as when interacting with the current project directory.",
3132
),
33+
accept_gemini_tos: z
34+
.boolean()
35+
.optional()
36+
.describe("Accept the Gemini in Firebase terms of service."),
3237
}),
3338
annotations: {
3439
title: "Update Firebase Environment",
3540
readOnlyHint: false,
3641
},
3742
_meta: {
43+
optionalProjectDir: true,
3844
requiresAuth: false,
3945
requiresProject: false,
4046
},
4147
},
42-
async ({ project_dir, active_project, active_user_account }, { config, rc, host }) => {
48+
async (
49+
{ project_dir, active_project, active_user_account, accept_gemini_tos },
50+
{ config, rc, host },
51+
) => {
4352
let output = "";
4453
if (project_dir) {
4554
if (!existsSync(project_dir))
@@ -55,12 +64,14 @@ export const update_environment = tool(
5564
}
5665
if (active_user_account) {
5766
assertAccount(active_user_account, { mcp: true });
58-
setProjectAccount(host.cachedProjectRoot!, active_user_account);
67+
setProjectAccount(host.cachedProjectDir!, active_user_account);
5968
output += `- Updated active account to '${active_user_account}'\n`;
6069
}
61-
70+
if (accept_gemini_tos) {
71+
configstore.set("gemini", true);
72+
output += `- Accepted the Gemini in Firebase terms of service\n`;
73+
}
6274
if (output === "") output = "No changes were made.";
63-
6475
return toContent(output);
6576
},
6677
);

src/mcp/tools/rules/validate_rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function validateRulesTool(productName: string) {
105105
let rulesSourceContent: string;
106106
if (source_file) {
107107
try {
108-
const filePath = resolve(source_file, host.cachedProjectRoot!);
108+
const filePath = resolve(source_file, host.cachedProjectDir!);
109109
if (filePath.includes("../"))
110110
return mcpError("Cannot read files outside of the project directory.");
111111
rulesSourceContent = config.readProjectFile(source_file);

0 commit comments

Comments
 (0)