Skip to content

Commit 31af72a

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 76e0c4d commit 31af72a

File tree

3 files changed

+134
-43
lines changed

3 files changed

+134
-43
lines changed

src/init/features/emulators.ts

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

12-
interface EmulatorsInitSelections {
13-
emulators?: Emulators[];
14-
download?: boolean;
12+
export interface EmulatorsInfo {
13+
emulators: Emulators[];
14+
download: boolean;
15+
config: EmulatorsConfig;
1516
}
1617

17-
export async function doSetup(setup: Setup, config: Config) {
18+
export async function emulatorsAskQuestions(setup: Setup, config: Config): Promise<void> {
1819
const choices = ALL_SERVICE_EMULATORS.map((e) => {
1920
return {
2021
value: e,
21-
// TODO: latest versions of inquirer have a name vs description.
22-
// We should learn more and whether it's worth investing in.
2322
name: Constants.description(e),
2423
checked: config?.has(e) || config?.has(`emulators.${e}`),
2524
};
2625
});
2726

28-
const selections: EmulatorsInitSelections = {};
29-
selections.emulators = await checkbox<Emulators>({
27+
const selectedEmulators = await checkbox<Emulators>({
3028
message:
3129
"Which Firebase emulators do you want to set up? " +
3230
"Press Space to select emulators, then Enter to confirm your choices.",
3331
choices: choices,
3432
});
3533

36-
if (!selections.emulators) {
34+
if (!selectedEmulators || !selectedEmulators.length) {
3735
return;
3836
}
3937

40-
setup.config.emulators = setup.config.emulators || {};
41-
const emulators: EmulatorsConfig = setup.config.emulators || {};
42-
for (const selected of selections.emulators) {
43-
if (selected === "extensions") continue;
44-
const selectedEmulator = emulators[selected] || {};
38+
setup.featureInfo = setup.featureInfo || {};
39+
const emulatorsInfo: EmulatorsInfo = {
40+
emulators: selectedEmulators,
41+
config: {},
42+
download: false,
43+
};
44+
setup.featureInfo.emulators = emulatorsInfo;
45+
46+
const newConfig = emulatorsInfo.config;
47+
const existingConfig = setup.config.emulators || {};
4548

46-
const currentPort = selectedEmulator.port;
49+
for (const selected of selectedEmulators) {
50+
if (selected === "extensions") continue;
51+
newConfig[selected] = {};
52+
const currentPort = existingConfig[selected]?.port;
4753
if (currentPort) {
4854
utils.logBullet(`Port for ${selected} already configured: ${clc.cyan(currentPort)}`);
4955
} else {
50-
selectedEmulator.port = await number({
56+
newConfig[selected]!.port = await number({
5157
message: `Which port do you want to use for the ${clc.underline(selected)} emulator?`,
5258
default: Constants.getDefaultPort(selected),
5359
});
5460
}
55-
emulators[selected] = selectedEmulator;
5661

5762
const additionalInitFn = AdditionalInitFns[selected];
5863
if (additionalInitFn) {
5964
const additionalOptions = await additionalInitFn(config);
6065
if (additionalOptions) {
61-
emulators[selected] = {
62-
...setup.config.emulators[selected],
63-
...additionalOptions,
64-
};
66+
Object.assign(newConfig[selected]!, additionalOptions);
6567
}
6668
}
6769
}
6870

69-
if (selections.emulators.length) {
71+
if (selectedEmulators.length) {
7072
const uiDesc = Constants.description(Emulators.UI);
71-
if (setup.config.emulators.ui && setup.config.emulators.ui.enabled !== false) {
72-
const currentPort = setup.config.emulators.ui.port || "(automatic)";
73-
utils.logBullet(`${uiDesc} already enabled with port: ${clc.cyan(currentPort)}`);
74-
} else {
75-
const ui = setup.config.emulators.ui || {};
76-
setup.config.emulators.ui = ui;
73+
const existingUiConfig = existingConfig.ui || {};
74+
newConfig.ui = {};
7775

78-
ui.enabled = await confirm({
76+
let enableUi: boolean;
77+
if (existingUiConfig.enabled !== undefined) {
78+
utils.logBullet(`${uiDesc} already ${existingUiConfig.enabled ? "enabled" : "disabled"}.`);
79+
enableUi = existingUiConfig.enabled;
80+
} else {
81+
enableUi = await confirm({
7982
message: `Would you like to enable the ${uiDesc}?`,
8083
default: true,
8184
});
85+
}
86+
newConfig.ui.enabled = enableUi;
8287

83-
if (ui.enabled) {
84-
ui.port = await number({
88+
if (newConfig.ui.enabled) {
89+
const currentPort = existingUiConfig.port;
90+
if (currentPort) {
91+
utils.logBullet(`Port for ${uiDesc} already configured: ${clc.cyan(currentPort)}`);
92+
} else {
93+
newConfig.ui.port = await number({
8594
message: `Which port do you want to use for the ${clc.underline(uiDesc)} (leave empty to use any available port)?`,
8695
required: false,
8796
});
8897
}
8998
}
99+
}
90100

91-
selections.download = await confirm({
101+
if (selectedEmulators.length) {
102+
emulatorsInfo.download = await confirm({
92103
message: "Would you like to download the emulators now?",
93104
default: true,
94105
});
95106
}
107+
}
108+
109+
export async function emulatorsActuate(setup: Setup, config: Config): Promise<void> {
110+
const emulatorsInfo = setup.featureInfo?.emulators;
111+
if (!emulatorsInfo) {
112+
return;
113+
}
114+
115+
setup.config.emulators = setup.config.emulators || {};
116+
const emulatorsConfig = setup.config.emulators;
117+
118+
// Merge the config from the questions into the main config.
119+
for (const emulatorName of Object.keys(emulatorsInfo.config)) {
120+
const key = emulatorName as keyof EmulatorsConfig;
121+
if (key === "ui") {
122+
emulatorsConfig.ui = { ...emulatorsConfig.ui, ...emulatorsInfo.config.ui };
123+
} else if (emulatorsInfo.config[key]) {
124+
emulatorsConfig[key] = { ...emulatorsConfig[key], ...emulatorsInfo.config[key] };
125+
}
126+
}
96127

97128
// Set the default behavior to be single project mode.
98-
if (setup.config.emulators.singleProjectMode === undefined) {
99-
setup.config.emulators.singleProjectMode = true;
129+
if (emulatorsConfig.singleProjectMode === undefined) {
130+
emulatorsConfig.singleProjectMode = true;
100131
}
101132

102-
if (selections.download) {
103-
for (const selected of selections.emulators) {
133+
if (emulatorsInfo.download) {
134+
for (const selected of emulatorsInfo.emulators) {
104135
if (isDownloadableEmulator(selected)) {
105136
await downloadIfNecessary(selected);
106137
}
107138
}
108139

109-
if (setup?.config?.emulators?.ui?.enabled) {
110-
downloadIfNecessary(Emulators.UI);
140+
if (emulatorsConfig.ui?.enabled) {
141+
await downloadIfNecessary(Emulators.UI);
111142
}
112143
}
113-
}
144+
}

src/init/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface SetupInfo {
3838
dataconnectSdk?: features.DataconnectSdkInfo;
3939
storage?: features.StorageInfo;
4040
apptesting?: features.ApptestingInfo;
41+
emulators?: features.EmulatorsInfo;
4142
}
4243

4344
interface Feature {
@@ -84,7 +85,11 @@ const featuresList: Feature[] = [
8485
askQuestions: features.storageAskQuestions,
8586
actuate: features.storageActuate,
8687
},
87-
{ name: "emulators", doSetup: features.emulators },
88+
{
89+
name: "emulators",
90+
askQuestions: features.emulatorsAskQuestions,
91+
actuate: features.emulatorsActuate,
92+
},
8893
{ name: "extensions", doSetup: features.extensions },
8994
{ name: "project", doSetup: features.project }, // always runs, sets up .firebaserc
9095
{ name: "remoteconfig", doSetup: features.remoteconfig },

src/mcp/tools/core/init.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import { DEFAULT_RULES } from "../../../init/features/database";
55
import { actuate, Setup, SetupInfo } from "../../../init/index";
66
import { freeTrialTermsLink } from "../../../dataconnect/freeTrial";
77
import { requireGeminiToS } from "../../errors";
8+
import { Emulators } from "../../../emulator/types";
9+
10+
const emulatorHostPortSchema = z.object({
11+
host: z.string().optional().describe("The host to use for the emulator."),
12+
port: z.number().optional().describe("The port to use for the emulator."),
13+
});
814

915
export const init = tool(
1016
{
1117
name: "init",
1218
description:
13-
"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. " +
19+
"Initializes selected Firebase features in the workspace (Firestore, Data Connect, Realtime Database, Emulators). All features are optional; provide only the products you wish to set up. " +
1420
"You can initialize new features into an existing project directory, but re-initializing an existing feature may overwrite configuration. " +
1521
"To deploy the initialized features, run the `firebase deploy` command after `firebase_init` tool.",
1622
inputSchema: z.object({
@@ -121,6 +127,31 @@ export const init = tool(
121127
.describe(
122128
"Provide this object to initialize Firebase Storage in this project directory.",
123129
),
130+
emulators: z
131+
.object({
132+
auth: emulatorHostPortSchema.optional(),
133+
database: emulatorHostPortSchema.optional(),
134+
firestore: emulatorHostPortSchema.optional(),
135+
functions: emulatorHostPortSchema.optional(),
136+
hosting: emulatorHostPortSchema.optional(),
137+
storage: emulatorHostPortSchema.optional(),
138+
pubsub: emulatorHostPortSchema.optional(),
139+
ui: z
140+
.object({
141+
enabled: z.boolean().optional(),
142+
host: z.string().optional(),
143+
port: z.union([z.string(), z.number()]).optional(),
144+
})
145+
.optional(),
146+
singleProjectMode: z
147+
.boolean()
148+
.optional()
149+
.describe("If true, do not warn on detection of multiple project IDs."),
150+
})
151+
.optional()
152+
.describe(
153+
"Provide this object to configure Firebase emulators in this project directory.",
154+
),
124155
}),
125156
}),
126157
annotations: {
@@ -178,8 +209,32 @@ export const init = tool(
178209
apps: [],
179210
};
180211
}
212+
if (features.storage) {
213+
featuresList.push("storage");
214+
featureInfo.storage = {
215+
rulesFilename: features.storage.rules_filename,
216+
rules: features.storage.rules,
217+
writeRules: true,
218+
};
219+
}
220+
if (features.emulators) {
221+
featuresList.push("emulators");
222+
const emulatorKeys = Object.keys(features.emulators).filter(
223+
(key) =>
224+
key !== "ui" &&
225+
key !== "singleProjectMode" &&
226+
Object.values(Emulators).includes(key as Emulators),
227+
) as Emulators[];
228+
229+
featureInfo.emulators = {
230+
emulators: emulatorKeys,
231+
config: features.emulators,
232+
download: true, // Non-interactive, so default to downloading.
233+
};
234+
}
235+
181236
const setup: Setup = {
182-
config: config?.src,
237+
config: config?.src || {},
183238
rcfile: rc?.data,
184239
projectId: projectId,
185240
features: [...featuresList],
@@ -206,4 +261,4 @@ To get started:
206261
`,
207262
);
208263
},
209-
);
264+
);

0 commit comments

Comments
 (0)