-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(mcp): Adds dataconnect_compile
tool to MCP server.
#8979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
5393734
d1905c0
f8639b7
9d4189f
fdbd439
9aa46c5
7d1b504
5ff2dc6
ce98e24
b7775df
edda60f
f5827a7
2f0b457
6b06187
7c8af96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
|
||
const configs = readFirebaseJson(config); | ||
return Promise.all(configs.map((c) => load(projectId, config, c.source))); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,7 +54,7 @@ | |
] as const; | ||
|
||
export class FirebaseMcpServer { | ||
private _ready: boolean = false; | ||
private _readyPromises: { resolve: () => void; reject: (err: unknown) => void }[] = []; | ||
startupRoot?: string; | ||
cachedProjectRoot?: string; | ||
|
@@ -86,7 +86,7 @@ | |
mcp_client_name: this.clientInfo?.name || "<unknown-client>", | ||
mcp_client_version: this.clientInfo?.version || "<unknown-version>", | ||
}; | ||
trackGA4(event, { ...params, ...clientInfoParams }); | ||
} | ||
|
||
constructor(options: { activeFeatures?: ServerFeature[]; projectRoot?: string }) { | ||
|
@@ -104,11 +104,11 @@ | |
this.server.setRequestHandler(ListPromptsRequestSchema, this.mcpListPrompts.bind(this)); | ||
this.server.setRequestHandler(GetPromptRequestSchema, this.mcpGetPrompt.bind(this)); | ||
|
||
this.server.oninitialized = async () => { | ||
const clientInfo = this.server.getClientVersion(); | ||
this.clientInfo = clientInfo; | ||
if (clientInfo?.name) { | ||
this.trackGA4("mcp_client_connected"); | ||
} | ||
if (!this.clientInfo?.name) this.clientInfo = { name: "<unknown-client>" }; | ||
|
||
|
@@ -123,12 +123,12 @@ | |
return {}; | ||
}); | ||
|
||
this.detectProjectRoot(); | ||
this.detectActiveFeatures(); | ||
} | ||
|
||
/** Wait until initialization has finished. */ | ||
ready() { | ||
if (this._ready) return Promise.resolve(); | ||
return new Promise((resolve, reject) => { | ||
this._readyPromises.push({ resolve: resolve as () => void, reject }); | ||
|
@@ -139,7 +139,7 @@ | |
return this.clientInfo?.name ?? (isFirebaseStudio() ? "Firebase Studio" : "<unknown-client>"); | ||
} | ||
|
||
private get clientConfigKey() { | ||
return `mcp.clientConfigs.${this.clientName}:${this.startupRoot || process.cwd()}`; | ||
} | ||
|
||
|
@@ -227,6 +227,10 @@ | |
} | ||
|
||
get availablePrompts(): ServerPrompt[] { | ||
this.log( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this change intended? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
); | ||
|
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); | ||
} |
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
|
||
mbleigh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.`, | ||
}, | ||
}, | ||
]; | ||
}, | ||
); |
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.", | ||
mbleigh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
inputSchema: z.object({ | ||
error_filter: z | ||
.enum(["all", "schema", "operations"]) | ||
mbleigh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.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." }] }; | ||
}, | ||
); |
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
|
||
mbleigh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all clients follow the We should probably not support I can find a correct way to do this later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I believe this will always be correct -- the path is like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") || "" | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since your PR, I added
loadAll
in a different file~https://github.com/firebase/firebase-tools/blob/2f0b457b299019ada540f0ebd96c64070af19578/src/dataconnect/load.ts#L61C23-L61C30