UNPKG

firebase-tools

Version:
282 lines (281 loc) 12.9 kB
"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_js_1 = require("./util.js"); const types_js_2 = require("./types.js"); const index_js_2 = require("./tools/index.js"); const configstore_js_1 = require("../configstore.js"); const command_js_1 = require("../command.js"); const requireAuth_js_1 = require("../requireAuth.js"); const projectUtils_js_1 = require("../projectUtils.js"); const errors_js_1 = require("./errors.js"); const track_js_1 = require("../track.js"); const config_js_1 = require("../config.js"); const rc_js_1 = require("../rc.js"); const hubClient_js_1 = require("../emulator/hubClient.js"); const node_fs_1 = require("node:fs"); const ensureApiEnabled_js_1 = require("../ensureApiEnabled.js"); const api = require("../api.js"); const logging_transport_js_1 = require("./logging-transport.js"); const env_js_1 = require("../env.js"); const timeout_js_1 = require("../timeout.js"); const SERVER_VERSION = "0.2.0"; const cmd = new command_js_1.Command("experimental: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_js_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>", }; (0, track_js_1.trackGA4)(event, Object.assign(Object.assign({}, params), clientInfoParams)); } constructor(options) { this._ready = false; this._readyPromises = []; this.currentLogLevel = process.env.FIREBASE_MCP_DEBUG_LOG ? "debug" : undefined; this.logger = Object.fromEntries(orderedLogLevels.map((logLevel) => [ logLevel, (message) => this.log(logLevel, message), ])); 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: {} }); this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, this.mcpListTools.bind(this)); this.server.setRequestHandler(types_js_1.CallToolRequestSchema, this.mcpCallTool.bind(this)); this.server.oninitialized = async () => { var _a, _b; const clientInfo = this.server.getClientVersion(); this.clientInfo = clientInfo; if (clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.name) { 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.setRequestHandler(types_js_1.SetLevelRequestSchema, async ({ params }) => { this.currentLogLevel = params.level; return {}; }); this.detectProjectRoot(); this.detectActiveFeatures(); } 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_js_1.isFirebaseStudio)() ? "Firebase Studio" : "<unknown-client>"); } get clientConfigKey() { return `mcp.clientConfigs.${this.clientName}:${this.startupRoot || process.cwd()}`; } getStoredClientConfig() { return configstore_js_1.configstore.get(this.clientConfigKey) || {}; } updateStoredClientConfig(update) { const config = configstore_js_1.configstore.get(this.clientConfigKey) || {}; const newConfig = Object.assign(Object.assign({}, config), update); configstore_js_1.configstore.set(this.clientConfigKey, newConfig); return newConfig; } async detectProjectRoot() { await (0, timeout_js_1.timeoutFallback)(this.ready(), null, 2000); if (this.cachedProjectRoot) return this.cachedProjectRoot; const storedRoot = this.getStoredClientConfig().projectRoot; this.cachedProjectRoot = storedRoot || this.startupRoot || process.cwd(); this.log("debug", "detected and cached project root: " + this.cachedProjectRoot); return this.cachedProjectRoot; } async detectActiveFeatures() { var _a; if ((_a = this.detectedFeatures) === null || _a === void 0 ? void 0 : _a.length) return this.detectedFeatures; this.log("debug", "detecting active features of Firebase MCP server..."); const options = await this.resolveOptions(); const projectId = await this.getProjectId(); const detected = await Promise.all(types_js_2.SERVER_FEATURES.map(async (f) => { if (await (0, util_js_1.checkFeatureActive)(f, projectId, options)) return f; return null; })); this.detectedFeatures = detected.filter((f) => !!f); this.log("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(); if (!projectId) { return; } this.emulatorHubClient = new hubClient_js_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 Firestore Emulator found running. Make sure your project firebase.json file includes firestore 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}`; } get availableTools() { var _a; return (0, index_js_2.availableTools)(((_a = this.activeFeatures) === null || _a === void 0 ? void 0 : _a.length) ? this.activeFeatures : this.detectedFeatures); } getTool(name) { return this.availableTools.find((t) => t.mcp.name === name) || null; } setProjectRoot(newRoot) { this.updateStoredClientConfig({ projectRoot: newRoot }); this.cachedProjectRoot = newRoot || undefined; this.detectedFeatures = undefined; void this.server.sendToolListChanged(); } async resolveOptions() { const options = { cwd: this.cachedProjectRoot, isMCP: true }; await cmd.prepare(options); return options; } async getProjectId() { return (0, projectUtils_js_1.getProjectId)(await this.resolveOptions()); } async getAuthenticatedUser(skipAutoAuth = false) { try { this.log("debug", `calling requireAuth`); const email = await (0, requireAuth_js_1.requireAuth)(await this.resolveOptions(), skipAutoAuth); this.log("debug", `detected authenticated account: ${email || "<none>"}`); return (email !== null && email !== void 0 ? email : skipAutoAuth) ? null : "Application Default Credentials"; } catch (e) { this.log("debug", `error in requireAuth: ${e}`); return null; } } async mcpListTools() { await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]); const hasActiveProject = !!(await this.getProjectId()); await this.trackGA4("mcp_list_tools"); const skipAutoAuthForStudio = (0, env_js_1.isFirebaseStudio)(); this.log("debug", `skip auto-auth in studio environment: ${skipAutoAuthForStudio}`); return { tools: this.availableTools.map((t) => t.mcp), _meta: { projectRoot: this.cachedProjectRoot, projectDetected: hasActiveProject, authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio), activeFeatures: this.activeFeatures, detectedFeatures: this.detectedFeatures, }, }; } async mcpCallTool(request) { var _a, _b, _c; await this.detectProjectRoot(); const toolName = request.params.name; const toolArgs = request.params.arguments; const tool = this.getTool(toolName); if (!tool) throw new Error(`Tool '${toolName}' could not be found.`); if (tool.mcp.name !== "firebase_update_environment" && (!this.cachedProjectRoot || !(0, node_fs_1.existsSync)(this.cachedProjectRoot))) { return (0, util_js_1.mcpError)(`The current project directory '${this.cachedProjectRoot || "<NO PROJECT DIRECTORY FOUND>"}' does not exist. Please use the 'update_firebase_environment' tool to target a different project directory.`); } let projectId = await this.getProjectId(); if (((_a = tool.mcp._meta) === null || _a === void 0 ? void 0 : _a.requiresProject) && !projectId) { return errors_js_1.NO_PROJECT_ERROR; } projectId = projectId || ""; const accountEmail = await this.getAuthenticatedUser(); if (((_b = tool.mcp._meta) === null || _b === void 0 ? void 0 : _b.requiresAuth) && !accountEmail) { return (0, errors_js_1.mcpAuthError)(); } if ((_c = tool.mcp._meta) === null || _c === void 0 ? void 0 : _c.requiresGemini) { if (configstore_js_1.configstore.get("gemini")) { await (0, ensureApiEnabled_js_1.ensure)(projectId, api.cloudAiCompanionOrigin(), ""); } else { if (!(await (0, ensureApiEnabled_js_1.check)(projectId, api.cloudAiCompanionOrigin(), ""))) { return (0, errors_js_1.mcpGeminiError)(projectId); } } } const options = { projectDir: this.cachedProjectRoot, cwd: this.cachedProjectRoot }; const toolsCtx = { projectId: projectId, host: this, config: config_js_1.Config.load(options, true) || new config_js_1.Config({}, options), rc: (0, rc_js_1.loadRC)(options), 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_js_1.mcpError)(err); } } async start() { const transport = process.env.FIREBASE_MCP_DEBUG_LOG ? new logging_transport_js_1.LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG) : new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); } async log(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) await this.server.sendLoggingMessage({ level, data }); } } exports.FirebaseMcpServer = FirebaseMcpServer;