Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 44 additions & 40 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,6 @@
// logging spec:
// https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging
currentLogLevel?: LoggingLevel = process.env.FIREBASE_MCP_DEBUG_LOG ? "debug" : undefined;
// 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<LoggingLevel, (message: unknown) => Promise<void>>;

/** Create a special tracking function to avoid blocking everything on initialization notification. */
private async trackGA4(
Expand Down Expand Up @@ -154,7 +147,7 @@
}

/** Wait until initialization has finished. */
ready() {

Check warning on line 150 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
if (this._ready) return Promise.resolve();
return new Promise((resolve, reject) => {
this._readyPromises.push({ resolve: resolve as () => void, reject });
Expand All @@ -165,19 +158,19 @@
return this.clientInfo?.name ?? (isFirebaseStudio() ? "Firebase Studio" : "<unknown-client>");
}

private get clientConfigKey() {

Check warning on line 161 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
return `mcp.clientConfigs.${this.clientName}:${this.startupRoot || process.cwd()}`;
}

getStoredClientConfig(): ClientConfig {
return configstore.get(this.clientConfigKey) || {};

Check warning on line 166 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value
}

updateStoredClientConfig(update: Partial<ClientConfig>) {

Check warning on line 169 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const config = configstore.get(this.clientConfigKey) || {};

Check warning on line 170 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
const newConfig = { ...config, ...update };

Check warning on line 171 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
configstore.set(this.clientConfigKey, newConfig);
return newConfig;

Check warning on line 173 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value
}

async detectProjectSetup(): Promise<void> {
Expand All @@ -191,13 +184,13 @@
if (this.cachedProjectDir) return this.cachedProjectDir;
const storedRoot = this.getStoredClientConfig().projectRoot;
this.cachedProjectDir = storedRoot || this.startupRoot || process.cwd();
this.log("debug", "detected and cached project root: " + this.cachedProjectDir);
this.logger.debug(`detected and cached project root: ${this.cachedProjectDir}`);

Check warning on line 187 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
return this.cachedProjectDir;
}

async detectActiveFeatures(): Promise<ServerFeature[]> {
if (this.detectedFeatures?.length) return this.detectedFeatures; // memoized
this.log("debug", "detecting active features of Firebase MCP server...");
this.logger.debug("detecting active features of Firebase MCP server...");

Check warning on line 193 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
const projectId = (await this.getProjectId()) || "";
const accountEmail = await this.getAuthenticatedUser();
const ctx = this._createMcpContext(projectId, accountEmail);
Expand All @@ -209,9 +202,8 @@
}),
);
this.detectedFeatures = detected.filter((f) => !!f) as ServerFeature[];
this.log(
"debug",
"detected features of Firebase MCP server: " + (this.detectedFeatures.join(", ") || "<none>"),
this.logger.debug(

Check warning on line 205 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
`detected features of Firebase MCP server: ${this.detectedFeatures.join(", ") || "<none>"}`,
);
return this.detectedFeatures;
}
Expand Down Expand Up @@ -295,12 +287,12 @@

async getAuthenticatedUser(skipAutoAuth: boolean = false): Promise<string | null> {
try {
this.log("debug", `calling requireAuth`);
this.logger.debug("calling requireAuth");
const email = await requireAuth(await this.resolveOptions(), skipAutoAuth);
this.log("debug", `detected authenticated account: ${email || "<none>"}`);
this.logger.debug(`detected authenticated account: ${email || "<none>"}`);
return email ?? (skipAutoAuth ? null : "Application Default Credentials");
} catch (e) {
this.log("debug", `error in requireAuth: ${e}`);
this.logger.debug(`error in requireAuth: ${e}`);
return null;
}
}
Expand Down Expand Up @@ -330,7 +322,7 @@
const hasActiveProject = !!(await this.getProjectId());
await this.trackGA4("mcp_list_tools");
const skipAutoAuthForStudio = isFirebaseStudio();
this.log("debug", `skip auto-auth in studio environment: ${skipAutoAuthForStudio}`);
this.logger.debug(`skip auto-auth in studio environment: ${skipAutoAuthForStudio}`);
const availableTools = await this.getAvailableTools();
return {
tools: availableTools.map((t) => t.mcp),
Expand Down Expand Up @@ -491,35 +483,47 @@
await this.server.connect(transport);
}

log(level: LoggingLevel, message: unknown): void {
let data = message;
get logger() {
const logAtLevel = (level: LoggingLevel, message: unknown): void => {
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 };
}
// 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 (!this.currentLogLevel) {
return;
}

if (orderedLogLevels.indexOf(this.currentLogLevel) > orderedLogLevels.indexOf(level)) {
return;
}
if (orderedLogLevels.indexOf(this.currentLogLevel) > orderedLogLevels.indexOf(level)) {
return;
}

if (this._ready) {
// once ready, flush all pending messages before sending the next message
// this should only happen during startup
while (this._pendingMessages.length) {
const message = this._pendingMessages.shift();
if (!message) continue;
this.server.sendLoggingMessage({ level: message.level, data: message.data });
if (this._ready) {
// once ready, flush all pending messages before sending the next message
// this should only happen during startup
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 });
}
};

void this.server.sendLoggingMessage({ level, data });
} else {
this._pendingMessages.push({ level, data });
}
return Object.fromEntries(
orderedLogLevels.map((logLevel) => [
logLevel,
(message: unknown) => logAtLevel(logLevel, message),
]),
) as Record<LoggingLevel, (message: unknown) => Promise<void>>;
}
}
2 changes: 1 addition & 1 deletion src/mcp/util/apptesting/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function isAppTestingAvailable(ctx: McpContext): Promise<boolean> {
const supportedPlatforms = [Platform.FLUTTER, Platform.ANDROID, Platform.IOS];

if (!platforms.some((p) => supportedPlatforms.includes(p))) {
host.log("debug", `Found no supported App Testing platforms.`);
host.logger.debug("Found no supported App Testing platforms.");
return false;
}

Expand Down
13 changes: 6 additions & 7 deletions src/mcp/util/crashlytics/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as path from "path";
* Returns a function that detects whether Crashlytics is available.
*/
export async function isCrashlyticsAvailable(ctx: McpContext): Promise<boolean> {
ctx.host.log("debug", `Looking for whether crashlytics is installed...`);
ctx.host.logger.debug("Looking for whether crashlytics is installed...");
return await isCrashlyticsInstalled(ctx);
}

Expand All @@ -22,25 +22,24 @@ async function isCrashlyticsInstalled(ctx: McpContext): Promise<boolean> {
!platforms.includes(Platform.ANDROID) &&
!platforms.includes(Platform.IOS)
) {
host.log("debug", `Found no supported Crashlytics platforms.`);
host.logger.debug("Found no supported Crashlytics platforms.");
return false;
}

if (platforms.includes(Platform.FLUTTER) && (await flutterAppUsesCrashlytics(projectDir))) {
host.log("debug", `Found Flutter app using Crashlytics`);
host.logger.debug("Found Flutter app using Crashlytics");
return true;
}
if (platforms.includes(Platform.ANDROID) && (await androidAppUsesCrashlytics(projectDir))) {
host.log("debug", `Found Android app using Crashlytics`);
host.logger.debug("Found Android app using Crashlytics");
return true;
}
if (platforms.includes(Platform.IOS) && (await iosAppUsesCrashlytics(projectDir))) {
host.log("debug", `Found iOS app using Crashlytics`);
host.logger.debug("Found iOS app using Crashlytics");
return true;
}

host.log(
"debug",
host.logger.debug(
`Found supported platforms ${JSON.stringify(platforms)}, but did not find a Crashlytics dependency.`,
);
return false;
Expand Down
Loading