firebase-tools
Version:
Command-Line Interface for Firebase
417 lines (416 loc) • 18.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FirebaseMcpServer = void 0;
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const util_1 = require("./util");
const types_1 = require("./types");
const index_1 = require("./tools/index");
const index_2 = require("./prompts/index");
const configstore_1 = require("../configstore");
const command_1 = require("../command");
const requireAuth_1 = require("../requireAuth");
const projectUtils_1 = require("../projectUtils");
const errors_1 = require("./errors");
const track_1 = require("../track");
const config_1 = require("../config");
const rc_1 = require("../rc");
const hubClient_1 = require("../emulator/hubClient");
const node_fs_1 = require("node:fs");
const logging_transport_1 = require("./logging-transport");
const env_1 = require("../env");
const timeout_1 = require("../timeout");
const resources_1 = require("./resources");
const crossSpawn = require("cross-spawn");
const availability_1 = require("./util/availability");
const SERVER_VERSION = "0.3.0";
const cmd = new command_1.Command("mcp");
const orderedLogLevels = [
"debug",
"info",
"notice",
"warning",
"error",
"critical",
"alert",
"emergency",
];
class FirebaseMcpServer {
async trackGA4(event, params = {}) {
var _a, _b;
if (!this.clientInfo)
await (0, timeout_1.timeoutFallback)(this.ready(), null, 2000);
const clientInfoParams = {
mcp_client_name: ((_a = this.clientInfo) === null || _a === void 0 ? void 0 : _a.name) || "<unknown-client>",
mcp_client_version: ((_b = this.clientInfo) === null || _b === void 0 ? void 0 : _b.version) || "<unknown-version>",
gemini_cli_extension: process.env.IS_GEMINI_CLI_EXTENSION ? "true" : "false",
};
return (0, track_1.trackGA4)(event, Object.assign(Object.assign({}, params), clientInfoParams));
}
constructor(options) {
this._ready = false;
this._readyPromises = [];
this._pendingMessages = [];
this.currentLogLevel = process.env.FIREBASE_MCP_DEBUG_LOG ? "debug" : undefined;
this.activeFeatures = options.activeFeatures;
this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT;
this.server = new index_js_1.Server({ name: "firebase", version: SERVER_VERSION });
this.server.registerCapabilities({
tools: { listChanged: true },
logging: {},
prompts: { listChanged: true },
resources: {},
});
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, this.mcpListTools.bind(this));
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, this.mcpCallTool.bind(this));
this.server.setRequestHandler(types_js_1.ListPromptsRequestSchema, this.mcpListPrompts.bind(this));
this.server.setRequestHandler(types_js_1.GetPromptRequestSchema, this.mcpGetPrompt.bind(this));
this.server.setRequestHandler(types_js_1.ListResourceTemplatesRequestSchema, this.mcpListResourceTemplates.bind(this));
this.server.setRequestHandler(types_js_1.ListResourcesRequestSchema, this.mcpListResources.bind(this));
this.server.setRequestHandler(types_js_1.ReadResourceRequestSchema, this.mcpReadResource.bind(this));
const onInitialized = () => {
var _a, _b;
const clientInfo = this.server.getClientVersion();
this.clientInfo = clientInfo;
if (clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.name) {
void this.trackGA4("mcp_client_connected");
}
if (!((_a = this.clientInfo) === null || _a === void 0 ? void 0 : _a.name))
this.clientInfo = { name: "<unknown-client>" };
this._ready = true;
while (this._readyPromises.length) {
(_b = this._readyPromises.pop()) === null || _b === void 0 ? void 0 : _b.resolve();
}
};
this.server.oninitialized = () => {
void onInitialized();
};
this.server.setRequestHandler(types_js_1.SetLevelRequestSchema, async ({ params }) => {
this.currentLogLevel = params.level;
return {};
});
void this.detectProjectSetup();
}
ready() {
if (this._ready)
return Promise.resolve();
return new Promise((resolve, reject) => {
this._readyPromises.push({ resolve: resolve, reject });
});
}
get clientName() {
var _a, _b;
return (_b = (_a = this.clientInfo) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : ((0, env_1.isFirebaseStudio)() ? "Firebase Studio" : "<unknown-client>");
}
get clientConfigKey() {
return `mcp.clientConfigs.${this.clientName}:${this.startupRoot || process.cwd()}`;
}
getStoredClientConfig() {
return configstore_1.configstore.get(this.clientConfigKey) || {};
}
updateStoredClientConfig(update) {
const config = configstore_1.configstore.get(this.clientConfigKey) || {};
const newConfig = Object.assign(Object.assign({}, config), update);
configstore_1.configstore.set(this.clientConfigKey, newConfig);
return newConfig;
}
async detectProjectSetup() {
await this.detectProjectRoot();
await this.detectActiveFeatures();
}
async detectProjectRoot() {
await (0, timeout_1.timeoutFallback)(this.ready(), null, 2000);
if (this.cachedProjectDir)
return this.cachedProjectDir;
const storedRoot = this.getStoredClientConfig().projectRoot;
this.cachedProjectDir = storedRoot || this.startupRoot || process.cwd();
this.logger.debug(`detected and cached project root: ${this.cachedProjectDir}`);
return this.cachedProjectDir;
}
async detectActiveFeatures() {
var _a;
if ((_a = this.detectedFeatures) === null || _a === void 0 ? void 0 : _a.length)
return this.detectedFeatures;
this.logger.debug("detecting active features of Firebase MCP server...");
const projectId = (await this.getProjectId()) || "";
const accountEmail = await this.getAuthenticatedUser();
const ctx = this._createMcpContext(projectId, accountEmail);
const detected = await Promise.all(types_1.SERVER_FEATURES.map(async (f) => {
const availabilityCheck = (0, availability_1.getDefaultFeatureAvailabilityCheck)(f);
if (await availabilityCheck(ctx))
return f;
return null;
}));
this.detectedFeatures = detected.filter((f) => !!f);
this.logger.debug(`detected features of Firebase MCP server: ${this.detectedFeatures.join(", ") || "<none>"}`);
return this.detectedFeatures;
}
async getEmulatorHubClient() {
if (this.emulatorHubClient) {
return this.emulatorHubClient;
}
const projectId = await this.getProjectId();
this.emulatorHubClient = new hubClient_1.EmulatorHubClient(projectId);
return this.emulatorHubClient;
}
async getEmulatorUrl(emulatorType) {
const hubClient = await this.getEmulatorHubClient();
if (!hubClient) {
throw Error("Emulator Hub not found or is not running. You can start the emulator by running `firebase emulators:start` in your firebase project directory.");
}
const emulators = await hubClient.getEmulators();
const emulatorInfo = emulators[emulatorType];
if (!emulatorInfo) {
throw Error(`No ${emulatorType} Emulator found running. Make sure your project firebase.json file includes ${emulatorType} and then rerun emulator using \`firebase emulators:start\` from your project directory.`);
}
const host = emulatorInfo.host.includes(":") ? `[${emulatorInfo.host}]` : emulatorInfo.host;
return `http://${host}:${emulatorInfo.port}`;
}
async getAvailableTools() {
var _a;
const features = ((_a = this.activeFeatures) === null || _a === void 0 ? void 0 : _a.length) ? this.activeFeatures : this.detectedFeatures;
const projectId = (await this.getProjectId()) || "";
const accountEmail = await this.getAuthenticatedUser();
const ctx = this._createMcpContext(projectId, accountEmail);
return (0, index_1.availableTools)(ctx, features);
}
async getTool(name) {
const tools = await this.getAvailableTools();
return tools.find((t) => t.mcp.name === name) || null;
}
async getAvailablePrompts() {
var _a;
const features = ((_a = this.activeFeatures) === null || _a === void 0 ? void 0 : _a.length) ? this.activeFeatures : this.detectedFeatures;
const projectId = (await this.getProjectId()) || "";
const accountEmail = await this.getAuthenticatedUser();
const ctx = this._createMcpContext(projectId, accountEmail);
return (0, index_2.availablePrompts)(ctx, features);
}
async getPrompt(name) {
const prompts = await this.getAvailablePrompts();
return prompts.find((p) => p.mcp.name === name) || null;
}
setProjectRoot(newRoot) {
this.updateStoredClientConfig({ projectRoot: newRoot });
this.cachedProjectDir = newRoot || undefined;
this.detectedFeatures = undefined;
void this.server.sendToolListChanged();
void this.server.sendPromptListChanged();
}
async resolveOptions() {
const options = { cwd: this.cachedProjectDir, isMCP: true };
await cmd.prepare(options);
return options;
}
async getProjectId() {
return (0, projectUtils_1.getProjectId)(await this.resolveOptions());
}
async getAuthenticatedUser(skipAutoAuth = false) {
try {
this.logger.debug("calling requireAuth");
const email = await (0, requireAuth_1.requireAuth)(await this.resolveOptions(), skipAutoAuth);
this.logger.debug(`detected authenticated account: ${email || "<none>"}`);
return email !== null && email !== void 0 ? email : (skipAutoAuth ? null : "Application Default Credentials");
}
catch (e) {
this.logger.debug(`error in requireAuth: ${e}`);
return null;
}
}
_createMcpContext(projectId, accountEmail) {
const options = { projectDir: this.cachedProjectDir, cwd: this.cachedProjectDir };
return {
projectId: projectId,
host: this,
config: config_1.Config.load(options, true) || new config_1.Config({}, options),
rc: (0, rc_1.loadRC)(options),
accountEmail,
firebaseCliCommand: this._getFirebaseCliCommand(),
};
}
_getFirebaseCliCommand() {
if (!this.cliCommand) {
const testCommand = crossSpawn.sync("firebase --version");
this.cliCommand = testCommand.error ? "npx firebase-tools@latest" : "firebase";
}
return this.cliCommand;
}
async mcpListTools() {
await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]);
const hasActiveProject = !!(await this.getProjectId());
await this.trackGA4("mcp_list_tools");
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
this.logger.debug(`skip auto-auth in studio environment: ${skipAutoAuthForStudio}`);
const availableTools = await this.getAvailableTools();
return {
tools: availableTools.map((t) => t.mcp),
_meta: {
projectRoot: this.cachedProjectDir,
projectDetected: hasActiveProject,
authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio),
activeFeatures: this.activeFeatures,
detectedFeatures: this.detectedFeatures,
},
};
}
async mcpCallTool(request) {
var _a, _b, _c, _d;
await this.detectProjectRoot();
const toolName = request.params.name;
const toolArgs = request.params.arguments;
const tool = await this.getTool(toolName);
if (!tool)
throw new Error(`Tool '${toolName}' could not be found.`);
if (!((_a = tool.mcp._meta) === null || _a === void 0 ? void 0 : _a.optionalProjectDir)) {
if (!this.cachedProjectDir || !(0, node_fs_1.existsSync)(this.cachedProjectDir)) {
return (0, errors_1.noProjectDirectory)(this.cachedProjectDir);
}
}
let projectId = await this.getProjectId();
if (((_b = tool.mcp._meta) === null || _b === void 0 ? void 0 : _b.requiresProject) && !projectId) {
return errors_1.NO_PROJECT_ERROR;
}
projectId = projectId || "";
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
if (((_c = tool.mcp._meta) === null || _c === void 0 ? void 0 : _c.requiresAuth) && !accountEmail) {
return (0, errors_1.mcpAuthError)(skipAutoAuthForStudio);
}
if ((_d = tool.mcp._meta) === null || _d === void 0 ? void 0 : _d.requiresGemini) {
const err = await (0, errors_1.requireGeminiToS)(projectId);
if (err)
return err;
}
const toolsCtx = this._createMcpContext(projectId, accountEmail);
try {
const res = await tool.fn(toolArgs, toolsCtx);
await this.trackGA4("mcp_tool_call", {
tool_name: toolName,
error: res.isError ? 1 : 0,
});
return res;
}
catch (err) {
await this.trackGA4("mcp_tool_call", {
tool_name: toolName,
error: 1,
});
return (0, util_1.mcpError)(err);
}
}
async mcpListPrompts() {
await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]);
const hasActiveProject = !!(await this.getProjectId());
await this.trackGA4("mcp_list_prompts");
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
return {
prompts: (await this.getAvailablePrompts()).map((p) => ({
name: p.mcp.name,
description: p.mcp.description,
annotations: p.mcp.annotations,
arguments: p.mcp.arguments,
})),
_meta: {
projectRoot: this.cachedProjectDir,
projectDetected: hasActiveProject,
authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio),
activeFeatures: this.activeFeatures,
detectedFeatures: this.detectedFeatures,
},
};
}
async mcpGetPrompt(req) {
await this.detectProjectRoot();
const promptName = req.params.name;
const promptArgs = req.params.arguments || {};
const prompt = await this.getPrompt(promptName);
if (!prompt) {
throw new Error(`Prompt '${promptName}' could not be found.`);
}
let projectId = await this.getProjectId();
projectId = projectId || "";
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
const promptsCtx = this._createMcpContext(projectId, accountEmail);
try {
const messages = await prompt.fn(promptArgs, promptsCtx);
await this.trackGA4("mcp_get_prompt", {
tool_name: promptName,
});
return {
messages,
};
}
catch (err) {
await this.trackGA4("mcp_get_prompt", {
tool_name: promptName,
error: 1,
});
throw err;
}
}
async mcpListResources() {
await (0, track_1.trackGA4)("mcp_read_resource", { resource_name: "__list__" });
return {
resources: resources_1.resources.map((r) => r.mcp),
};
}
async mcpListResourceTemplates() {
return {
resourceTemplates: resources_1.resourceTemplates.map((rt) => rt.mcp),
};
}
async mcpReadResource(req) {
let projectId = await this.getProjectId();
projectId = projectId || "";
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
const resourceCtx = this._createMcpContext(projectId, accountEmail);
const resolved = await (0, resources_1.resolveResource)(req.params.uri, resourceCtx);
if (!resolved) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Resource '${req.params.uri}' could not be found.`);
}
return resolved.result;
}
async start() {
const transport = process.env.FIREBASE_MCP_DEBUG_LOG
? new logging_transport_1.LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG)
: new stdio_js_1.StdioServerTransport();
await this.server.connect(transport);
}
get logger() {
const logAtLevel = (level, message) => {
let data = message;
if (typeof message === "string") {
data = { message };
}
if (!this.currentLogLevel) {
return;
}
if (orderedLogLevels.indexOf(this.currentLogLevel) > orderedLogLevels.indexOf(level)) {
return;
}
if (this._ready) {
while (this._pendingMessages.length) {
const message = this._pendingMessages.shift();
if (!message)
continue;
this.server.sendLoggingMessage({
level: message.level,
data: message.data,
});
}
void this.server.sendLoggingMessage({ level, data });
}
else {
this._pendingMessages.push({ level, data });
}
};
return Object.fromEntries(orderedLogLevels.map((logLevel) => [
logLevel,
(message) => logAtLevel(logLevel, message),
]));
}
}
exports.FirebaseMcpServer = FirebaseMcpServer;