UNPKG

firebase-tools

Version:
222 lines (221 loc) 10.4 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 SERVER_VERSION = "0.1.0"; const cmd = new command_js_1.Command("experimental:mcp").before(requireAuth_js_1.requireAuth); class FirebaseMcpServer { constructor(options) { this._ready = false; this._readyPromises = []; 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 } }); 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) { (0, track_js_1.trackGA4)("mcp_client_connected", { mcp_client_name: clientInfo.name, mcp_client_version: clientInfo.version, }); } 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.detectProjectRoot(); this.detectActiveFeatures(); } ready() { if (this._ready) return Promise.resolve(); return new Promise((resolve, reject) => { this._readyPromises.push({ resolve: resolve, reject }); }); } get clientConfigKey() { var _a; return `mcp.clientConfigs.${((_a = this.clientInfo) === null || _a === void 0 ? void 0 : _a.name) || "<unknown-client>"}:${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 this.ready(); if (this.cachedProjectRoot) return this.cachedProjectRoot; const storedRoot = this.getStoredClientConfig().projectRoot; this.cachedProjectRoot = storedRoot || this.startupRoot || process.cwd(); return this.cachedProjectRoot; } async detectActiveFeatures() { var _a; if ((_a = this.detectedFeatures) === null || _a === void 0 ? void 0 : _a.length) return this.detectedFeatures; 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); 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() { try { const email = await (0, requireAuth_js_1.requireAuth)(await this.resolveOptions()); return email !== null && email !== void 0 ? email : "Application Default Credentials"; } catch (e) { return null; } } async mcpListTools() { var _a, _b; await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]); const hasActiveProject = !!(await this.getProjectId()); await (0, track_js_1.trackGA4)("mcp_list_tools", { mcp_client_name: (_a = this.clientInfo) === null || _a === void 0 ? void 0 : _a.name, mcp_client_version: (_b = this.clientInfo) === null || _b === void 0 ? void 0 : _b.version, }); return { tools: this.availableTools.map((t) => t.mcp), _meta: { projectRoot: this.cachedProjectRoot, projectDetected: hasActiveProject, authenticatedUser: await this.getAuthenticatedUser(), activeFeatures: this.activeFeatures, detectedFeatures: this.detectedFeatures, }, }; } async mcpCallTool(request) { var _a, _b, _c, _d, _e, _f; 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)(); } 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 (0, track_js_1.trackGA4)("mcp_tool_call", { tool_name: toolName, error: res.isError ? 1 : 0, mcp_client_name: (_c = this.clientInfo) === null || _c === void 0 ? void 0 : _c.name, mcp_client_version: (_d = this.clientInfo) === null || _d === void 0 ? void 0 : _d.version, }); return res; } catch (err) { await (0, track_js_1.trackGA4)("mcp_tool_call", { tool_name: toolName, error: 1, mcp_client_name: (_e = this.clientInfo) === null || _e === void 0 ? void 0 : _e.name, mcp_client_version: (_f = this.clientInfo) === null || _f === void 0 ? void 0 : _f.version, }); return (0, util_js_1.mcpError)(err); } } async start() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); } } exports.FirebaseMcpServer = FirebaseMcpServer;