Skip to content

Commit 3ea1168

Browse files
MichaelDoylejoehan
andauthored
feat(mcp): fine grained control over tool loading (#9601)
--tools will override discovery and provide only the listed tools Co-authored-by: Joe Hanley <joehanley@google.com>
1 parent 422ff9c commit 3ea1168

File tree

4 files changed

+205
-86
lines changed

4 files changed

+205
-86
lines changed

src/bin/mcp.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env node
22

3+
import { resolve } from "path";
4+
import { parseArgs } from "util";
35
import { useFileLogger } from "../logger";
46
import { FirebaseMcpServer } from "../mcp/index";
5-
import { parseArgs } from "util";
6-
import { SERVER_FEATURES, ServerFeature } from "../mcp/types";
7-
import { markdownDocsOfTools } from "../mcp/tools/index.js";
87
import { markdownDocsOfPrompts } from "../mcp/prompts/index.js";
98
import { markdownDocsOfResources } from "../mcp/resources/index.js";
10-
import { resolve } from "path";
9+
import { markdownDocsOfTools } from "../mcp/tools/index.js";
10+
import { SERVER_FEATURES, ServerFeature } from "../mcp/types";
1111

1212
const STARTUP_MESSAGE = `
1313
This is a running process of the Firebase MCP server. This command should only be executed by an MCP client. An example MCP client configuration might be:
@@ -22,19 +22,52 @@ This is a running process of the Firebase MCP server. This command should only b
2222
}
2323
`;
2424

25+
const HELP_TEXT = `Usage: firebase mcp [options]
26+
27+
Description:
28+
Starts the Model Context Protocol (MCP) server for the Firebase CLI. This server provides a
29+
standardized way for AI agents and IDEs to interact with your Firebase project.
30+
31+
Tool Discovery & Loading:
32+
The server automatically determines which tools to expose based on your project context.
33+
34+
1. Auto-Detection (Default):
35+
- Scans 'firebase.json' for configured services (e.g., Hosting, Firestore).
36+
- Checks enabled Google Cloud APIs for the active project.
37+
- Inspects project files for specific SDKs (e.g., Crashlytics in Android/iOS).
38+
39+
2. Manual Overrides:
40+
- Use '--only' to restrict tool discovery to specific feature sets (e.g., core, firestore).
41+
- Use '--tools' to disable auto-detection entirely and load specific tools by name.
42+
43+
Options:
44+
--dir <path> Project root directory (defaults to current working directory).
45+
--only <features> Comma-separated list of features to enable (e.g. core, firestore).
46+
If specified, auto-detection is disabled for other features.
47+
--tools <tools> Comma-separated list of specific tools to enable. Disables
48+
auto-detection entirely.
49+
-h, --help Show this help message.
50+
`;
51+
2552
export async function mcp(): Promise<void> {
2653
const { values } = parseArgs({
2754
options: {
2855
only: { type: "string", default: "" },
56+
tools: { type: "string", default: "" },
2957
dir: { type: "string" },
3058
"generate-tool-list": { type: "boolean", default: false },
3159
"generate-prompt-list": { type: "boolean", default: false },
3260
"generate-resource-list": { type: "boolean", default: false },
61+
help: { type: "boolean", default: false, short: "h" },
3362
},
3463
allowPositionals: true,
3564
});
3665

3766
let earlyExit = false;
67+
if (values.help) {
68+
console.log(HELP_TEXT);
69+
earlyExit = true;
70+
}
3871
if (values["generate-tool-list"]) {
3972
console.log(markdownDocsOfTools());
4073
earlyExit = true;
@@ -53,9 +86,15 @@ export async function mcp(): Promise<void> {
5386
useFileLogger();
5487
const activeFeatures = (values.only || "")
5588
.split(",")
89+
.map((f) => f.trim())
5690
.filter((f) => SERVER_FEATURES.includes(f as ServerFeature)) as ServerFeature[];
91+
const enabledTools = (values.tools || "")
92+
.split(",")
93+
.map((t) => t.trim())
94+
.filter((t) => t.length > 0);
5795
const server = new FirebaseMcpServer({
5896
activeFeatures,
97+
enabledTools,
5998
projectRoot: values.dir ? resolve(values.dir) : undefined,
6099
});
61100
await server.start();

src/mcp/index.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,49 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
33
import {
44
CallToolRequest,
55
CallToolRequestSchema,
6-
ListToolsResult,
7-
LoggingLevel,
8-
SetLevelRequestSchema,
9-
ListToolsRequestSchema,
106
CallToolResult,
11-
ListPromptsRequestSchema,
7+
ErrorCode,
8+
GetPromptRequest,
129
GetPromptRequestSchema,
13-
ListPromptsResult,
1410
GetPromptResult,
15-
GetPromptRequest,
11+
ListPromptsRequestSchema,
12+
ListPromptsResult,
1613
ListResourcesRequestSchema,
1714
ListResourcesResult,
18-
ReadResourceRequest,
19-
ReadResourceResult,
20-
ReadResourceRequestSchema,
2115
ListResourceTemplatesRequestSchema,
2216
ListResourceTemplatesResult,
17+
ListToolsRequestSchema,
18+
ListToolsResult,
19+
LoggingLevel,
2320
McpError,
24-
ErrorCode,
21+
ReadResourceRequest,
22+
ReadResourceRequestSchema,
23+
ReadResourceResult,
24+
SetLevelRequestSchema,
2525
} from "@modelcontextprotocol/sdk/types.js";
26-
import { mcpError } from "./util";
27-
import { ClientConfig, McpContext, SERVER_FEATURES, ServerFeature } from "./types";
28-
import { availableTools } from "./tools/index";
29-
import { ServerTool } from "./tool";
30-
import { availablePrompts } from "./prompts/index";
31-
import { ServerPrompt } from "./prompt";
32-
import { configstore } from "../configstore";
26+
import * as crossSpawn from "cross-spawn";
27+
import { existsSync } from "node:fs";
3328
import { Command } from "../command";
34-
import { requireAuth } from "../requireAuth";
35-
import { Options } from "../options";
36-
import { getProjectId } from "../projectUtils";
37-
import { mcpAuthError, noProjectDirectory, NO_PROJECT_ERROR, requireGeminiToS } from "./errors";
38-
import { trackGA4 } from "../track";
3929
import { Config } from "../config";
40-
import { loadRC } from "../rc";
30+
import { configstore } from "../configstore";
4131
import { EmulatorHubClient } from "../emulator/hubClient";
4232
import { Emulators } from "../emulator/types";
43-
import { existsSync } from "node:fs";
44-
import { LoggingStdioServerTransport } from "./logging-transport";
4533
import { isFirebaseStudio } from "../env";
34+
import { Options } from "../options";
35+
import { getProjectId } from "../projectUtils";
36+
import { loadRC } from "../rc";
37+
import { requireAuth } from "../requireAuth";
4638
import { timeoutFallback } from "../timeout";
39+
import { trackGA4 } from "../track";
40+
import { mcpAuthError, NO_PROJECT_ERROR, noProjectDirectory, requireGeminiToS } from "./errors";
41+
import { LoggingStdioServerTransport } from "./logging-transport";
42+
import { ServerPrompt } from "./prompt";
43+
import { availablePrompts } from "./prompts/index";
4744
import { resolveResource, resources, resourceTemplates } from "./resources";
48-
import * as crossSpawn from "cross-spawn";
45+
import { ServerTool } from "./tool";
46+
import { availableTools } from "./tools/index";
47+
import { ClientConfig, McpContext, SERVER_FEATURES, ServerFeature } from "./types";
48+
import { mcpError } from "./util";
4949
import { getDefaultFeatureAvailabilityCheck } from "./util/availability";
5050

5151
const SERVER_VERSION = "0.3.0";
@@ -72,6 +72,7 @@ export class FirebaseMcpServer {
7272
server: Server;
7373
activeFeatures?: ServerFeature[];
7474
detectedFeatures?: ServerFeature[];
75+
enabledTools?: string[];
7576
clientInfo?: { name?: string; version?: string };
7677
emulatorHubClient?: EmulatorHubClient;
7778
private cliCommand?: string;
@@ -99,9 +100,14 @@ export class FirebaseMcpServer {
99100
return trackGA4(event, { ...params, ...clientInfoParams });
100101
}
101102

102-
constructor(options: { activeFeatures?: ServerFeature[]; projectRoot?: string }) {
103+
constructor(options: {
104+
activeFeatures?: ServerFeature[];
105+
projectRoot?: string;
106+
enabledTools?: string[];
107+
}) {
103108
this.activeFeatures = options.activeFeatures;
104109
this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT;
110+
this.enabledTools = options.enabledTools;
105111
this.server = new Server({ name: "firebase", version: SERVER_VERSION });
106112
this.server.registerCapabilities({
107113
tools: { listChanged: true },
@@ -245,7 +251,7 @@ export class FirebaseMcpServer {
245251
const projectId = (await this.getProjectId()) || "";
246252
const accountEmail = await this.getAuthenticatedUser();
247253
const ctx = this._createMcpContext(projectId, accountEmail);
248-
return availableTools(ctx, features);
254+
return availableTools(ctx, features, this.enabledTools);
249255
}
250256

251257
async getTool(name: string): Promise<ServerTool | null> {

src/mcp/tools/index.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { expect } from "chai";
2+
import { McpContext } from "../types";
3+
import { availableTools } from "./index";
4+
5+
describe("availableTools", () => {
6+
const mockContext: McpContext = {
7+
projectId: "test-project",
8+
accountEmail: "test@example.com",
9+
config: {} as any,
10+
host: {
11+
logger: {
12+
debug: () => void 0,
13+
info: () => void 0,
14+
warn: () => void 0,
15+
error: () => void 0,
16+
},
17+
} as any,
18+
rc: {} as any,
19+
firebaseCliCommand: "firebase",
20+
};
21+
22+
it("should return specific tools when enabledTools is provided", async () => {
23+
const tools = await availableTools(mockContext, [], ["firebase_login"]);
24+
25+
expect(tools).to.have.length(1);
26+
expect(tools[0].mcp.name).to.equal("firebase_login");
27+
});
28+
29+
it("should return core tools by default", async () => {
30+
const tools = await availableTools(mockContext, []);
31+
// an example of a core tool
32+
const loginTool = tools.find((t) => t.mcp.name === "firebase_login");
33+
34+
expect(loginTool).to.exist;
35+
});
36+
37+
it("should include feature-specific tools when activeFeatures is provided", async () => {
38+
const tools = await availableTools(mockContext, ["firestore"]);
39+
const firestoreTool = tools.find((t) => t.mcp.name.startsWith("firestore_"));
40+
41+
expect(firestoreTool).to.exist;
42+
});
43+
44+
it("should not include feature tools if no active features", async () => {
45+
const tools = await availableTools(mockContext, ["core"]);
46+
const firestoreTool = tools.find((t) => t.mcp.name.startsWith("firestore_"));
47+
48+
expect(firestoreTool).to.not.exist;
49+
});
50+
});

0 commit comments

Comments
 (0)