firebase-tools
Version:
Command-Line Interface for Firebase
222 lines (221 loc) • 10.4 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_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;