Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
8 changes: 8 additions & 0 deletions src/dataconnect/loadAll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { load } from "./load";
import type { Config } from "../config";
import { readFirebaseJson } from "./fileUtils";

export function loadAll(projectId: string, config: Config) {

Check warning on line 5 in src/dataconnect/loadAll.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment

Check warning on line 5 in src/dataconnect/loadAll.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const configs = readFirebaseJson(config);
return Promise.all(configs.map((c) => load(projectId, config, c.source)));
}
5 changes: 5 additions & 0 deletions src/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export const ALL_EXPERIMENTS = experiments({
default: true,
public: false,
},
mcpalpha: {
shortDescription: "Opt-in to early MCP features before they're widely released.",
default: false,
public: false,
},
apptesting: {
shortDescription: "Adds experimental App Testing feature",
public: true,
Expand Down
4 changes: 4 additions & 0 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
] as const;

export class FirebaseMcpServer {
private _ready: boolean = false;

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

View workflow job for this annotation

GitHub Actions / lint (20)

Type boolean trivially inferred from a boolean literal, remove type annotation
private _readyPromises: { resolve: () => void; reject: (err: unknown) => void }[] = [];
startupRoot?: string;
cachedProjectRoot?: string;
Expand Down Expand Up @@ -86,7 +86,7 @@
mcp_client_name: this.clientInfo?.name || "<unknown-client>",
mcp_client_version: this.clientInfo?.version || "<unknown-version>",
};
trackGA4(event, { ...params, ...clientInfoParams });

Check warning on line 89 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
}

constructor(options: { activeFeatures?: ServerFeature[]; projectRoot?: string }) {
Expand All @@ -104,11 +104,11 @@
this.server.setRequestHandler(ListPromptsRequestSchema, this.mcpListPrompts.bind(this));
this.server.setRequestHandler(GetPromptRequestSchema, this.mcpGetPrompt.bind(this));

this.server.oninitialized = async () => {

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

View workflow job for this annotation

GitHub Actions / lint (20)

Promise-returning function provided to variable where a void return was expected
const clientInfo = this.server.getClientVersion();
this.clientInfo = clientInfo;
if (clientInfo?.name) {
this.trackGA4("mcp_client_connected");

Check warning on line 111 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
}
if (!this.clientInfo?.name) this.clientInfo = { name: "<unknown-client>" };

Expand All @@ -123,12 +123,12 @@
return {};
});

this.detectProjectRoot();

Check warning on line 126 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
this.detectActiveFeatures();

Check warning on line 127 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
}

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

Check warning on line 131 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 @@ -139,7 +139,7 @@
return this.clientInfo?.name ?? (isFirebaseStudio() ? "Firebase Studio" : "<unknown-client>");
}

private get clientConfigKey() {

Check warning on line 142 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()}`;
}

Expand Down Expand Up @@ -227,6 +227,10 @@
}

get availablePrompts(): ServerPrompt[] {
this.log(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

"debug",
`availablePrompts: ${JSON.stringify(this.activeFeatures)} // ${JSON.stringify(this.detectedFeatures)}`,
);
return availablePrompts(
this.activeFeatures?.length ? this.activeFeatures : this.detectedFeatures,
);
Expand Down
9 changes: 9 additions & 0 deletions src/mcp/prompts/dataconnect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { isEnabled } from "../../../experiments";
import { schema } from "./schema";
import type { ServerPrompt } from "../../prompt";

export const dataconnectPrompts: ServerPrompt[] = [];

if (isEnabled("mcpalpha")) {
dataconnectPrompts.push(schema);
}
74 changes: 74 additions & 0 deletions src/mcp/prompts/dataconnect/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { z } from "zod";

Check failure on line 1 in src/mcp/prompts/dataconnect/schema.ts

View workflow job for this annotation

GitHub Actions / unit (20)

'z' is defined but never used

Check failure on line 1 in src/mcp/prompts/dataconnect/schema.ts

View workflow job for this annotation

GitHub Actions / unit (22)

'z' is defined but never used

Check failure on line 1 in src/mcp/prompts/dataconnect/schema.ts

View workflow job for this annotation

GitHub Actions / unit (20)

'z' is defined but never used

Check failure on line 1 in src/mcp/prompts/dataconnect/schema.ts

View workflow job for this annotation

GitHub Actions / lint (20)

'z' is defined but never used

Check failure on line 1 in src/mcp/prompts/dataconnect/schema.ts

View workflow job for this annotation

GitHub Actions / unit (22)

'z' is defined but never used
import { prompt } from "../../prompt";
import { loadAll } from "../../../dataconnect/loadAll";
import type { ServiceInfo } from "../../../dataconnect/types";
import { BUILTIN_SDL, MAIN_INSTRUCTIONS } from "../../util/dataconnect/content";
import { compileErrors } from "../../util/dataconnect/compile";

function renderServices(fdcServices: ServiceInfo[]) {
if (!fdcServices.length) return "Data Connect Status: <UNCONFIGURED>";

return `\n\n## Data Connect Schema

The following is the up-to-date content of existing schema files (their paths are relative to the Data Connect source directory).

${fdcServices[0].schema.source.files?.map((f) => `\`\`\`graphql ${f.path}\n${f.content}\n\`\`\``).join("\n\n")}`;
}

function renderErrors(errors?: string) {
return `\n\n## Current Schema Build Errors\n\n${errors || "<NO ERRORS>"}`;
}

export const schema = prompt(
{
name: "schema",
description: "Generate or update your Firebase Data Connect schema.",
arguments: [
{
name: "prompt",
description:
"describe the schema you want generated or the edits you want to make to your existing schema",
required: true,
},
],
annotations: {
title: "Deploy to Firebase",
Copy link
Contributor

@fredzqm fredzqm Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"title" looks off.

I am wondering if it makes more sense to rely on the GiF backend more, which is known to have good accuracy.

I have concerns over lack of eval & performance visibility in local MCP prompts. It gets worse if this tool shadows the dataconnect_generate_schema tool.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[not blocking] Feel free to check this in.

I am trying to figure out where that line should be. Maybe it's edit vs create.

Use GiF API to generate the initial schema and new operations.

Use the local prompts to iterate and edit them. Cursor probably has more local context.

Without evaluation, it's really a pain to figure how the experience is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think our visibility into GiF is all that much better, though I'd like us to come up with an informal eval system utilizing Gemini CLI for the MCP server generally. This is currently flagged off in the "mcpalpha" experiment so I'm not too worried about it one way or the other for now.

},
},
async ({ prompt }, { config, projectId, accountEmail }) => {
const fdcServices = await loadAll(projectId, config);
const buildErrors = fdcServices.length
? await compileErrors(fdcServices[0].sourceDirectory)
: "";

return [
{
role: "user" as const,
content: {
type: "text",
text: `
${MAIN_INSTRUCTIONS}\n\n${BUILTIN_SDL}

==== CURRENT ENVIRONMENT INFO ====

User Email: ${accountEmail || "<NONE>"}
Project ID: ${projectId || "<NONE>"}
${renderServices(fdcServices)}${renderErrors(buildErrors)}

==== USER PROMPT ====

${prompt}

==== TASK INSTRUCTIONS ====

1. If Data Connect is marked as \`<UNCONFIGURED>\`, first run the \`firebase_init\` tool with \`{dataconnect: {}}\` arguments to initialize it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: for myself. We can pass in appIdea here to let GiF generates the schema and connectors for us

2. If there is not an existing schema to work with (or the existing schema is the commented-out default schema about a movie app), follow the user's prompt to generate a robust schema meeting the specified requirements.
3. If there is already a schema, perform edits to the existing schema file(s) based on the user's instructions. If schema build errors are present and seem relevant to your changes, attempt to fix them.
4. After you have performed edits on the schema, run the \`dataconnect_compile\` tool to build the schema and see if there are any errors. Fix errors that are related to the user's prompt or your changes.
5. If there are errors, attempt to fix them. If you have attempted to fix them 3 times without success, ask the user for help.
6. If there are no errors, write a brief paragraph summarizing your changes.`,
},
},
];
},
);
5 changes: 3 additions & 2 deletions src/mcp/prompts/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ServerFeature } from "../types";
import { ServerPrompt } from "../prompt";
import { corePrompts } from "./core";
import { dataconnectPrompts } from "./dataconnect";

const prompts: Record<ServerFeature, ServerPrompt[]> = {
core: corePrompts,
firestore: [],
storage: [],
dataconnect: [],
dataconnect: dataconnectPrompts,
auth: [],
messaging: [],
remoteconfig: [],
Expand Down Expand Up @@ -36,7 +37,7 @@ function namespacePrompts(

export function availablePrompts(features?: ServerFeature[]): ServerPrompt[] {
const allPrompts: ServerPrompt[] = namespacePrompts(prompts["core"], "core");
if (!features) {
if (!features?.length) {
features = Object.keys(prompts).filter((f) => f !== "core") as ServerFeature[];
}

Expand Down
47 changes: 47 additions & 0 deletions src/mcp/tools/dataconnect/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { z } from "zod";
import { tool } from "../../tool";
import { pickService } from "../../../dataconnect/fileUtils";
import { compileErrors } from "../../util/dataconnect/compile";

export const compile = tool(
{
name: "compile",
description:
"Use this to compile Firebase Data Connect schema and/or operations and check for build errors.",
inputSchema: z.object({
error_filter: z
.enum(["all", "schema", "operations"])
.describe("filter errors to a specific type only. defaults to `all` if omitted.")
.optional(),
service_id: z
.string()
.optional()
.describe(
"The Firebase Data Connect service ID to look for. If omitted, the first service defined in `firebase.json` is used.",
),
}),
annotations: {
title: "Compile Data Connect",
readOnlyHint: true,
},
_meta: {
requiresProject: false,
requiresAuth: false,
},
},
async ({ service_id, error_filter }, { projectId, config }) => {
const serviceInfo = await pickService(projectId, config, service_id || undefined);
const errors = await compileErrors(serviceInfo.sourceDirectory, error_filter);
if (errors)
return {
content: [
{
type: "text",
text: `The following errors were encountered while compiling Data Connect from directory \`${serviceInfo.sourceDirectory}\`:\n\n${errors}`,
},
],
isError: true,
};
return { content: [{ type: "text", text: "Compiled successfully." }] };
},
);
4 changes: 2 additions & 2 deletions src/mcp/tools/dataconnect/execute_graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { z } from "zod";
import { tool } from "../../tool";
import * as dataplane from "../../../dataconnect/dataplaneClient";
import { pickService } from "../../../dataconnect/fileUtils";
import { graphqlResponseToToolResponse, parseVariables } from "./converter";
import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter";
import { Client } from "../../../apiv2";
import { getDataConnectEmulatorClient } from "./emulator";
import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator";

export const execute_graphql = tool(
{
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/tools/dataconnect/execute_graphql_read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { z } from "zod";
import { tool } from "../../tool";
import * as dataplane from "../../../dataconnect/dataplaneClient";
import { pickService } from "../../../dataconnect/fileUtils";
import { graphqlResponseToToolResponse, parseVariables } from "./converter";
import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter";
import { Client } from "../../../apiv2";
import { getDataConnectEmulatorClient } from "./emulator";
import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator";

export const execute_graphql_read = tool(
{
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/tools/dataconnect/execute_mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { tool } from "../../tool";
import { mcpError } from "../../util";
import * as dataplane from "../../../dataconnect/dataplaneClient";
import { pickService } from "../../../dataconnect/fileUtils";
import { graphqlResponseToToolResponse, parseVariables } from "./converter";
import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter";
import { Client } from "../../../apiv2";
import { getDataConnectEmulatorClient } from "./emulator";
import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator";

export const execute_mutation = tool(
{
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/tools/dataconnect/execute_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { tool } from "../../tool";
import { mcpError } from "../../util";
import * as dataplane from "../../../dataconnect/dataplaneClient";
import { pickService } from "../../../dataconnect/fileUtils";
import { graphqlResponseToToolResponse, parseVariables } from "./converter";
import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter";
import { Client } from "../../../apiv2";
import { getDataConnectEmulatorClient } from "./emulator";
import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator";

export const execute_query = tool(
{
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tools/dataconnect/get_connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { tool } from "../../tool";
import { toContent } from "../../util";
import * as client from "../../../dataconnect/client";
import { pickService } from "../../../dataconnect/fileUtils";
import { connectorToText } from "./converter";
import { connectorToText } from "../../util/dataconnect/converter";

export const get_connectors = tool(
{
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tools/dataconnect/get_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { tool } from "../../tool";
import { toContent } from "../../util";
import * as client from "../../../dataconnect/client";
import { pickService } from "../../../dataconnect/fileUtils";
import { schemaToText } from "./converter";
import { schemaToText } from "../../util/dataconnect/converter";

export const get_schema = tool(
{
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/tools/dataconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { execute_graphql } from "./execute_graphql";
import { execute_graphql_read } from "./execute_graphql_read";
import { execute_query } from "./execute_query";
import { execute_mutation } from "./execute_mutation";
import { compile } from "./compile";

export const dataconnectTools: ServerTool[] = [
compile,
list_services,
generate_schema,
generate_operation,
Expand Down
21 changes: 21 additions & 0 deletions src/mcp/util/dataconnect/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { prettify } from "../../../dataconnect/graphqlError";
import { ServiceInfo } from "../../../dataconnect/types";

Check failure on line 2 in src/mcp/util/dataconnect/compile.ts

View workflow job for this annotation

GitHub Actions / unit (20)

'ServiceInfo' is defined but never used

Check failure on line 2 in src/mcp/util/dataconnect/compile.ts

View workflow job for this annotation

GitHub Actions / unit (22)

'ServiceInfo' is defined but never used

Check failure on line 2 in src/mcp/util/dataconnect/compile.ts

View workflow job for this annotation

GitHub Actions / unit (20)

'ServiceInfo' is defined but never used

Check failure on line 2 in src/mcp/util/dataconnect/compile.ts

View workflow job for this annotation

GitHub Actions / lint (20)

'ServiceInfo' is defined but never used

Check failure on line 2 in src/mcp/util/dataconnect/compile.ts

View workflow job for this annotation

GitHub Actions / unit (22)

'ServiceInfo' is defined but never used
import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator";

export async function compileErrors(
configDir: string,
errorFilter?: "all" | "schema" | "operations",
) {
const errors = (await DataConnectEmulator.build({ configDir })).errors;
return (
errors
?.filter((e) => {
const isOperationError = ["query", "mutation"].includes(e.path?.[0] as string);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all clients follow the query & mutation convention. Only our CLI init template does.

We should probably not support errorFilter for now.

I can find a correct way to do this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I believe this will always be correct -- the path is like ["query", "users", "someField"] because it's mapping query { users { someField }}. I believe it will always work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. This is the GQL path error.

I got it mixed up with the file name.

if (errorFilter === "operations") return isOperationError;
if (errorFilter === "schema") return !isOperationError;
return true;
})
.map(prettify)
.join("\n") || ""
);
}
Loading
Loading