Skip to content

Commit 6156145

Browse files
authored
[MCP] Adds support for prompts. (#8965)
- Adds support for prompts in MCP server - Adds `deploy` prompt
1 parent a41c523 commit 6156145

File tree

10 files changed

+260
-6
lines changed

10 files changed

+260
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
- [Added] Support for creating Firestore Enterprise databases using `firestore:databases:create --edition enterprise`. (#8952)
66
- [Added] Support for Firestore Enterprise database index configurations. (#8939)
77
- [fixed] MCP: The `get_sdk_config` tool now properly returns decoded file content for Android and iOS.
8+
- [added] MCP: prompts are now supported with a `deploy` prompt as the first available.

src/mcp/index.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import {
88
SetLevelRequestSchema,
99
ListToolsRequestSchema,
1010
CallToolResult,
11+
ListPromptsRequestSchema,
12+
GetPromptRequestSchema,
13+
ListPromptsResult,
14+
GetPromptResult,
15+
GetPromptRequest,
1116
} from "@modelcontextprotocol/sdk/types.js";
1217
import { checkFeatureActive, mcpError } from "./util";
1318
import { ClientConfig, SERVER_FEATURES, ServerFeature } from "./types";
1419
import { availableTools } from "./tools/index";
1520
import { ServerTool, ServerToolContext } from "./tool";
21+
import { availablePrompts } from "./prompts/index";
22+
import { ServerPrompt, ServerPromptContext } from "./prompt";
1623
import { configstore } from "../configstore";
1724
import { Command } from "../command";
1825
import { requireAuth } from "../requireAuth";
@@ -31,7 +38,7 @@ import { LoggingStdioServerTransport } from "./logging-transport";
3138
import { isFirebaseStudio } from "../env";
3239
import { timeoutFallback } from "../timeout";
3340

34-
const SERVER_VERSION = "0.2.0";
41+
const SERVER_VERSION = "0.3.0";
3542

3643
const cmd = new Command("experimental:mcp");
3744

@@ -86,9 +93,17 @@ export class FirebaseMcpServer {
8693
this.activeFeatures = options.activeFeatures;
8794
this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT;
8895
this.server = new Server({ name: "firebase", version: SERVER_VERSION });
89-
this.server.registerCapabilities({ tools: { listChanged: true }, logging: {} });
96+
this.server.registerCapabilities({
97+
tools: { listChanged: true },
98+
logging: {},
99+
prompts: { listChanged: true },
100+
});
101+
90102
this.server.setRequestHandler(ListToolsRequestSchema, this.mcpListTools.bind(this));
91103
this.server.setRequestHandler(CallToolRequestSchema, this.mcpCallTool.bind(this));
104+
this.server.setRequestHandler(ListPromptsRequestSchema, this.mcpListPrompts.bind(this));
105+
this.server.setRequestHandler(GetPromptRequestSchema, this.mcpGetPrompt.bind(this));
106+
92107
this.server.oninitialized = async () => {
93108
const clientInfo = this.server.getClientVersion();
94109
this.clientInfo = clientInfo;
@@ -211,11 +226,22 @@ export class FirebaseMcpServer {
211226
return this.availableTools.find((t) => t.mcp.name === name) || null;
212227
}
213228

229+
get availablePrompts(): ServerPrompt[] {
230+
return availablePrompts(
231+
this.activeFeatures?.length ? this.activeFeatures : this.detectedFeatures,
232+
);
233+
}
234+
235+
getPrompt(name: string): ServerPrompt | null {
236+
return this.availablePrompts.find((p) => p.mcp.name === name) || null;
237+
}
238+
214239
setProjectRoot(newRoot: string | null): void {
215240
this.updateStoredClientConfig({ projectRoot: newRoot });
216241
this.cachedProjectRoot = newRoot || undefined;
217242
this.detectedFeatures = undefined; // reset detected features
218243
void this.server.sendToolListChanged();
244+
void this.server.sendPromptListChanged();
219245
}
220246

221247
async resolveOptions(): Promise<Partial<Options>> {
@@ -271,7 +297,9 @@ export class FirebaseMcpServer {
271297
(!this.cachedProjectRoot || !existsSync(this.cachedProjectRoot))
272298
) {
273299
return mcpError(
274-
`The current project directory '${this.cachedProjectRoot || "<NO PROJECT DIRECTORY FOUND>"}' does not exist. Please use the 'update_firebase_environment' tool to target a different project directory.`,
300+
`The current project directory '${
301+
this.cachedProjectRoot || "<NO PROJECT DIRECTORY FOUND>"
302+
}' does not exist. Please use the 'update_firebase_environment' tool to target a different project directory.`,
275303
);
276304
}
277305

@@ -324,6 +352,70 @@ export class FirebaseMcpServer {
324352
}
325353
}
326354

355+
async mcpListPrompts(): Promise<ListPromptsResult> {
356+
await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]);
357+
const hasActiveProject = !!(await this.getProjectId());
358+
await this.trackGA4("mcp_list_prompts");
359+
const skipAutoAuthForStudio = isFirebaseStudio();
360+
return {
361+
prompts: this.availablePrompts.map((p) => ({
362+
name: p.mcp.name,
363+
description: p.mcp.description,
364+
annotations: p.mcp.annotations,
365+
inputSchema: p.mcp.inputSchema,
366+
})),
367+
_meta: {
368+
projectRoot: this.cachedProjectRoot,
369+
projectDetected: hasActiveProject,
370+
authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio),
371+
activeFeatures: this.activeFeatures,
372+
detectedFeatures: this.detectedFeatures,
373+
},
374+
};
375+
}
376+
377+
async mcpGetPrompt(req: GetPromptRequest): Promise<GetPromptResult> {
378+
await this.detectProjectRoot();
379+
const promptName = req.params.name;
380+
const promptArgs = req.params.arguments;
381+
const prompt = this.getPrompt(promptName);
382+
if (!prompt) {
383+
throw new Error(`Prompt '${promptName}' could not be found.`);
384+
}
385+
386+
let projectId = await this.getProjectId();
387+
projectId = projectId || "";
388+
389+
const skipAutoAuthForStudio = isFirebaseStudio();
390+
const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
391+
392+
const options = { projectDir: this.cachedProjectRoot, cwd: this.cachedProjectRoot };
393+
const promptsCtx: ServerPromptContext = {
394+
projectId: projectId,
395+
host: this,
396+
config: Config.load(options, true) || new Config({}, options),
397+
rc: loadRC(options),
398+
accountEmail,
399+
};
400+
401+
try {
402+
const messages = await prompt.fn(promptArgs, promptsCtx);
403+
await this.trackGA4("mcp_get_prompt", {
404+
tool_name: promptName,
405+
});
406+
return {
407+
messages,
408+
};
409+
} catch (err: unknown) {
410+
await this.trackGA4("mcp_get_prompt", {
411+
tool_name: promptName,
412+
error: 1,
413+
});
414+
// TODO: should we return mcpError here?
415+
throw err;
416+
}
417+
}
418+
327419
async start(): Promise<void> {
328420
const transport = process.env.FIREBASE_MCP_DEBUG_LOG
329421
? new LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG)

src/mcp/prompt.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { PromptMessage } from "@modelcontextprotocol/sdk/types.js";
2+
import { z, ZodTypeAny } from "zod";
3+
import { zodToJsonSchema } from "zod-to-json-schema";
4+
import type { FirebaseMcpServer } from "./index";
5+
import type { Config } from "../config";
6+
import { RC } from "../rc";
7+
import { cleanSchema } from "./util";
8+
9+
export interface ServerPromptContext {
10+
projectId: string;
11+
accountEmail: string | null;
12+
config: Config;
13+
host: FirebaseMcpServer;
14+
rc: RC;
15+
}
16+
17+
export interface ServerPrompt<InputSchema extends ZodTypeAny = ZodTypeAny> {
18+
mcp: {
19+
name: string;
20+
description?: string;
21+
inputSchema: any;
22+
omitPrefix?: boolean;
23+
annotations?: {
24+
title?: string;
25+
};
26+
_meta?: {
27+
/** Prompts are grouped by feature. --only can configure what prompts is available. */
28+
feature?: string;
29+
};
30+
};
31+
fn: (input: z.infer<InputSchema>, ctx: ServerPromptContext) => Promise<PromptMessage[]>;
32+
}
33+
34+
export function prompt<InputSchema extends ZodTypeAny>(
35+
options: Omit<ServerPrompt<InputSchema>["mcp"], "inputSchema" | "name"> & {
36+
name: string;
37+
inputSchema: InputSchema;
38+
omitPrefix?: boolean;
39+
},
40+
fn: ServerPrompt<InputSchema>["fn"],
41+
): ServerPrompt {
42+
return {
43+
mcp: { ...options, inputSchema: cleanSchema(zodToJsonSchema(options.inputSchema)) },
44+
fn,
45+
};
46+
}

src/mcp/prompts/core/deploy.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { z } from "zod";
2+
import { prompt } from "../../prompt";
3+
4+
export const deploy = prompt(
5+
{
6+
name: "deploy",
7+
omitPrefix: true,
8+
description: "Use this command to deploy resources to Firebase.",
9+
inputSchema: z.object({
10+
prompt: z
11+
.string()
12+
.describe("any specific instructions you wish to provide about deploying")
13+
.optional(),
14+
}),
15+
annotations: {
16+
title: "Deploy to Firebase",
17+
},
18+
},
19+
async ({ prompt }, { config, projectId, accountEmail }) => {
20+
return [
21+
{
22+
role: "user" as const,
23+
content: {
24+
type: "text",
25+
text: `
26+
Your goal is to deploy resources from the current project to Firebase.
27+
28+
Active user: ${accountEmail || "<NONE>"}
29+
Active project: ${projectId || "<NONE>"}
30+
31+
Contents of \`firebase.json\` config file:
32+
33+
\`\`\`json
34+
${config.readProjectFile("firebase.json", { fallback: "<FILE DOES NOT EXIST>" })}
35+
\`\`\`
36+
37+
## User Instructions
38+
39+
${prompt || "<the user didn't supply specific instructions>"}
40+
41+
## Steps
42+
43+
Follow the steps below taking note of any user instructions provided above.
44+
45+
1. If there is no active user, prompt the user to run \`firebase login\` in an interactive terminal before continuing.
46+
2. If there is no \`firebase.json\` file and the current workspace is a static web application, manually create a \`firebase.json\` with \`"hosting"\` configuration based on the current directory's web app configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
47+
3. If there is no active project, ask the user if they want to use an existing project or create a new one.
48+
3a. If create a new one, use the \`firebase_create_project\` tool.
49+
3b. If they want to use an existing one, ask them for a project id (the \`firebase_list_projects\` tool may be helpful).
50+
4. Only after making sure Firebase has been initialized, run the \`firebase deploy\` shell command to perform the deploy. This may take a few minutes.
51+
5. If the deploy has errors, attempt to fix them and ask the user clarifying questions as needed.
52+
6. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`firebase deploy --force\`.
53+
7. If only one specific feature is failing, use command \`firebase deploy --only <feature>\` as you debug.
54+
`.trim(),
55+
},
56+
},
57+
];
58+
},
59+
);

src/mcp/prompts/core/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { deploy } from "./deploy";
2+
3+
export const corePrompts = [deploy];

src/mcp/prompts/index.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ServerFeature } from "../types";
2+
import { ServerPrompt } from "../prompt";
3+
import { corePrompts } from "./core";
4+
5+
const prompts: Record<ServerFeature, ServerPrompt[]> = {
6+
core: corePrompts,
7+
firestore: [],
8+
storage: [],
9+
dataconnect: [],
10+
auth: [],
11+
messaging: [],
12+
remoteconfig: [],
13+
crashlytics: [],
14+
apphosting: [],
15+
database: [],
16+
};
17+
18+
function namespacePrompts(
19+
promptsToNamespace: ServerPrompt[],
20+
feature: ServerFeature,
21+
): ServerPrompt[] {
22+
return promptsToNamespace.map((p) => {
23+
const newPrompt = { ...p };
24+
newPrompt.mcp = { ...p.mcp };
25+
if (newPrompt.mcp.omitPrefix) {
26+
// name is as-is
27+
} else if (feature === "core") {
28+
newPrompt.mcp.name = `firebase:${p.mcp.name}`;
29+
} else {
30+
newPrompt.mcp.name = `firebase:${feature}:${p.mcp.name}`;
31+
}
32+
newPrompt.mcp._meta = { ...p.mcp._meta, feature };
33+
return newPrompt;
34+
});
35+
}
36+
37+
export function availablePrompts(features?: ServerFeature[]): ServerPrompt[] {
38+
const allPrompts: ServerPrompt[] = namespacePrompts(prompts["core"], "core");
39+
if (!features) {
40+
features = Object.keys(prompts).filter((f) => f !== "core") as ServerFeature[];
41+
}
42+
43+
for (const feature of features) {
44+
if (prompts[feature] && feature !== "core") {
45+
allPrompts.push(...namespacePrompts(prompts[feature], feature));
46+
}
47+
}
48+
return allPrompts;
49+
}

src/mcp/tools/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ import { realtimeDatabaseTools } from "./database/index";
1313

1414
/** availableTools returns the list of MCP tools available given the server flags */
1515
export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] {
16-
// Core tools are always present.
17-
const toolDefs: ServerTool[] = addFeaturePrefix("firebase", coreTools);
16+
const toolDefs: ServerTool[] = [];
1817
if (!activeFeatures?.length) {
1918
activeFeatures = Object.keys(tools) as ServerFeature[];
2019
}
@@ -25,6 +24,7 @@ export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] {
2524
}
2625

2726
const tools: Record<ServerFeature, ServerTool[]> = {
27+
core: addFeaturePrefix("firebase", coreTools),
2828
firestore: addFeaturePrefix("firestore", firestoreTools),
2929
auth: addFeaturePrefix("auth", authTools),
3030
dataconnect: addFeaturePrefix("dataconnect", dataconnectTools),

src/mcp/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const SERVER_FEATURES = [
2+
"core",
23
"firestore",
34
"storage",
45
"dataconnect",

src/mcp/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function commandExistsSync(command: string): boolean {
8787
}
8888

8989
const SERVER_FEATURE_APIS: Record<ServerFeature, string> = {
90+
core: "",
9091
firestore: firestoreOrigin(),
9192
storage: storageOrigin(),
9293
dataconnect: dataconnectOrigin(),

src/track.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ type cliEventNames =
2424
| "function_deploy_group"
2525
| "mcp_tool_call"
2626
| "mcp_list_tools"
27-
| "mcp_client_connected";
27+
| "mcp_client_connected"
28+
| "mcp_list_prompts"
29+
| "mcp_get_prompt";
2830
type GA4Property = "cli" | "emulator" | "vscode";
2931
interface GA4Info {
3032
measurementId: string;

0 commit comments

Comments
 (0)