Skip to content

Commit f7d55bb

Browse files
committed
Initial scaffold for Firebase MCP Server.
1 parent 951ec02 commit f7d55bb

17 files changed

+1502
-355
lines changed

npm-shrinkwrap.json

+1,058-193
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "Command-Line Interface for Firebase",
55
"main": "./lib/index.js",
66
"bin": {
7-
"firebase": "./lib/bin/firebase.js"
7+
"firebase": "./lib/bin/firebase.js",
8+
"firebase-mcp": "./lib/bin/firebase-mcp.js"
89
},
910
"scripts": {
1011
"build": "tsc && npm run copyfiles",
@@ -103,6 +104,7 @@
103104
"@electric-sql/pglite": "^0.2.16",
104105
"@google-cloud/cloud-sql-connector": "^1.3.3",
105106
"@google-cloud/pubsub": "^4.5.0",
107+
"@modelcontextprotocol/sdk": "^1.10.2",
106108
"abort-controller": "^3.0.0",
107109
"ajv": "^8.17.1",
108110
"ajv-formats": "3.0.1",

src/bin/cli.ts

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import * as updateNotifierPkg from "update-notifier-cjs";
2+
import * as clc from "colorette";
3+
import { markedTerminal } from "marked-terminal";
4+
import { marked } from "marked";
5+
marked.use(markedTerminal() as any);
6+
7+
import { CommanderStatic } from "commander";
8+
import { join } from "node:path";
9+
import { SPLAT } from "triple-beam";
10+
import { stripVTControlCharacters } from "node:util";
11+
import * as fs from "node:fs";
12+
13+
import { configstore } from "../configstore";
14+
import { errorOut } from "../errorOut";
15+
import { handlePreviewToggles } from "../handlePreviewToggles";
16+
import { logger } from "../logger";
17+
import * as client from "..";
18+
import * as fsutils from "../fsutils";
19+
import * as utils from "../utils";
20+
import * as winston from "winston";
21+
22+
import { enableExperimentsFromCliEnvVariable } from "../experiments";
23+
import { fetchMOTD } from "../fetchMOTD";
24+
25+
function cli(pkg: any) {
26+
const updateNotifier = updateNotifierPkg({ pkg });
27+
28+
const args = process.argv.slice(2);
29+
let cmd: CommanderStatic;
30+
31+
function findAvailableLogFile(): string {
32+
const candidates = ["firebase-debug.log"];
33+
for (let i = 1; i < 10; i++) {
34+
candidates.push(`firebase-debug.${i}.log`);
35+
}
36+
37+
for (const c of candidates) {
38+
const logFilename = join(process.cwd(), c);
39+
40+
try {
41+
const fd = fs.openSync(logFilename, "r+");
42+
fs.closeSync(fd);
43+
return logFilename;
44+
} catch (e: any) {
45+
if (e.code === "ENOENT") {
46+
// File does not exist, which is fine
47+
return logFilename;
48+
}
49+
50+
// Any other error (EPERM, etc) means we won't be able to log to
51+
// this file so we skip it.
52+
}
53+
}
54+
55+
throw new Error("Unable to obtain permissions for firebase-debug.log");
56+
}
57+
58+
const logFilename = findAvailableLogFile();
59+
60+
if (!process.env.DEBUG && args.includes("--debug")) {
61+
process.env.DEBUG = "true";
62+
}
63+
64+
process.env.IS_FIREBASE_CLI = "true";
65+
66+
logger.add(
67+
new winston.transports.File({
68+
level: "debug",
69+
filename: logFilename,
70+
format: winston.format.printf((info) => {
71+
const segments = [info.message, ...(info[SPLAT] || [])].map(utils.tryStringify);
72+
return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`;
73+
}),
74+
}),
75+
);
76+
77+
logger.debug("-".repeat(70));
78+
logger.debug("Command: ", process.argv.join(" "));
79+
logger.debug("CLI Version: ", pkg.version);
80+
logger.debug("Platform: ", process.platform);
81+
logger.debug("Node Version: ", process.version);
82+
logger.debug("Time: ", new Date().toString());
83+
if (utils.envOverrides.length) {
84+
logger.debug("Env Overrides:", utils.envOverrides.join(", "));
85+
}
86+
logger.debug("-".repeat(70));
87+
logger.debug();
88+
89+
enableExperimentsFromCliEnvVariable();
90+
fetchMOTD();
91+
92+
process.on("exit", (code) => {
93+
code = process.exitCode || code;
94+
if (!process.env.DEBUG && code < 2 && fsutils.fileExistsSync(logFilename)) {
95+
fs.unlinkSync(logFilename);
96+
}
97+
98+
if (code > 0 && process.stdout.isTTY) {
99+
const lastError = configstore.get("lastError") || 0;
100+
const timestamp = Date.now();
101+
if (lastError > timestamp - 120000) {
102+
let help;
103+
if (code === 1 && cmd) {
104+
help = "Having trouble? Try " + clc.bold("firebase [command] --help");
105+
} else {
106+
help = "Having trouble? Try again or contact support with contents of firebase-debug.log";
107+
}
108+
109+
if (cmd) {
110+
console.log();
111+
console.log(help);
112+
}
113+
}
114+
configstore.set("lastError", timestamp);
115+
} else {
116+
configstore.delete("lastError");
117+
}
118+
119+
// Notify about updates right before process exit.
120+
try {
121+
const installMethod = !process.env.FIREPIT_VERSION ? "npm" : "automatic script";
122+
const updateCommand = !process.env.FIREPIT_VERSION
123+
? "npm install -g firebase-tools"
124+
: "curl -sL https://firebase.tools | upgrade=true bash";
125+
126+
const updateMessage =
127+
`Update available ${clc.gray("{currentVersion}")}${clc.green("{latestVersion}")}\n` +
128+
`To update to the latest version using ${installMethod}, run\n${clc.cyan(updateCommand)}\n` +
129+
`For other CLI management options, visit the ${marked(
130+
"[CLI documentation](https://firebase.google.com/docs/cli#update-cli)",
131+
)}`;
132+
// `defer: true` would interfere with commands that perform tasks (emulators etc.)
133+
// before exit since it installs a SIGINT handler that immediately exits. See:
134+
// https://github.com/firebase/firebase-tools/issues/4981
135+
updateNotifier.notify({ defer: false, isGlobal: true, message: updateMessage });
136+
} catch (err) {
137+
// This is not a fatal error -- let's debug log, swallow, and exit cleanly.
138+
logger.debug("Error when notifying about new CLI updates:");
139+
if (err instanceof Error) {
140+
logger.debug(err);
141+
} else {
142+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
143+
logger.debug(`${err}`);
144+
}
145+
}
146+
});
147+
148+
process.on("uncaughtException", (err) => {
149+
errorOut(err);
150+
});
151+
152+
if (!handlePreviewToggles(args)) {
153+
// determine if there are any arguments. if not, display help
154+
if (!args.length) {
155+
client.cli.help();
156+
} else {
157+
cmd = client.cli.parse(process.argv);
158+
}
159+
}
160+
}

src/bin/firebase.ts

+7-156
Original file line numberDiff line numberDiff line change
@@ -11,160 +11,11 @@ if (!semver.satisfies(nodeVersion, pkg.engines.node)) {
1111
process.exit(1);
1212
}
1313

14-
import * as updateNotifierPkg from "update-notifier-cjs";
15-
import * as clc from "colorette";
16-
import { markedTerminal } from "marked-terminal";
17-
const updateNotifier = updateNotifierPkg({ pkg });
18-
import { marked } from "marked";
19-
marked.use(markedTerminal() as any);
20-
21-
import { CommanderStatic } from "commander";
22-
import { join } from "node:path";
23-
import { SPLAT } from "triple-beam";
24-
import { stripVTControlCharacters } from "node:util";
25-
import * as fs from "node:fs";
26-
27-
import { configstore } from "../configstore";
28-
import { errorOut } from "../errorOut";
29-
import { handlePreviewToggles } from "../handlePreviewToggles";
30-
import { logger } from "../logger";
31-
import * as client from "..";
32-
import * as fsutils from "../fsutils";
33-
import * as utils from "../utils";
34-
import * as winston from "winston";
35-
36-
const args = process.argv.slice(2);
37-
let cmd: CommanderStatic;
38-
39-
function findAvailableLogFile(): string {
40-
const candidates = ["firebase-debug.log"];
41-
for (let i = 1; i < 10; i++) {
42-
candidates.push(`firebase-debug.${i}.log`);
43-
}
44-
45-
for (const c of candidates) {
46-
const logFilename = join(process.cwd(), c);
47-
48-
try {
49-
const fd = fs.openSync(logFilename, "r+");
50-
fs.closeSync(fd);
51-
return logFilename;
52-
} catch (e: any) {
53-
if (e.code === "ENOENT") {
54-
// File does not exist, which is fine
55-
return logFilename;
56-
}
57-
58-
// Any other error (EPERM, etc) means we won't be able to log to
59-
// this file so we skip it.
60-
}
61-
}
62-
63-
throw new Error("Unable to obtain permissions for firebase-debug.log");
64-
}
65-
66-
const logFilename = findAvailableLogFile();
67-
68-
if (!process.env.DEBUG && args.includes("--debug")) {
69-
process.env.DEBUG = "true";
70-
}
71-
72-
process.env.IS_FIREBASE_CLI = "true";
73-
74-
logger.add(
75-
new winston.transports.File({
76-
level: "debug",
77-
filename: logFilename,
78-
format: winston.format.printf((info) => {
79-
const segments = [info.message, ...(info[SPLAT] || [])].map(utils.tryStringify);
80-
return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`;
81-
}),
82-
}),
83-
);
84-
85-
logger.debug("-".repeat(70));
86-
logger.debug("Command: ", process.argv.join(" "));
87-
logger.debug("CLI Version: ", pkg.version);
88-
logger.debug("Platform: ", process.platform);
89-
logger.debug("Node Version: ", process.version);
90-
logger.debug("Time: ", new Date().toString());
91-
if (utils.envOverrides.length) {
92-
logger.debug("Env Overrides:", utils.envOverrides.join(", "));
93-
}
94-
logger.debug("-".repeat(70));
95-
logger.debug();
96-
97-
import { enableExperimentsFromCliEnvVariable } from "../experiments";
98-
import { fetchMOTD } from "../fetchMOTD";
99-
100-
enableExperimentsFromCliEnvVariable();
101-
fetchMOTD();
102-
103-
process.on("exit", (code) => {
104-
code = process.exitCode || code;
105-
if (!process.env.DEBUG && code < 2 && fsutils.fileExistsSync(logFilename)) {
106-
fs.unlinkSync(logFilename);
107-
}
108-
109-
if (code > 0 && process.stdout.isTTY) {
110-
const lastError = configstore.get("lastError") || 0;
111-
const timestamp = Date.now();
112-
if (lastError > timestamp - 120000) {
113-
let help;
114-
if (code === 1 && cmd) {
115-
help = "Having trouble? Try " + clc.bold("firebase [command] --help");
116-
} else {
117-
help = "Having trouble? Try again or contact support with contents of firebase-debug.log";
118-
}
119-
120-
if (cmd) {
121-
console.log();
122-
console.log(help);
123-
}
124-
}
125-
configstore.set("lastError", timestamp);
126-
} else {
127-
configstore.delete("lastError");
128-
}
129-
130-
// Notify about updates right before process exit.
131-
try {
132-
const installMethod = !process.env.FIREPIT_VERSION ? "npm" : "automatic script";
133-
const updateCommand = !process.env.FIREPIT_VERSION
134-
? "npm install -g firebase-tools"
135-
: "curl -sL https://firebase.tools | upgrade=true bash";
136-
137-
const updateMessage =
138-
`Update available ${clc.gray("{currentVersion}")}${clc.green("{latestVersion}")}\n` +
139-
`To update to the latest version using ${installMethod}, run\n${clc.cyan(updateCommand)}\n` +
140-
`For other CLI management options, visit the ${marked(
141-
"[CLI documentation](https://firebase.google.com/docs/cli#update-cli)",
142-
)}`;
143-
// `defer: true` would interfere with commands that perform tasks (emulators etc.)
144-
// before exit since it installs a SIGINT handler that immediately exits. See:
145-
// https://github.com/firebase/firebase-tools/issues/4981
146-
updateNotifier.notify({ defer: false, isGlobal: true, message: updateMessage });
147-
} catch (err) {
148-
// This is not a fatal error -- let's debug log, swallow, and exit cleanly.
149-
logger.debug("Error when notifying about new CLI updates:");
150-
if (err instanceof Error) {
151-
logger.debug(err);
152-
} else {
153-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
154-
logger.debug(`${err}`);
155-
}
156-
}
157-
});
158-
159-
process.on("uncaughtException", (err) => {
160-
errorOut(err);
161-
});
162-
163-
if (!handlePreviewToggles(args)) {
164-
// determine if there are any arguments. if not, display help
165-
if (!args.length) {
166-
client.cli.help();
167-
} else {
168-
cmd = client.cli.parse(process.argv);
169-
}
14+
// we short-circuit the normal process for MCP
15+
if (process.argv[2] === "mcp") {
16+
const { mcp } = require("./mcp");
17+
mcp();
18+
} else {
19+
const { cli } = require("./cli");
20+
cli(pkg);
17021
}

src/bin/mcp.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env node
2+
3+
import { Command } from "../command";
4+
import { requireAuth } from "../requireAuth";
5+
6+
const { silenceStdout } = require("../logger");
7+
silenceStdout();
8+
9+
const { FirebaseMcpServer } = require("../mcp/index");
10+
11+
const cmd = new Command("mcp").before(requireAuth);
12+
13+
export async function mcp() {
14+
const options: any = {};
15+
await cmd.prepare(options);
16+
const server = new FirebaseMcpServer({ cliOptions: options });
17+
server.start();
18+
}

src/commands/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export function load(client: any): any {
195195
client.login.list = loadCommand("login-list");
196196
client.login.use = loadCommand("login-use");
197197
client.logout = loadCommand("logout");
198+
client.mcp = loadCommand("mcp");
198199
client.open = loadCommand("open");
199200
client.projects = {};
200201
client.projects.addfirebase = loadCommand("projects-addfirebase");

0 commit comments

Comments
 (0)