Skip to content

Commit d90a73b

Browse files
committed
feat(mcp): fine grained control over tool loading
--tools will override discovery and provide only the listed tools
1 parent 7c3170c commit d90a73b

File tree

3 files changed

+147
-82
lines changed

3 files changed

+147
-82
lines changed

src/bin/mcp.ts

Lines changed: 42 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,55 @@ 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+
--generate-tool-list Generate markdown documentation for all available tools.
50+
--generate-prompt-list Generate markdown documentation for all available prompts.
51+
--generate-resource-list Generate markdown documentation for all available resources.
52+
-h, --help Show this help message.
53+
`;
54+
2555
export async function mcp(): Promise<void> {
2656
const { values } = parseArgs({
2757
options: {
2858
only: { type: "string", default: "" },
59+
tools: { type: "string", default: "" },
2960
dir: { type: "string" },
3061
"generate-tool-list": { type: "boolean", default: false },
3162
"generate-prompt-list": { type: "boolean", default: false },
3263
"generate-resource-list": { type: "boolean", default: false },
64+
help: { type: "boolean", default: false, short: "h" },
3365
},
3466
allowPositionals: true,
3567
});
3668

3769
let earlyExit = false;
70+
if (values.help) {
71+
console.log(HELP_TEXT);
72+
earlyExit = true;
73+
}
3874
if (values["generate-tool-list"]) {
3975
console.log(markdownDocsOfTools());
4076
earlyExit = true;
@@ -54,8 +90,10 @@ export async function mcp(): Promise<void> {
5490
const activeFeatures = (values.only || "")
5591
.split(",")
5692
.filter((f) => SERVER_FEATURES.includes(f as ServerFeature)) as ServerFeature[];
93+
const enabledTools = (values.tools || "").split(",").filter((t) => t.length > 0);
5794
const server = new FirebaseMcpServer({
5895
activeFeatures,
96+
enabledTools,
5997
projectRoot: values.dir ? resolve(values.dir) : undefined,
6098
});
6199
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.ts

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,30 @@
11
import { ServerTool } from "../tool";
22
import { McpContext, ServerFeature } from "../types";
3+
import { appHostingTools } from "./apphosting/index";
4+
import { apptestingTools } from "./apptesting/index";
35
import { authTools } from "./auth/index";
6+
import { coreTools } from "./core/index";
7+
import { crashlyticsTools } from "./crashlytics/index";
48
import { dataconnectTools } from "./dataconnect/index";
59
import { firestoreTools } from "./firestore/index";
6-
import { coreTools } from "./core/index";
7-
import { storageTools } from "./storage/index";
10+
import { functionsTools } from "./functions/index";
811
import { messagingTools } from "./messaging/index";
9-
import { remoteConfigTools } from "./remoteconfig/index";
10-
import { crashlyticsTools } from "./crashlytics/index";
11-
import { appHostingTools } from "./apphosting/index";
12-
import { apptestingTools } from "./apptesting/index";
1312
import { realtimeDatabaseTools } from "./realtime_database/index";
14-
import { functionsTools } from "./functions/index";
15-
16-
/** availableTools returns the list of MCP tools available given the server flags */
17-
export async function availableTools(
18-
ctx: McpContext,
19-
activeFeatures?: ServerFeature[],
20-
): Promise<ServerTool[]> {
21-
const allTools = getAllTools(activeFeatures);
22-
const availabilities = await Promise.all(
23-
allTools.map((t) => {
24-
if (t.isAvailable) {
25-
return t.isAvailable(ctx);
26-
}
27-
return true;
28-
}),
29-
);
30-
return allTools.filter((_, i) => availabilities[i]);
31-
}
13+
import { remoteConfigTools } from "./remoteconfig/index";
14+
import { storageTools } from "./storage/index";
3215

33-
function getAllTools(activeFeatures?: ServerFeature[]): ServerTool[] {
34-
const toolDefs: ServerTool[] = [];
35-
if (!activeFeatures?.length) {
36-
activeFeatures = Object.keys(tools) as ServerFeature[];
37-
}
38-
if (!activeFeatures.includes("core")) {
39-
activeFeatures.unshift("core");
40-
}
41-
for (const key of activeFeatures) {
42-
toolDefs.push(...tools[key]);
43-
}
44-
return toolDefs;
16+
function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] {
17+
return tools.map((tool) => ({
18+
...tool,
19+
mcp: {
20+
...tool.mcp,
21+
name: `${feature}_${tool.mcp.name}`,
22+
_meta: {
23+
...tool.mcp._meta,
24+
feature,
25+
},
26+
},
27+
}));
4528
}
4629

4730
const tools: Record<ServerFeature, ServerTool[]> = {
@@ -59,26 +42,64 @@ const tools: Record<ServerFeature, ServerTool[]> = {
5942
database: addFeaturePrefix("realtimedatabase", realtimeDatabaseTools),
6043
};
6144

62-
function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] {
63-
return tools.map((tool) => ({
64-
...tool,
65-
mcp: {
66-
...tool.mcp,
67-
name: `${feature}_${tool.mcp.name}`,
68-
_meta: {
69-
...tool.mcp._meta,
70-
feature,
71-
},
72-
},
73-
}));
45+
function getToolsByName(names: string[]): ServerTool[] {
46+
const allToolsMap = new Map(
47+
Object.values(tools)
48+
.flat()
49+
.map((t) => [t.mcp.name, t]),
50+
);
51+
const selectedTools = new Set<ServerTool>();
52+
53+
for (const toolName of names) {
54+
const tool = allToolsMap.get(toolName);
55+
if (tool) {
56+
selectedTools.add(tool);
57+
}
58+
}
59+
return Array.from(selectedTools);
60+
}
61+
62+
function getToolsByFeature(serverFeatures?: ServerFeature[]): ServerTool[] {
63+
const features = new Set(
64+
serverFeatures?.length ? serverFeatures : (Object.keys(tools) as ServerFeature[]),
65+
);
66+
features.add("core");
67+
68+
return Array.from(features).flatMap((feature) => tools[feature] || []);
69+
}
70+
71+
/**
72+
* Discover all all available tools. When `activeFeatures` is provided, tool discovery will only
73+
* consider those features. When `enabledTools` is provided, discovery is skipped entirely, and
74+
* only tools with exactly those names are returned.
75+
*/
76+
export async function availableTools(
77+
ctx: McpContext,
78+
activeFeatures?: ServerFeature[],
79+
enabledTools?: string[],
80+
): Promise<ServerTool[]> {
81+
if (enabledTools?.length) {
82+
return getToolsByName(enabledTools);
83+
}
84+
85+
const allTools = getToolsByFeature(activeFeatures);
86+
const availabilities = await Promise.all(
87+
allTools.map((t) => {
88+
if (t.isAvailable) {
89+
return t.isAvailable(ctx);
90+
}
91+
return true;
92+
}),
93+
);
94+
return allTools.filter((_, i) => availabilities[i]);
7495
}
7596

7697
/**
7798
* Generates a markdown table of all available tools and their descriptions.
7899
* This is used for generating documentation.
79100
*/
80101
export function markdownDocsOfTools(): string {
81-
const allTools = getAllTools([]);
102+
const allTools = getToolsByFeature([]);
82103
let doc = `
83104
| Tool Name | Feature Group | Description |
84105
| --------- | ------------- | ----------- |`;

0 commit comments

Comments
 (0)