Skip to content

Commit d26423e

Browse files
authored
feat(mcp): Automatically detect features. (#8552)
1 parent 8e2f27d commit d26423e

File tree

5 files changed

+73
-19
lines changed

5 files changed

+73
-19
lines changed

src/api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export const apphostingGitHubAppInstallationURL = () =>
5252

5353
export const authOrigin = () =>
5454
utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com");
55+
export const authManagementOrigin = () =>
56+
utils.envOverride("FIREBASE_AUTH_MANAGEMENT_URL", "https://identitytoolkit.googleapis.com");
5557
export const consoleOrigin = () =>
5658
utils.envOverride("FIREBASE_CONSOLE_URL", "https://console.firebase.google.com");
5759
export const dynamicLinksOrigin = () =>

src/experiments.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export const ALL_EXPERIMENTS = experiments({
131131
},
132132
mcp: {
133133
shortDescription: "Adds experimental `firebase mcp` command for running a Firebase MCP server.",
134-
default: false,
134+
default: true,
135135
public: false,
136136
},
137137
});

src/mcp/index.ts

+39-17
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
ListToolsRequestSchema,
88
ListToolsResult,
99
} from "@modelcontextprotocol/sdk/types.js";
10-
import { mcpError } from "./util.js";
11-
import { ServerFeature } from "./types.js";
10+
import { checkFeatureActive, mcpError } from "./util.js";
11+
import { SERVER_FEATURES, ServerFeature } from "./types.js";
1212
import { availableTools } from "./tools/index.js";
1313
import { ServerTool } from "./tool.js";
1414
import { configstore } from "../configstore.js";
@@ -30,6 +30,7 @@ export class FirebaseMcpServer {
3030
projectRoot?: string;
3131
server: Server;
3232
activeFeatures?: ServerFeature[];
33+
detectedFeatures?: ServerFeature[];
3334
fixedRoot?: boolean;
3435

3536
constructor(options: { activeFeatures?: ServerFeature[]; projectRoot?: string }) {
@@ -44,30 +45,34 @@ export class FirebaseMcpServer {
4445
process.env.PROJECT_ROOT ??
4546
process.cwd();
4647
if (options.projectRoot) this.fixedRoot = true;
48+
this.detectActiveFeatures();
49+
}
50+
51+
async detectActiveFeatures(): Promise<ServerFeature[]> {
52+
if (this.detectedFeatures?.length) return this.detectedFeatures; // memoized
53+
const options = await this.resolveOptions();
54+
const projectId = await this.getProjectId();
55+
const detected = await Promise.all(
56+
SERVER_FEATURES.map(async (f) => {
57+
if (await checkFeatureActive(f, projectId, options)) return f;
58+
return null;
59+
}),
60+
);
61+
this.detectedFeatures = detected.filter((f) => !!f) as ServerFeature[];
62+
return this.detectedFeatures;
4763
}
4864

4965
get availableTools(): ServerTool[] {
50-
return availableTools(!!this.fixedRoot, this.activeFeatures);
66+
return availableTools(
67+
!!this.fixedRoot,
68+
this.activeFeatures?.length ? this.activeFeatures : this.detectedFeatures,
69+
);
5170
}
5271

5372
getTool(name: string): ServerTool | null {
5473
return this.availableTools.find((t) => t.mcp.name === name) || null;
5574
}
5675

57-
async mcpListTools(): Promise<ListToolsResult> {
58-
const hasActiveProject = !!(await this.getProjectId());
59-
await trackGA4("mcp_list_tools", {});
60-
return {
61-
tools: this.availableTools.map((t) => t.mcp),
62-
_meta: {
63-
projectRoot: this.projectRoot,
64-
projectDetected: hasActiveProject,
65-
authenticated: await this.getAuthenticated(),
66-
activeFeatures: this.activeFeatures,
67-
},
68-
};
69-
}
70-
7176
setProjectRoot(newRoot: string | null): void {
7277
if (newRoot === null) {
7378
configstore.delete(PROJECT_ROOT_KEY);
@@ -78,6 +83,7 @@ export class FirebaseMcpServer {
7883

7984
configstore.set(PROJECT_ROOT_KEY, newRoot);
8085
this.projectRoot = newRoot;
86+
this.detectedFeatures = undefined; // reset detected features
8187
void this.server.sendToolListChanged();
8288
}
8389

@@ -100,6 +106,22 @@ export class FirebaseMcpServer {
100106
}
101107
}
102108

109+
async mcpListTools(): Promise<ListToolsResult> {
110+
if (!this.activeFeatures) await this.detectActiveFeatures();
111+
const hasActiveProject = !!(await this.getProjectId());
112+
await trackGA4("mcp_list_tools", {});
113+
return {
114+
tools: this.availableTools.map((t) => t.mcp),
115+
_meta: {
116+
projectRoot: this.projectRoot,
117+
projectDetected: hasActiveProject,
118+
authenticated: await this.getAuthenticated(),
119+
activeFeatures: this.activeFeatures,
120+
detectedFeatures: this.detectedFeatures,
121+
},
122+
};
123+
}
124+
103125
async mcpCallTool(request: CallToolRequest): Promise<CallToolResult> {
104126
const toolName = request.params.name;
105127
const toolArgs = request.params.arguments;

src/mcp/tools/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function availableTools(fixedRoot: boolean, activeFeatures?: ServerFeatur
1515
// Present if the root is not fixed.
1616
toolDefs.push(...directoryTools);
1717
}
18-
if (!activeFeatures || !activeFeatures.length) {
18+
if (!activeFeatures?.length) {
1919
activeFeatures = Object.keys(tools) as ServerFeature[];
2020
}
2121
for (const key of activeFeatures) {

src/mcp/util.ts

+30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types";
22
import { execSync } from "child_process";
33
import { dump } from "js-yaml";
44
import { platform } from "os";
5+
import { ServerFeature } from "./types";
6+
import { authManagementOrigin, dataconnectOrigin, firestoreOrigin, storageOrigin } from "../api";
7+
import { check } from "../ensureApiEnabled";
58

69
export function toContent(data: any, options?: { format: "json" | "yaml" }): CallToolResult {
710
if (typeof data === "string") return { content: [{ type: "text", text: data }] };
@@ -53,3 +56,30 @@ export function commandExistsSync(command: string): boolean {
5356
return false;
5457
}
5558
}
59+
60+
const SERVER_FEATURE_APIS: Record<ServerFeature, string> = {
61+
firestore: firestoreOrigin(),
62+
storage: storageOrigin(),
63+
dataconnect: dataconnectOrigin(),
64+
auth: authManagementOrigin(),
65+
};
66+
/**
67+
* Detects whether an MCP feature is active in the current project root. Relies first on
68+
* `firebase.json` configuration, but falls back to API checks.
69+
*/
70+
export async function checkFeatureActive(
71+
feature: ServerFeature,
72+
projectId?: string,
73+
options?: any,
74+
): Promise<boolean> {
75+
// if the feature is configured in firebase.json, it's active
76+
if (feature in (options?.config?.data || {})) return true;
77+
// if the feature's api is active in the project, it's active
78+
try {
79+
if (projectId) return await check(projectId, SERVER_FEATURE_APIS[feature], "", true);
80+
} catch (e) {
81+
// if we don't have network or something, better to default to on
82+
return true;
83+
}
84+
return false;
85+
}

0 commit comments

Comments
 (0)