Skip to content

Commit be9b908

Browse files
feat: add emulator configuration to firebase_init tool
This change enhances the firebase_init MCP tool to allow for the configuration of Firebase emulators. It refactors the emulators setup logic in `src/init/features/emulators.ts` into `askQuestions` and `actuate` functions. The `firebase_init` tool now calls the `actuate` function to programmatically configure emulators, passing settings through the `featureInfo` object. A new `emulators` field has been added to the input schema to support this functionality.
1 parent 6856f6d commit be9b908

File tree

9 files changed

+50
-62
lines changed

9 files changed

+50
-62
lines changed

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,3 @@
33
- `firebase_update_environment` MCP tool supports accepting Gemini in Firebase Terms of Service.
44
- Fixed a bug when `firebase init dataconnect` failed to create a React app when launched from VS Code extension (#9171).
55
- Improved the clarity of the `firebase apptesting:execute` command when you have zero or multiple apps.
6-
- Added 'emulators' to `firebase_init` MCP tool.
7-
- `firebase dataconnect:sql:migrate` now supports Cloud SQL instances with only private IPs. The command must be run in the same VPC of the instance to work. (##9200)

src/dataconnect/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export async function upsertSchema(
140140
apiOrigin: dataconnectOrigin(),
141141
apiVersion: DATACONNECT_API_VERSION,
142142
operationResourceName: op.body.name,
143-
masterTimeout: 10000,
143+
masterTimeout: 120000,
144144
});
145145
}
146146

src/dataconnect/provisionCloudSql.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
22
import * as utils from "../utils";
3-
import * as clc from "colorette";
43
import { grantRolesToCloudSqlServiceAccount } from "./checkIam";
54
import { Instance } from "../gcp/cloudsql/types";
65
import { promiseWithSpinner } from "../utils";
@@ -36,23 +35,20 @@ async function upsertInstance(args: {
3635
const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args;
3736
try {
3837
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
39-
utils.logLabeledBullet(
40-
"dataconnect",
41-
`Found existing Cloud SQL instance ${clc.bold(instanceId)}.`,
42-
);
38+
utils.logLabeledBullet("dataconnect", `Found existing Cloud SQL instance ${instanceId}.`);
4339
const why = getUpdateReason(existingInstance, requireGoogleMlIntegration);
4440
if (why) {
4541
if (dryRun) {
4642
utils.logLabeledBullet(
4743
"dataconnect",
48-
`Cloud SQL instance ${clc.bold(instanceId)} settings are not compatible with Firebase Data Connect. ` +
44+
`Cloud SQL instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
4945
`It will be updated on your next deploy.` +
5046
why,
5147
);
5248
} else {
5349
utils.logLabeledBullet(
5450
"dataconnect",
55-
`Cloud SQL instance ${clc.bold(instanceId)} settings are not compatible with Firebase Data Connect. ` +
51+
`Cloud SQL instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
5652
why,
5753
);
5854
await promiseWithSpinner(
@@ -87,7 +83,7 @@ async function createInstance(args: {
8783
if (dryRun) {
8884
utils.logLabeledBullet(
8985
"dataconnect",
90-
`Cloud SQL Instance ${clc.bold(instanceId)} not found. It will be created on your next deploy.`,
86+
`Cloud SQL Instance ${instanceId} not found. It will be created on your next deploy.`,
9187
);
9288
} else {
9389
await cloudSqlAdminClient.createInstance({
@@ -113,7 +109,7 @@ export function cloudSQLBeingCreated(
113109
includeFreeTrialToS?: boolean,
114110
): string {
115111
return (
116-
`Cloud SQL Instance ${clc.bold(instanceId)} is being created.` +
112+
`Cloud SQL Instance ${instanceId} is being created.` +
117113
(includeFreeTrialToS
118114
? `\nThis instance is provided under the terms of the Data Connect no-cost trial ${freeTrialTermsLink()}`
119115
: "") +
@@ -161,19 +157,9 @@ async function upsertDatabase(args: {
161157
export function getUpdateReason(instance: Instance, requireGoogleMlIntegration: boolean): string {
162158
let reason = "";
163159
const settings = instance.settings;
160+
// Cloud SQL instances must have public IP enabled to be used with Firebase Data Connect.
164161
if (!settings.ipConfiguration?.ipv4Enabled) {
165-
utils.logLabeledWarning(
166-
"dataconnect",
167-
`Cloud SQL instance ${clc.bold(instance.name)} does not have a public IP.
168-
${clc.bold("firebase dataconnect:sql:migrate")} will only work within its VPC (e.g. GCE, GKE).`,
169-
);
170-
if (
171-
settings.ipConfiguration?.privateNetwork &&
172-
!settings.ipConfiguration?.enablePrivatePathForGoogleCloudServices
173-
) {
174-
// Cloud SQL instances with only private IP must enable PSC for Data Connect backend to connect to it.
175-
reason += "\n - to enable Private Path for Google Cloud Services.";
176-
}
162+
reason += "\n - to enable public IP.";
177163
}
178164

179165
if (requireGoogleMlIntegration) {

src/gcp/cloudsql/cloudsqladmin.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Client } from "../../apiv2";
22
import { cloudSQLAdminOrigin } from "../../api";
3-
import * as clc from "colorette";
43
import * as operationPoller from "../../operation-poller";
54
import { Instance, Database, User, UserType, DatabaseFlag } from "./types";
65
import { needProjectId } from "../../projectUtils";
@@ -48,7 +47,7 @@ export async function getInstance(projectId: string, instanceId: string): Promis
4847
const res = await client.get<Instance>(`projects/${projectId}/instances/${instanceId}`);
4948
if (res.body.state === "FAILED") {
5049
throw new FirebaseError(
51-
`Cloud SQL instance ${clc.bold(instanceId)} is in a failed state.\nGo to ${instanceConsoleLink(projectId, instanceId)} to repair or delete it.`,
50+
`Cloud SQL instance ${instanceId} is in a failed state.\nGo to ${instanceConsoleLink(projectId, instanceId)} to repair or delete it.`,
5251
);
5352
}
5453
return res.body;
@@ -122,8 +121,7 @@ export async function updateInstanceForDataConnect(
122121
{
123122
settings: {
124123
ipConfiguration: {
125-
enablePrivatePathForGoogleCloudServices:
126-
!!instance?.settings?.ipConfiguration?.privateNetwork,
124+
ipv4Enabled: true,
127125
},
128126
databaseFlags: dbFlags,
129127
enableGoogleMlIntegration,

src/gcp/cloudsql/connect.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,39 @@ export async function execute(
3434
);
3535
}
3636
let connector: Connector;
37-
let authType: AuthTypes;
37+
let pool: pg.Pool;
3838
switch (user.type) {
3939
case "CLOUD_IAM_USER": {
4040
connector = new Connector({
4141
auth: new FBToolsAuthClient(),
4242
});
43-
authType = AuthTypes.IAM;
43+
const clientOpts = await connector.getOptions({
44+
instanceConnectionName: connectionName,
45+
ipType: IpAddressTypes.PUBLIC,
46+
authType: AuthTypes.IAM,
47+
});
48+
pool = new pg.Pool({
49+
...clientOpts,
50+
user: opts.username,
51+
database: opts.databaseId,
52+
});
4453
break;
4554
}
4655
case "CLOUD_IAM_SERVICE_ACCOUNT": {
4756
connector = new Connector();
48-
authType = AuthTypes.IAM;
4957
// Currently, this only works with Application Default credentials
5058
// https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/issues/61 is an open
5159
// FR to add support for OAuth2 tokens.
60+
const clientOpts = await connector.getOptions({
61+
instanceConnectionName: connectionName,
62+
ipType: IpAddressTypes.PUBLIC,
63+
authType: AuthTypes.IAM,
64+
});
65+
pool = new pg.Pool({
66+
...clientOpts,
67+
user: opts.username,
68+
database: opts.databaseId,
69+
});
5270
break;
5371
}
5472
default: {
@@ -59,24 +77,19 @@ export async function execute(
5977
connector = new Connector({
6078
auth: new FBToolsAuthClient(),
6179
});
62-
authType = AuthTypes.PASSWORD;
80+
const clientOpts = await connector.getOptions({
81+
instanceConnectionName: connectionName,
82+
ipType: IpAddressTypes.PUBLIC,
83+
});
84+
pool = new pg.Pool({
85+
...clientOpts,
86+
user: opts.username,
87+
password: opts.password,
88+
database: opts.databaseId,
89+
});
6390
break;
6491
}
6592
}
66-
const connectionOpts = {
67-
instanceConnectionName: connectionName,
68-
ipType: instance.ipAddresses.some((ip) => ip.type === "PRIMARY")
69-
? IpAddressTypes.PUBLIC
70-
: IpAddressTypes.PRIVATE,
71-
authType: authType,
72-
};
73-
const pool = new pg.Pool({
74-
...(await connector.getOptions(connectionOpts)),
75-
connectionTimeoutMillis: 1000,
76-
password: opts.password,
77-
user: opts.username,
78-
database: opts.databaseId,
79-
});
8093

8194
const cleanUpFn = async () => {
8295
conn.release();

src/gcp/cloudsql/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export interface IpConfiguration {
2323
allowedConsumerProjects: string[];
2424
pscEnabled: boolean;
2525
};
26-
enablePrivatePathForGoogleCloudServices?: boolean;
2726
}
2827

2928
export interface InstanceSettings {

src/init/features/emulators.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import { AdditionalInitFns } from "../../emulator/initEmulators";
99
import { Config } from "../../config";
1010
import { EmulatorsConfig } from "../../firebaseConfig";
1111

12-
export interface RequiredInfo {
12+
export interface EmulatorsInfo {
1313
emulators: Emulators[];
1414
download: boolean;
1515
config: EmulatorsConfig;
1616
}
1717

18-
export async function askQuestions(setup: Setup, config: Config): Promise<void> {
18+
export async function emulatorsAskQuestions(setup: Setup, config: Config): Promise<void> {
1919
const choices = ALL_SERVICE_EMULATORS.map((e) => {
2020
return {
2121
value: e,
@@ -36,7 +36,7 @@ export async function askQuestions(setup: Setup, config: Config): Promise<void>
3636
}
3737

3838
setup.featureInfo = setup.featureInfo || {};
39-
const emulatorsInfo: RequiredInfo = {
39+
const emulatorsInfo: EmulatorsInfo = {
4040
emulators: selectedEmulators,
4141
config: {},
4242
download: false,
@@ -106,7 +106,7 @@ export async function askQuestions(setup: Setup, config: Config): Promise<void>
106106
}
107107
}
108108

109-
export async function actuate(setup: Setup): Promise<void> {
109+
export async function emulatorsActuate(setup: Setup, config: Config): Promise<void> {
110110
const emulatorsInfo = setup.featureInfo?.emulators;
111111
if (!emulatorsInfo) {
112112
return;
@@ -120,8 +120,6 @@ export async function actuate(setup: Setup): Promise<void> {
120120
const key = emulatorName as keyof EmulatorsConfig;
121121
if (key === "ui") {
122122
emulatorsConfig.ui = { ...emulatorsConfig.ui, ...emulatorsInfo.config.ui };
123-
} else if (key === "singleProjectMode") {
124-
emulatorsConfig.singleProjectMode = emulatorsInfo.config[key];
125123
} else if (emulatorsInfo.config[key]) {
126124
emulatorsConfig[key] = { ...emulatorsConfig[key], ...emulatorsInfo.config[key] };
127125
}
@@ -143,4 +141,4 @@ export async function actuate(setup: Setup): Promise<void> {
143141
await downloadIfNecessary(Emulators.UI);
144142
}
145143
}
146-
}
144+
}

src/init/features/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ export {
1616
RequiredInfo as StorageInfo,
1717
actuate as storageActuate,
1818
} from "./storage";
19-
export {
20-
askQuestions as emulatorsAskQuestions,
21-
RequiredInfo as EmulatorsInfo,
22-
actuate as emulatorsActuate,
23-
} from "./emulators";
19+
export { doSetup as emulators } from "./emulators";
2420
export { doSetup as extensions } from "./extensions";
2521
// always runs, sets up .firebaserc
2622
export { doSetup as project } from "./project";

src/mcp/tools/core/init.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export const init = tool(
140140
.object({
141141
enabled: z.boolean().optional(),
142142
host: z.string().optional(),
143-
port: z.number().optional(),
143+
port: z.union([z.string(), z.number()]).optional(),
144144
})
145145
.optional(),
146146
singleProjectMode: z
@@ -213,7 +213,7 @@ export const init = tool(
213213
featuresList.push("storage");
214214
featureInfo.storage = {
215215
rulesFilename: features.storage.rules_filename,
216-
rules: features.storage.rules || "",
216+
rules: features.storage.rules,
217217
writeRules: true,
218218
};
219219
}
@@ -261,4 +261,4 @@ To get started:
261261
`,
262262
);
263263
},
264-
);
264+
);

0 commit comments

Comments
 (0)