From 9725bbc67e81d567128ec16ce90f2b268fccd332 Mon Sep 17 00:00:00 2001 From: oleina Date: Thu, 10 Jul 2025 16:40:22 -0700 Subject: [PATCH 1/6] implement mcp logging --- src/mcp/index.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 1711997673b..4a32b27588e 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -3,9 +3,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { CallToolRequest, CallToolRequestSchema, - CallToolResult, - ListToolsRequestSchema, ListToolsResult, + LoggingLevel, + SetLevelRequestSchema, + ListToolsRequestSchema, + CallToolResult, } from "@modelcontextprotocol/sdk/types.js"; import { checkFeatureActive, mcpError } from "./util.js"; import { ClientConfig, SERVER_FEATURES, ServerFeature } from "./types.js"; @@ -30,6 +32,17 @@ const SERVER_VERSION = "0.1.0"; const cmd = new Command("experimental:mcp").before(requireAuth); +const orderedLogLevels = [ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency", +] as const; + export class FirebaseMcpServer { private _ready: boolean = false; private _readyPromises: { resolve: () => void; reject: (err: unknown) => void }[] = []; @@ -41,11 +54,22 @@ export class FirebaseMcpServer { clientInfo?: { name?: string; version?: string }; emulatorHubClient?: EmulatorHubClient; + // logging spec: + // https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging + currentLogLevel?: LoggingLevel; + // the api of logging from a consumers perspective looks like `server.logger.warn("my warning")`. + public readonly logger = Object.fromEntries( + orderedLogLevels.map((logLevel) => [ + logLevel, + (message: unknown) => this.log(logLevel, message), + ]), + ) as Record Promise>; + constructor(options: { activeFeatures?: ServerFeature[]; projectRoot?: string }) { this.activeFeatures = options.activeFeatures; this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT; this.server = new Server({ name: "firebase", version: SERVER_VERSION }); - this.server.registerCapabilities({ tools: { listChanged: true } }); + this.server.registerCapabilities({ tools: { listChanged: true }, logging: {} }); this.server.setRequestHandler(ListToolsRequestSchema, this.mcpListTools.bind(this)); this.server.setRequestHandler(CallToolRequestSchema, this.mcpCallTool.bind(this)); this.server.oninitialized = async () => { @@ -64,6 +88,12 @@ export class FirebaseMcpServer { this._readyPromises.pop()?.resolve(); } }; + + this.server.setRequestHandler(SetLevelRequestSchema, async ({ params }) => { + this.currentLogLevel = params.level; + return {}; + }); + this.detectProjectRoot(); this.detectActiveFeatures(); } @@ -275,4 +305,24 @@ export class FirebaseMcpServer { const transport = new StdioServerTransport(); await this.server.connect(transport); } + + private async log(level: LoggingLevel, message: unknown) { + let data = message; + + // mcp protocol only takes jsons or it errors; for convienence, format + // a a string into a json. + if (typeof message === "string") { + data = { message }; + } + + if (!this.currentLogLevel) { + return; + } + + if (orderedLogLevels.indexOf(this.currentLogLevel) > orderedLogLevels.indexOf(level)) { + return; + } + + await this.server.sendLoggingMessage({ level, data }); + } } From da45a6b2ec7cadf06bca8791a9bd23e4eb07f873 Mon Sep 17 00:00:00 2001 From: oleina Date: Thu, 10 Jul 2025 17:53:30 +0000 Subject: [PATCH 2/6] some in-line docs for the mcp tool --- src/mcp/tool.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mcp/tool.ts b/src/mcp/tool.ts index 5523416ba8c..bd1e740c13c 100644 --- a/src/mcp/tool.ts +++ b/src/mcp/tool.ts @@ -21,9 +21,19 @@ export interface ServerTool { inputSchema: any; annotations?: { title?: string; + + // If this tool modifies data or not. readOnlyHint?: boolean; + + // this tool can destroy data. destructiveHint?: boolean; + + // this tool is safe to run multiple times. idempotentHint?: boolean; + + // If this is true, it connects to the internet or other open world + // systems. If false, the tool only performs actions in an enclosed + // system, such as your project. openWorldHint?: boolean; }; _meta?: { From 5034ab77b08260dcfc17c64c0b3e658efb1c730a Mon Sep 17 00:00:00 2001 From: oleina Date: Thu, 10 Jul 2025 17:53:30 +0000 Subject: [PATCH 3/6] [MCP] implement {get,set}_data and {validate,get}_rules for RTDB MCP --- src/mcp/tools/database/get_data.ts | 63 ++++++++++++++++++++++ src/mcp/tools/database/get_rules.ts | 46 ++++++++++++++++ src/mcp/tools/database/index.ts | 7 +++ src/mcp/tools/database/set_data.ts | 67 ++++++++++++++++++++++++ src/mcp/tools/database/set_rules.ts | 48 +++++++++++++++++ src/mcp/tools/database/validate_rules.ts | 50 ++++++++++++++++++ src/mcp/tools/index.ts | 2 + src/mcp/tools/rules/get_rules.ts | 2 +- src/mcp/types.ts | 1 + src/mcp/util.ts | 6 +++ src/rtdb.ts | 18 +++++-- 11 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 src/mcp/tools/database/get_data.ts create mode 100644 src/mcp/tools/database/get_rules.ts create mode 100644 src/mcp/tools/database/index.ts create mode 100644 src/mcp/tools/database/set_data.ts create mode 100644 src/mcp/tools/database/set_rules.ts create mode 100644 src/mcp/tools/database/validate_rules.ts diff --git a/src/mcp/tools/database/get_data.ts b/src/mcp/tools/database/get_data.ts new file mode 100644 index 00000000000..cf1547a7d9f --- /dev/null +++ b/src/mcp/tools/database/get_data.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import * as url from "node:url"; +import { Client } from "../../../apiv2"; +import { text } from "node:stream/consumers"; + +export const get_data = tool( + { + name: "get_data", + description: "Returns RTDB data from the specified location", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + ), + path: z.string().describe("The path to the data to read. (ex: /my/cool/path)"), + }), + annotations: { + title: "Get Realtime Database data", + readOnlyHint: true, + }, + + _meta: { + // it's possible that a user attempts to query a database that they aren't + // authed into: we should let the rules evaluate as the author intended. + // If they have written rules to leave paths public, then having mcp + // grab their data is perfectly valid. + requiresAuth: false, + requiresProject: false, + }, + }, + async ({ path, databaseUrl }, { projectId, host }) => { + if (!path.startsWith("/")) { + return mcpError(`paths must start with '/' (you passed ''${path}')`); + } + + const dbUrl = new url.URL( + databaseUrl + ? `${databaseUrl}/${path}.json` + : `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app/${path}.json`, + ); + + const client = new Client({ + urlPrefix: dbUrl.origin, + auth: true, + }); + + host.logger.debug(`sending read request to path '${path}' for url '${dbUrl.toString()}'`); + + const res = await client.request({ + method: "GET", + path: dbUrl.pathname, + responseType: "stream", + resolveOnHTTPError: true, + }); + + const content = await text(res.body); + return toContent(content); + }, +); diff --git a/src/mcp/tools/database/get_rules.ts b/src/mcp/tools/database/get_rules.ts new file mode 100644 index 00000000000..e9eb05d6cef --- /dev/null +++ b/src/mcp/tools/database/get_rules.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; +import { Client } from "../../../apiv2"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; + +export const get_rules = tool( + { + name: "get_rules", + description: "Get an RTDB database's rules", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + ), + }), + annotations: { + title: "Get Realtime Database rules", + readOnlyHint: true, + }, + + _meta: { + requiresAuth: false, + requiresProject: false, + }, + }, + async ({ databaseUrl }, { projectId }) => { + const dbUrl = + databaseUrl ?? `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`; + + const client = new Client({ urlPrefix: dbUrl }); + const response = await client.request({ + method: "GET", + path: "/.settings/rules.json", + responseType: "stream", + resolveOnHTTPError: true, + }); + if (response.status !== 200) { + return mcpError(`Failed to fetch current rules. Code: ${response.status}`); + } + + const rules = await response.response.text(); + return toContent(rules); + }, +); diff --git a/src/mcp/tools/database/index.ts b/src/mcp/tools/database/index.ts new file mode 100644 index 00000000000..f532461fa74 --- /dev/null +++ b/src/mcp/tools/database/index.ts @@ -0,0 +1,7 @@ +import type { ServerTool } from "../../tool"; +import { get_rules } from "./get_rules"; +import { get_data } from "./get_data"; +import { set_data } from "./set_data"; +import { validate_rules } from "./validate_rules"; + +export const realtimeDatabaseTools: ServerTool[] = [get_data, set_data, get_rules, validate_rules]; diff --git a/src/mcp/tools/database/set_data.ts b/src/mcp/tools/database/set_data.ts new file mode 100644 index 00000000000..964d058ce6e --- /dev/null +++ b/src/mcp/tools/database/set_data.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import * as url from "node:url"; +import { stringToStream } from "../../../utils"; +import { Client } from "../../../apiv2"; +import { getErrMsg } from "../../../error"; + +export const set_data = tool( + { + name: "set_data", + description: "Writes RTDB data to the specified location", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + ), + path: z.string().describe("The path to the data to read. (ex: /my/cool/path)"), + data: z.string().describe('The JSON to write. (ex: {"alphabet": ["a", "b", "c"]})'), + }), + annotations: { + title: "Set Realtime Database data", + readOnlyHint: false, + idempotentHint: true, + }, + + _meta: { + requiresAuth: false, + requiresProject: false, + }, + }, + async ({ path, databaseUrl, data }, { projectId, host }) => { + if (!path.startsWith("/")) { + return mcpError(`paths must start with '/' (you passed ''${path}')`); + } + + const dbUrl = new url.URL( + databaseUrl + ? `${databaseUrl}/${path}.json` + : `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app/${path}.json`, + ); + + const client = new Client({ + urlPrefix: dbUrl.origin, + auth: true, + }); + + const inStream = stringToStream(data); + + host.logger.debug(`sending write request to path '${path}' for url '${dbUrl.toString()}'`); + + try { + await client.request({ + method: "PUT", + path: dbUrl.pathname, + body: inStream, + }); + } catch (err: unknown) { + host.logger.debug(getErrMsg(err)); + return mcpError(`Unexpected error while setting data: ${getErrMsg(err)}`); + } + + return toContent("write successful!"); + }, +); diff --git a/src/mcp/tools/database/set_rules.ts b/src/mcp/tools/database/set_rules.ts new file mode 100644 index 00000000000..817284606a9 --- /dev/null +++ b/src/mcp/tools/database/set_rules.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { Client } from "../../../apiv2"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { updateRulesWithClient } from "../../../rtdb"; +import { getErrMsg } from "../../../error"; + +export const validate_rules = tool( + { + name: "validate_rules", + description: "Validates an RTDB database's rules", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + ), + rules: z + .string() + .describe('The rules object, as a string (ex: {".read": false, ".write": false})'), + }), + annotations: { + title: "Validate Realtime Database rules", + idempotentHint: true, + }, + + _meta: { + requiresAuth: true, + requiresProject: false, + }, + }, + async ({ databaseUrl, rules }, { projectId, host }) => { + const dbUrl = + databaseUrl ?? `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`; + + const client = new Client({ urlPrefix: dbUrl }); + + try { + await updateRulesWithClient(client, rules, { dryRun: true }); + } catch (e: unknown) { + host.logger.debug(`failed to update rules at url ${dbUrl}`); + return mcpError(getErrMsg(e)); + } + + return toContent("the inputted rules are valid!"); + }, +); diff --git a/src/mcp/tools/database/validate_rules.ts b/src/mcp/tools/database/validate_rules.ts new file mode 100644 index 00000000000..7f7405bec5a --- /dev/null +++ b/src/mcp/tools/database/validate_rules.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; +import { Client } from "../../../apiv2"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { updateRulesWithClient } from "../../../rtdb"; +import { getErrMsg } from "../../../error"; + +export const validate_rules = tool( + { + name: "validate_rules", + description: "Validates an RTDB database's rules", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + ), + rules: z + .string() + .describe( + 'The rules object, as a string (ex: {"rules": {".read": false, ".write": false}})', + ), + }), + annotations: { + title: "Validate Realtime Database rules", + idempotentHint: true, + }, + + _meta: { + requiresAuth: true, + requiresProject: false, + }, + }, + async ({ databaseUrl, rules }, { projectId, host }) => { + const dbUrl = + databaseUrl ?? `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`; + + const client = new Client({ urlPrefix: dbUrl }); + + try { + await updateRulesWithClient(client, rules, { dryRun: true }); + } catch (e: unknown) { + host.logger.debug(`failed to update rules at url ${dbUrl}`); + return mcpError(getErrMsg(e)); + } + + return toContent("the inputted rules are valid!"); + }, +); diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index 332a08838a6..7b0ba361368 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -9,6 +9,7 @@ import { messagingTools } from "./messaging/index.js"; import { remoteConfigTools } from "./remoteconfig/index.js"; import { crashlyticsTools } from "./crashlytics/index.js"; import { appHostingTools } from "./apphosting/index.js"; +import { realtimeDatabaseTools } from "./database/index.js"; /** availableTools returns the list of MCP tools available given the server flags */ export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] { @@ -32,6 +33,7 @@ const tools: Record = { remoteconfig: addFeaturePrefix("remoteconfig", remoteConfigTools), crashlytics: addFeaturePrefix("crashlytics", crashlyticsTools), apphosting: addFeaturePrefix("apphosting", appHostingTools), + database: addFeaturePrefix("database", realtimeDatabaseTools), }; function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] { diff --git a/src/mcp/tools/rules/get_rules.ts b/src/mcp/tools/rules/get_rules.ts index 034e2aab1bf..fc777076910 100644 --- a/src/mcp/tools/rules/get_rules.ts +++ b/src/mcp/tools/rules/get_rules.ts @@ -21,7 +21,7 @@ export function getRulesTool(productName: string, releaseName: string) { async (_, { projectId }) => { const rulesetName = await getLatestRulesetName(projectId, releaseName); if (!rulesetName) - return mcpError(`No active Firestore rules were found in project '${projectId}'`); + return mcpError(`No active ${productName} rules were found in project '${projectId}'`); const rules = await getRulesetContent(rulesetName); return toContent(rules[0].content); }, diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 784137b4ab4..aff2f7f421f 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -7,6 +7,7 @@ export const SERVER_FEATURES = [ "remoteconfig", "crashlytics", "apphosting", + "database", ] as const; export type ServerFeature = (typeof SERVER_FEATURES)[number]; diff --git a/src/mcp/util.ts b/src/mcp/util.ts index 256e139a91d..6cff04952d2 100644 --- a/src/mcp/util.ts +++ b/src/mcp/util.ts @@ -12,6 +12,7 @@ import { remoteConfigApiOrigin, storageOrigin, crashlyticsApiOrigin, + realtimeOrigin, } from "../api"; import { check } from "../ensureApiEnabled"; @@ -58,6 +59,10 @@ export function mcpError(message: Error | string | unknown, code?: string): Call }; } +/* + * Wraps a throwing function with a safe conversion to mcpError. + */ + /** * Checks if a command exists in the system. */ @@ -89,6 +94,7 @@ const SERVER_FEATURE_APIS: Record = { remoteconfig: remoteConfigApiOrigin(), crashlytics: crashlyticsApiOrigin(), apphosting: apphostingOrigin(), + database: realtimeOrigin(), }; /** diff --git a/src/rtdb.ts b/src/rtdb.ts index f3407f3beea..1a9d9aa4d4e 100644 --- a/src/rtdb.ts +++ b/src/rtdb.ts @@ -13,10 +13,6 @@ export async function updateRules( src: any, options: { dryRun?: boolean } = {}, ): Promise { - const queryParams: { dryRun?: string } = {}; - if (options.dryRun) { - queryParams.dryRun = "true"; - } const downstreamOptions: { instance: string; project: string; @@ -32,6 +28,20 @@ export async function updateRules( "", ); const client = new Client({ urlPrefix: origin }); + + return updateRulesWithClient(client, options); +} + +export async function updateRulesWithClient( + client: Client, + src: unknown, + options: { dryRun?: boolean } = {}, +) { + const queryParams: { dryRun?: string } = {}; + if (options.dryRun) { + queryParams.dryRun = "true"; + } + const response = await client.request({ method: "PUT", path: ".settings/rules.json", From b265398b71fd113610f0f04ddf325bcd72b17e07 Mon Sep 17 00:00:00 2001 From: oleina Date: Wed, 30 Jul 2025 13:11:27 -0700 Subject: [PATCH 4/6] review round 1: use path.join, fix mismatched url --- src/mcp/tools/database/get_data.ts | 18 +++++++++++------- src/mcp/tools/database/set_data.ts | 18 +++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/mcp/tools/database/get_data.ts b/src/mcp/tools/database/get_data.ts index cf1547a7d9f..ea88e39ecb3 100644 --- a/src/mcp/tools/database/get_data.ts +++ b/src/mcp/tools/database/get_data.ts @@ -4,6 +4,7 @@ import { mcpError, toContent } from "../../util"; import * as url from "node:url"; import { Client } from "../../../apiv2"; import { text } from "node:stream/consumers"; +import path from "node:path"; export const get_data = tool( { @@ -14,7 +15,7 @@ export const get_data = tool( .string() .optional() .describe( - "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebasedatabase.app. Can point to emulator URL (e.g. localhost:6000/)", ), path: z.string().describe("The path to the data to read. (ex: /my/cool/path)"), }), @@ -32,15 +33,18 @@ export const get_data = tool( requiresProject: false, }, }, - async ({ path, databaseUrl }, { projectId, host }) => { - if (!path.startsWith("/")) { - return mcpError(`paths must start with '/' (you passed ''${path}')`); + async ({ path: getPath, databaseUrl }, { projectId, host }) => { + if (!getPath.startsWith("/")) { + return mcpError(`paths must start with '/' (you passed ''${getPath}')`); } const dbUrl = new url.URL( databaseUrl - ? `${databaseUrl}/${path}.json` - : `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app/${path}.json`, + ? `${databaseUrl}/${getPath}.json` + : path.join( + `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`, + `${getPath}.json`, + ), ); const client = new Client({ @@ -48,7 +52,7 @@ export const get_data = tool( auth: true, }); - host.logger.debug(`sending read request to path '${path}' for url '${dbUrl.toString()}'`); + host.logger.debug(`sending read request to path '${getPath}' for url '${dbUrl.toString()}'`); const res = await client.request({ method: "GET", diff --git a/src/mcp/tools/database/set_data.ts b/src/mcp/tools/database/set_data.ts index 964d058ce6e..998e76a6703 100644 --- a/src/mcp/tools/database/set_data.ts +++ b/src/mcp/tools/database/set_data.ts @@ -5,6 +5,7 @@ import * as url from "node:url"; import { stringToStream } from "../../../utils"; import { Client } from "../../../apiv2"; import { getErrMsg } from "../../../error"; +import path from "node:path"; export const set_data = tool( { @@ -15,7 +16,7 @@ export const set_data = tool( .string() .optional() .describe( - "connect to the database at url. If omitted, use default database instance -default-rtdb.firebaseio.com. Can point to emulator URL (e.g. localhost:6000/)", + "connect to the database at url. If omitted, use default database instance -default-rtdb.us-central1.firebasedatabase.app. Can point to emulator URL (e.g. localhost:6000/)", ), path: z.string().describe("The path to the data to read. (ex: /my/cool/path)"), data: z.string().describe('The JSON to write. (ex: {"alphabet": ["a", "b", "c"]})'), @@ -31,15 +32,18 @@ export const set_data = tool( requiresProject: false, }, }, - async ({ path, databaseUrl, data }, { projectId, host }) => { - if (!path.startsWith("/")) { - return mcpError(`paths must start with '/' (you passed ''${path}')`); + async ({ path: setPath, databaseUrl, data }, { projectId, host }) => { + if (!setPath.startsWith("/")) { + return mcpError(`paths must start with '/' (you passed ''${setPath}')`); } const dbUrl = new url.URL( databaseUrl - ? `${databaseUrl}/${path}.json` - : `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app/${path}.json`, + ? `${databaseUrl}/${setPath}.json` + : path.join( + `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`, + `${setPath}.json`, + ), ); const client = new Client({ @@ -49,7 +53,7 @@ export const set_data = tool( const inStream = stringToStream(data); - host.logger.debug(`sending write request to path '${path}' for url '${dbUrl.toString()}'`); + host.logger.debug(`sending write request to path '${setPath}' for url '${dbUrl.toString()}'`); try { await client.request({ From 00d7692e548b7146aeda40892ee5fa009c8d2ced Mon Sep 17 00:00:00 2001 From: oleina Date: Wed, 30 Jul 2025 13:38:03 -0700 Subject: [PATCH 5/6] fix another typo I missed --- src/mcp/tools/database/validate_rules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/tools/database/validate_rules.ts b/src/mcp/tools/database/validate_rules.ts index 7f7405bec5a..53603472ed8 100644 --- a/src/mcp/tools/database/validate_rules.ts +++ b/src/mcp/tools/database/validate_rules.ts @@ -41,7 +41,7 @@ export const validate_rules = tool( try { await updateRulesWithClient(client, rules, { dryRun: true }); } catch (e: unknown) { - host.logger.debug(`failed to update rules at url ${dbUrl}`); + host.logger.debug(`failed to validate rules at url ${dbUrl}`); return mcpError(getErrMsg(e)); } From 671f28465eb1bd45568cd9555e6ebd3ea95655dd Mon Sep 17 00:00:00 2001 From: oleina Date: Wed, 30 Jul 2025 13:49:46 -0700 Subject: [PATCH 6/6] fix import to not have js suffix --- src/mcp/tools/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index 4bf861dccb4..83bc2647073 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -9,7 +9,7 @@ import { messagingTools } from "./messaging/index"; import { remoteConfigTools } from "./remoteconfig/index"; import { crashlyticsTools } from "./crashlytics/index"; import { appHostingTools } from "./apphosting/index"; -import { realtimeDatabaseTools } from "./database/index.js"; +import { realtimeDatabaseTools } from "./database/index"; /** availableTools returns the list of MCP tools available given the server flags */ export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] {