From 5393734b8e6e51ff2e9774febe60f14439877295 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 10:22:34 -0700 Subject: [PATCH 01/10] feat(mcp): Adds `dataconnect_compile` tool to MCP server. --- package.json | 3 +- src/dataconnect/loadAll.ts | 8 ++ src/experiments.ts | 5 ++ src/mcp/index.ts | 12 +++ src/mcp/prompts/dataconnect/index.ts | 9 +++ src/mcp/prompts/dataconnect/schema.ts | 74 +++++++++++++++++++ src/mcp/prompts/index.ts | 5 +- src/mcp/tools/dataconnect/compile.ts | 47 ++++++++++++ src/mcp/tools/dataconnect/converter.ts | 68 ----------------- src/mcp/tools/dataconnect/emulator.ts | 16 ---- src/mcp/tools/dataconnect/execute_graphql.ts | 4 +- .../tools/dataconnect/execute_graphql_read.ts | 4 +- src/mcp/tools/dataconnect/execute_mutation.ts | 4 +- src/mcp/tools/dataconnect/execute_query.ts | 4 +- src/mcp/tools/dataconnect/get_connector.ts | 2 +- src/mcp/tools/dataconnect/get_schema.ts | 2 +- src/mcp/tools/dataconnect/index.ts | 2 + 17 files changed, 172 insertions(+), 97 deletions(-) create mode 100644 src/dataconnect/loadAll.ts create mode 100644 src/mcp/prompts/dataconnect/index.ts create mode 100644 src/mcp/prompts/dataconnect/schema.ts create mode 100644 src/mcp/tools/dataconnect/compile.ts delete mode 100644 src/mcp/tools/dataconnect/converter.ts delete mode 100644 src/mcp/tools/dataconnect/emulator.ts diff --git a/package.json b/package.json index c70003bf97b..f9b49fc3589 100644 --- a/package.json +++ b/package.json @@ -273,5 +273,6 @@ "node-fetch": { "whatwg-url": "^14.0.0" } - } + }, + "packageManager": "pnpm@10.13.1+sha256.0f9ed48d808996ae007835fb5c4641cf9a300def2eddc9e957d9bbe4768c5f28" } diff --git a/src/dataconnect/loadAll.ts b/src/dataconnect/loadAll.ts new file mode 100644 index 00000000000..b7a473d3d97 --- /dev/null +++ b/src/dataconnect/loadAll.ts @@ -0,0 +1,8 @@ +import { load } from "./load"; +import type { Config } from "../config"; +import { readFirebaseJson } from "./fileUtils"; + +export function loadAll(projectId: string, config: Config) { + const configs = readFirebaseJson(config); + return Promise.all(configs.map((c) => load(projectId, config, c.source))); +} diff --git a/src/experiments.ts b/src/experiments.ts index a7929494f99..a1010f0e5b2 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -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, diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 477bf9581e9..8f4fb7cb95c 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -227,6 +227,10 @@ export class FirebaseMcpServer { } get availablePrompts(): ServerPrompt[] { + this.log( + "debug", + `availablePrompts: ${JSON.stringify(this.activeFeatures)} // ${JSON.stringify(this.detectedFeatures)}`, + ); return availablePrompts( this.activeFeatures?.length ? this.activeFeatures : this.detectedFeatures, ); @@ -357,6 +361,14 @@ export class FirebaseMcpServer { const hasActiveProject = !!(await this.getProjectId()); await this.trackGA4("mcp_list_prompts"); const skipAutoAuthForStudio = isFirebaseStudio(); + this.log( + "debug", + `availablePrompts: ${this.availablePrompts.map((p) => p.mcp.name).join(", ")}`, + ); + this.log( + "debug", + `availablePrompts: ${this.availablePrompts.map((p) => p.mcp.name).join(", ")}`, + ); return { prompts: this.availablePrompts.map((p) => ({ name: p.mcp.name, diff --git a/src/mcp/prompts/dataconnect/index.ts b/src/mcp/prompts/dataconnect/index.ts new file mode 100644 index 00000000000..e1c323ec73b --- /dev/null +++ b/src/mcp/prompts/dataconnect/index.ts @@ -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); +} diff --git a/src/mcp/prompts/dataconnect/schema.ts b/src/mcp/prompts/dataconnect/schema.ts new file mode 100644 index 00000000000..69c2a33533b --- /dev/null +++ b/src/mcp/prompts/dataconnect/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod"; +import { prompt } from "../../prompt"; +import { loadAll } from "../../../dataconnect/loadAll"; +import type { ServiceInfo } from "../../../dataconnect/types"; +import { BUILTIN_SDL, MAIN_INSTRUCTIONS } from "../../lib/dataconnect/content"; +import { compileErrors } from "../../lib/dataconnect/compile"; + +function renderServices(fdcServices: ServiceInfo[]) { + if (!fdcServices.length) return "Data Connect Status: "; + + 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 || ""}`; +} + +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", + }, + }, + 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 || ""} +Project ID: ${projectId || ""} +${renderServices(fdcServices)}${renderErrors(buildErrors)} + +==== USER PROMPT ==== + +${prompt} + +==== TASK INSTRUCTIONS ==== + +1. If Data Connect is marked as \`\`, first run the \`firebase_init\` tool with \`{dataconnect: {}}\` arguments to initialize it. +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 +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.`, + }, + }, + ]; + }, +); diff --git a/src/mcp/prompts/index.ts b/src/mcp/prompts/index.ts index f621d86d997..b0d8205c935 100644 --- a/src/mcp/prompts/index.ts +++ b/src/mcp/prompts/index.ts @@ -1,12 +1,13 @@ import { ServerFeature } from "../types"; import { ServerPrompt } from "../prompt"; import { corePrompts } from "./core"; +import { dataconnectPrompts } from "./dataconnect"; const prompts: Record = { core: corePrompts, firestore: [], storage: [], - dataconnect: [], + dataconnect: dataconnectPrompts, auth: [], messaging: [], remoteconfig: [], @@ -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[]; } diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts new file mode 100644 index 00000000000..a60ad9e6349 --- /dev/null +++ b/src/mcp/tools/dataconnect/compile.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { pickService } from "../../../dataconnect/fileUtils"; +import { compileErrors } from "../../lib/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." }] }; + }, +); diff --git a/src/mcp/tools/dataconnect/converter.ts b/src/mcp/tools/dataconnect/converter.ts deleted file mode 100644 index 29939e2a03c..00000000000 --- a/src/mcp/tools/dataconnect/converter.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { dump } from "js-yaml"; -import { - Schema, - Connector, - Source, - GraphqlResponseError, - GraphqlResponse, - isGraphQLResponse, -} from "../../../dataconnect/types"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { mcpError } from "../../util"; - -export function schemaToText(s: Schema): string { - return ( - dump({ - name: s.name, - datasources: s.datasources, - }) + - "\n\n" + - sourceToText(s.source) - ); -} - -export function connectorToText(s: Connector): string { - return ( - dump({ - name: s.name, - }) + - "\n\n" + - sourceToText(s.source) - ); -} - -export function sourceToText(s: Source): string { - let output = ""; - s.files?.forEach((f) => { - output += `\n# ${f.path}`; - output += "\n```graphql\n"; - output += `${f.content.trim()}\n`; - output += "```\n"; - }); - return output; -} - -export function graphqlResponseToToolResponse( - g: GraphqlResponse | GraphqlResponseError, -): CallToolResult { - if (isGraphQLResponse(g)) { - const isError = g.errors?.length > 0; - const contentString = `${isError ? "A GraphQL error occurred while executing the operation:" : ""}${JSON.stringify(g, null, 2)}`; - return { - isError, - content: [{ type: "text", text: contentString }], - }; - } else { - return mcpError(JSON.stringify(g, null, 2)); - } -} - -export function parseVariables(unparsedVariables?: string): Record { - try { - const variables = JSON.parse(unparsedVariables || "{}"); - if (typeof variables !== "object") throw new Error("not an object"); - return variables; - } catch (e) { - throw new Error("Provided variables string `" + unparsedVariables + "` is not valid JSON."); - } -} diff --git a/src/mcp/tools/dataconnect/emulator.ts b/src/mcp/tools/dataconnect/emulator.ts deleted file mode 100644 index b0a75d60ac8..00000000000 --- a/src/mcp/tools/dataconnect/emulator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Emulators } from "../../../emulator/types"; -import { Client } from "../../../apiv2"; -import { DATACONNECT_API_VERSION } from "../../../dataconnect/dataplaneClient"; -import type { FirebaseMcpServer } from "../../index"; - -export async function getDataConnectEmulatorClient(host: FirebaseMcpServer): Promise { - const emulatorUrl = await host.getEmulatorUrl(Emulators.DATACONNECT); - - const apiClient = new Client({ - urlPrefix: emulatorUrl, - apiVersion: DATACONNECT_API_VERSION, - auth: false, - }); - - return apiClient; -} diff --git a/src/mcp/tools/dataconnect/execute_graphql.ts b/src/mcp/tools/dataconnect/execute_graphql.ts index b0e52548960..1943f784404 100644 --- a/src/mcp/tools/dataconnect/execute_graphql.ts +++ b/src/mcp/tools/dataconnect/execute_graphql.ts @@ -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 "../../lib/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "./emulator"; +import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; export const execute_graphql = tool( { diff --git a/src/mcp/tools/dataconnect/execute_graphql_read.ts b/src/mcp/tools/dataconnect/execute_graphql_read.ts index 73f757f9022..d3e9873306f 100644 --- a/src/mcp/tools/dataconnect/execute_graphql_read.ts +++ b/src/mcp/tools/dataconnect/execute_graphql_read.ts @@ -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 "../../lib/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "./emulator"; +import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; export const execute_graphql_read = tool( { diff --git a/src/mcp/tools/dataconnect/execute_mutation.ts b/src/mcp/tools/dataconnect/execute_mutation.ts index 0db07e2c9a5..f637b817aaa 100644 --- a/src/mcp/tools/dataconnect/execute_mutation.ts +++ b/src/mcp/tools/dataconnect/execute_mutation.ts @@ -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 "../../lib/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "./emulator"; +import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; export const execute_mutation = tool( { diff --git a/src/mcp/tools/dataconnect/execute_query.ts b/src/mcp/tools/dataconnect/execute_query.ts index fc4576c0d43..1d9ab715b7d 100644 --- a/src/mcp/tools/dataconnect/execute_query.ts +++ b/src/mcp/tools/dataconnect/execute_query.ts @@ -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 "../../lib/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "./emulator"; +import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; export const execute_query = tool( { diff --git a/src/mcp/tools/dataconnect/get_connector.ts b/src/mcp/tools/dataconnect/get_connector.ts index 6ff835228d6..c966934f3c8 100644 --- a/src/mcp/tools/dataconnect/get_connector.ts +++ b/src/mcp/tools/dataconnect/get_connector.ts @@ -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 "../../lib/dataconnect/converter"; export const get_connectors = tool( { diff --git a/src/mcp/tools/dataconnect/get_schema.ts b/src/mcp/tools/dataconnect/get_schema.ts index ab673d72ac6..a0c39ff9f3f 100644 --- a/src/mcp/tools/dataconnect/get_schema.ts +++ b/src/mcp/tools/dataconnect/get_schema.ts @@ -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 "../../lib/dataconnect/converter"; export const get_schema = tool( { diff --git a/src/mcp/tools/dataconnect/index.ts b/src/mcp/tools/dataconnect/index.ts index ee4e5755d5f..8e460728082 100644 --- a/src/mcp/tools/dataconnect/index.ts +++ b/src/mcp/tools/dataconnect/index.ts @@ -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, From d1905c0c4cc13e4c1d000c8767302b30b8982905 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 10:25:49 -0700 Subject: [PATCH 02/10] revert pnpm --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index f9b49fc3589..c70003bf97b 100644 --- a/package.json +++ b/package.json @@ -273,6 +273,5 @@ "node-fetch": { "whatwg-url": "^14.0.0" } - }, - "packageManager": "pnpm@10.13.1+sha256.0f9ed48d808996ae007835fb5c4641cf9a300def2eddc9e957d9bbe4768c5f28" + } } From f8639b7145f3135e38d6328e490660fd967a66cd Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 10:27:55 -0700 Subject: [PATCH 03/10] rename lib -> util bc gitignore --- src/mcp/prompts/dataconnect/schema.ts | 4 +- src/mcp/tools/dataconnect/compile.ts | 2 +- src/mcp/tools/dataconnect/execute_graphql.ts | 4 +- .../tools/dataconnect/execute_graphql_read.ts | 4 +- src/mcp/tools/dataconnect/execute_mutation.ts | 4 +- src/mcp/tools/dataconnect/execute_query.ts | 4 +- src/mcp/tools/dataconnect/get_connector.ts | 2 +- src/mcp/tools/dataconnect/get_schema.ts | 2 +- src/mcp/util/dataconnect/compile.ts | 21 + src/mcp/util/dataconnect/content.ts | 653 ++++++++++++++++++ src/mcp/util/dataconnect/converter.ts | 68 ++ src/mcp/util/dataconnect/emulator.ts | 16 + 12 files changed, 771 insertions(+), 13 deletions(-) create mode 100644 src/mcp/util/dataconnect/compile.ts create mode 100644 src/mcp/util/dataconnect/content.ts create mode 100644 src/mcp/util/dataconnect/converter.ts create mode 100644 src/mcp/util/dataconnect/emulator.ts diff --git a/src/mcp/prompts/dataconnect/schema.ts b/src/mcp/prompts/dataconnect/schema.ts index 69c2a33533b..f5d5e51adb1 100644 --- a/src/mcp/prompts/dataconnect/schema.ts +++ b/src/mcp/prompts/dataconnect/schema.ts @@ -2,8 +2,8 @@ import { z } from "zod"; import { prompt } from "../../prompt"; import { loadAll } from "../../../dataconnect/loadAll"; import type { ServiceInfo } from "../../../dataconnect/types"; -import { BUILTIN_SDL, MAIN_INSTRUCTIONS } from "../../lib/dataconnect/content"; -import { compileErrors } from "../../lib/dataconnect/compile"; +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: "; diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index a60ad9e6349..ebb1d47ad27 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { tool } from "../../tool"; import { pickService } from "../../../dataconnect/fileUtils"; -import { compileErrors } from "../../lib/dataconnect/compile"; +import { compileErrors } from "../../util/dataconnect/compile"; export const compile = tool( { diff --git a/src/mcp/tools/dataconnect/execute_graphql.ts b/src/mcp/tools/dataconnect/execute_graphql.ts index 1943f784404..6aa0abf4654 100644 --- a/src/mcp/tools/dataconnect/execute_graphql.ts +++ b/src/mcp/tools/dataconnect/execute_graphql.ts @@ -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 "../../lib/dataconnect/converter"; +import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; +import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; export const execute_graphql = tool( { diff --git a/src/mcp/tools/dataconnect/execute_graphql_read.ts b/src/mcp/tools/dataconnect/execute_graphql_read.ts index d3e9873306f..0ef30479512 100644 --- a/src/mcp/tools/dataconnect/execute_graphql_read.ts +++ b/src/mcp/tools/dataconnect/execute_graphql_read.ts @@ -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 "../../lib/dataconnect/converter"; +import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; +import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; export const execute_graphql_read = tool( { diff --git a/src/mcp/tools/dataconnect/execute_mutation.ts b/src/mcp/tools/dataconnect/execute_mutation.ts index f637b817aaa..785db445b37 100644 --- a/src/mcp/tools/dataconnect/execute_mutation.ts +++ b/src/mcp/tools/dataconnect/execute_mutation.ts @@ -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 "../../lib/dataconnect/converter"; +import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; +import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; export const execute_mutation = tool( { diff --git a/src/mcp/tools/dataconnect/execute_query.ts b/src/mcp/tools/dataconnect/execute_query.ts index 1d9ab715b7d..2e8287a3e3a 100644 --- a/src/mcp/tools/dataconnect/execute_query.ts +++ b/src/mcp/tools/dataconnect/execute_query.ts @@ -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 "../../lib/dataconnect/converter"; +import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; import { Client } from "../../../apiv2"; -import { getDataConnectEmulatorClient } from "../../lib/dataconnect/emulator"; +import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; export const execute_query = tool( { diff --git a/src/mcp/tools/dataconnect/get_connector.ts b/src/mcp/tools/dataconnect/get_connector.ts index c966934f3c8..a725b5bc787 100644 --- a/src/mcp/tools/dataconnect/get_connector.ts +++ b/src/mcp/tools/dataconnect/get_connector.ts @@ -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 "../../lib/dataconnect/converter"; +import { connectorToText } from "../../util/dataconnect/converter"; export const get_connectors = tool( { diff --git a/src/mcp/tools/dataconnect/get_schema.ts b/src/mcp/tools/dataconnect/get_schema.ts index a0c39ff9f3f..4d79d092194 100644 --- a/src/mcp/tools/dataconnect/get_schema.ts +++ b/src/mcp/tools/dataconnect/get_schema.ts @@ -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 "../../lib/dataconnect/converter"; +import { schemaToText } from "../../util/dataconnect/converter"; export const get_schema = tool( { diff --git a/src/mcp/util/dataconnect/compile.ts b/src/mcp/util/dataconnect/compile.ts new file mode 100644 index 00000000000..c81165d35af --- /dev/null +++ b/src/mcp/util/dataconnect/compile.ts @@ -0,0 +1,21 @@ +import { prettify } from "../../../dataconnect/graphqlError"; +import { ServiceInfo } from "../../../dataconnect/types"; +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); + if (errorFilter === "operations") return isOperationError; + if (errorFilter === "schema") return !isOperationError; + return true; + }) + .map(prettify) + .join("\n") || "" + ); +} diff --git a/src/mcp/util/dataconnect/content.ts b/src/mcp/util/dataconnect/content.ts new file mode 100644 index 00000000000..417d72a73a0 --- /dev/null +++ b/src/mcp/util/dataconnect/content.ts @@ -0,0 +1,653 @@ +export const MAIN_INSTRUCTIONS = ` +Closely follow the following instructions: + +You are Firebase Data Connect expert that is responsible for creating data connect schemas code in GraphQL for users.You will be given a description of the desired schema using Firebase Data Connect and your task is to write the schema code in GraphQL that fulfills the requirements and correct any mistakes in your generation. + +For example, if I were to ask for a schema for a GraphQL database that contains a table called "users" with a field called "name" and another table called "posts" with a field called "body", I would get the following schema: +\`\`\` +type User @table { + name: String! +} + +type Post @table { + body: String! + author: User +} +\`\`\` + +Simple Firebase Data Connect schema often takes the following form: +\`\`\`graphql +type TableName @table { + uuidField: UUID + uuidArrayField: [UUID] + stringField: String + stringArrayField: [String] + intField: Int + intArrayField: [Int] + int64Field: Int64 + int64ArrayField: [Int64] + floatField: Float + floatArrayField: [Float] + booleanField: Boolean + booleanArrayField: [Boolean] + timestampField: Timestamp + timestampArrayField: [Timestamp] + dateField: Date + dateArrayField: [Date] + vectorField: Vector @col(size:168) +} +\`\`\` + +Leave out objects named after \`Query\` and \`Mutation\` + +Firebase Data Connect implicitly adds \`id: UUID!\` to every table and implicitly makes it primary key. Therefore, leave out the \`id\` field. + +Use \`UUID\` type instead of \`ID\` type or \`String\` type for id-like fields. + +Array reference fields, like \`[SomeTable]\` and \`[SomeTable!]!\`, are not supported. Use the singular reference field instead. +For example, for a one-to-many relationship like one user is assiend to many bugs in a software project: +\`\`\`graphql +type User @table { + name: String! + # bugs: [Bug] # Not supported. Do not use +} + +type Bug @table { + title: String! + assignee: User + reporter: User +} +\`\`\` + +For another example, for a many-to-many relationship like each crew member is assigned to many chores and each chores requires many crews to complete: +\`\`\`graphql +type Crew @table { + name: String! + # assignedChores: [Chore!]! # No supported. Do not use +} + +type Chore @table { + name: String! + description: String! + # assignedCrews: [Crews!]! # No supported. Do not use +} + +type Assignment @table(key: ["crew", "chore"]) { + crew: Crew! + chore: Chore! +} +\`\`\` + +Leave out \`@relation\` because it is not supported yet. + +Leave out \`directive\`, \`enum\` and \`scalar\`. + +Leave out \`@view\`. + +Be sure that your response contains a valid Firebase Data Connect schema in a single GraphQL code block inside of triple backticks and closely follows my instructions and description. +`.trim(); + +export const BUILTIN_SDL = ` +# Directives + +Directives define specific behaviors that can be applied to fields or types within a GraphQL schema. + +## Data Connect Defined + +### @col on \`FIELD_DEFINITION\` {:#col} +Customizes a field that represents a SQL database table column. + +Data Connect maps scalar Fields on [\`@table\`](directive.md#table) type to a SQL column of +corresponding data type. + +- scalar [\`UUID\`](scalar.md#UUID) maps to [\`uuid\`](https://www.postgresql.org/docs/current/datatype-uuid.html). +- scalar [\`String\`](scalar.md#String) maps to [\`text\`](https://www.postgresql.org/docs/current/datatype-character.html). +- scalar [\`Int\`](scalar.md#Int) maps to [\`int\`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar [\`Int64\`](scalar.md#Int64) maps to [\`bigint\`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar [\`Float\`](scalar.md#Float) maps to [\`double precision\`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar [\`Boolean\`](scalar.md#Boolean) maps to [\`boolean\`](https://www.postgresql.org/docs/current/datatype-boolean.html). +- scalar [\`Date\`](scalar.md#Date) maps to [\`date\`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- scalar [\`Timestamp\`](scalar.md#Timestamp) maps to [\`timestamptz\`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- scalar [\`Any\`](scalar.md#Any) maps to [\`jsonb\`](https://www.postgresql.org/docs/current/datatype-json.html). +- scalar [\`Vector\`](scalar.md#Vector) maps to [\`pgvector\`](https://github.com/pgvector/pgvector). + +Array scalar fields are mapped to [Postgres arrays](https://www.postgresql.org/docs/current/arrays.html). + +###### Example: Serial Primary Key + +For example, you can define auto-increment primary key. + +\`\`\`graphql +type Post @table { + id: Int! @col(name: "post_id", dataType: "serial") +} +\`\`\` + +Data Connect converts it to the following SQL table schema. + +\`\`\`sql +CREATE TABLE "public"."post" ( + "post_id" serial NOT NULL, + PRIMARY KEY ("id") +) +\`\`\` + +###### Example: Vector + +\`\`\`graphql +type Post @table { + content: String! @col(name: "post_content") + contentEmbedding: Vector! @col(size:768) +} +\`\`\` + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | The SQL database column name. Defaults to snake_case of the field name. | +| \`dataType\` | [\`String\`](scalar.md#String) | Configures the custom SQL data type. Each GraphQL type can map to multiple SQL data types. Refer to [Postgres supported data types](https://www.postgresql.org/docs/current/datatype.html). Incompatible SQL data type will lead to undefined behavior. | +| \`size\` | [\`Int\`](scalar.md#Int) | Required on [\`Vector\`](scalar.md#Vector) columns. It specifies the length of the Vector. \`textembedding-gecko@003\` model generates [\`Vector\`](scalar.md#Vector) of \`@col(size:768)\`. | + +### @default on \`FIELD_DEFINITION\` {:#default} +Specifies the default value for a column field. + +For example: + +\`\`\`graphql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + number: Int! @col(dataType: "serial") + createdAt: Date! @default(expr: "request.time") + role: String! @default(value: "Member") + credit: Int! @default(value: 100) +} +\`\`\` + +The supported arguments vary based on the field type. + +| Argument | Type | Description | +|---|---|---| +| \`value\` | [\`Any\`](scalar.md#Any) | A constant value validated against the field's GraphQL type during compilation. | +| \`expr\` | [\`Any_Expr\`](scalar.md#Any_Expr) | A CEL expression whose return value must match the field's data type. | +| \`sql\` | [\`Any_SQL\`](scalar.md#Any_SQL) | A raw SQL expression, whose SQL data type must match the underlying column. The value is any variable-free expression (in particular, cross-references to other columns in the current table are not allowed). Subqueries are not allowed either. See [PostgreSQL defaults](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-DEFAULT) for more details. | + +### @index on \`FIELD_DEFINITION\` | \`OBJECT\` {:#index} +Defines a database index to optimize query performance. + +\`\`\`graphql +type User @table @index(fields: ["name", "phoneNumber"], order: [ASC, DESC]) { + name: String @index + phoneNumber: Int64 @index + tags: [String] @index # GIN Index +} +\`\`\` + +##### Single Field Index + +You can put [\`@index\`](directive.md#index) on a [\`@col\`](directive.md#col) field to create a SQL index. + +\`@index(order)\` matters little for single field indexes, as they can be scanned +in both directions. + +##### Composite Index + +You can put \`@index(fields: [...])\` on [\`@table\`](directive.md#table) type to define composite indexes. + +\`@index(order: [...])\` can customize the index order to satisfy particular +filter and order requirement. + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | Configure the SQL database index id. If not overridden, Data Connect generates the index name: - \`{table_name}_{first_field}_{second_field}_aa_idx\` - \`{table_name}_{field_name}_idx\` | +| \`fields\` | [\`[String!]\`](scalar.md#String) | Only allowed and required when used on a [\`@table\`](directive.md#table) type. Specifies the fields to create the index on. | +| \`order\` | [\`[IndexFieldOrder!]\`](enum.md#IndexFieldOrder) | Only allowed for \`BTREE\` [\`@index\`](directive.md#index) on [\`@table\`](directive.md#table) type. Specifies the order for each indexed column. Defaults to all \`ASC\`. | +| \`type\` | [\`IndexType\`](enum.md#IndexType) | Customize the index type. For most index, it defaults to \`BTREE\`. For array fields, only allowed [\`IndexType\`](enum.md#IndexType) is \`GIN\`. For [\`Vector\`](scalar.md#Vector) fields, defaults to \`HNSW\`, may configure to \`IVFFLAT\`. | +| \`vector_method\` | [\`VectorSimilarityMethod\`](enum.md#VectorSimilarityMethod) | Only allowed when used on vector field. Defines the vector similarity method. Defaults to \`INNER_PRODUCT\`. | + +### @ref on \`FIELD_DEFINITION\` {:#ref} +Defines a foreign key reference to another table. + +For example, we can define a many-to-one relation. + +\`\`\`graphql +type ManyTable @table { + refField: OneTable! +} +type OneTable @table { + someField: String! +} +\`\`\` +Data Connect adds implicit foreign key column and relation query field. So the +above schema is equivalent to the following schema. + +\`\`\`graphql +type ManyTable @table { + id: UUID! @default(expr: "uuidV4()") + refField: OneTable! @ref(fields: "refFieldId", references: "id") + refFieldId: UUID! +} +type OneTable @table { + id: UUID! @default(expr: "uuidV4()") + someField: UUID! + # Generated Fields: + # manyTables_on_refField: [ManyTable!]! +} +\`\`\` +Data Connect generates the necessary foreign key constraint. + +\`\`\`sql +CREATE TABLE "public"."many_table" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "ref_field_id" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "many_table_ref_field_id_fkey" FOREIGN KEY ("ref_field_id") REFERENCES "public"."one_table" ("id") ON DELETE CASCADE +) +\`\`\` + +###### Example: Traverse the Reference Field + +\`\`\`graphql +query ($id: UUID!) { + manyTable(id: $id) { + refField { id } + } +} +\`\`\` + +###### Example: Reverse Traverse the Reference field + +\`\`\`graphql +query ($id: UUID!) { + oneTable(id: $id) { + manyTables_on_refField { id } + } +} +\`\`\` + +##### Optional Many-to-One Relation + +An optional foreign key reference will be set to null if the referenced row is deleted. + +In this example, if a \`User\` is deleted, the \`assignee\` and \`reporter\` +references will be set to null. + +\`\`\`graphql +type Bug @table { + title: String! + assignee: User + reproter: User +} + +type User @table { name: String! } +\`\`\` + +##### Required Many-to-One Relation + +A required foreign key reference will cascade delete if the referenced row is +deleted. + +In this example, if a \`Post\` is deleted, associated comments will also be +deleted. + +\`\`\`graphql +type Comment @table { + post: Post! + content: String! +} + +type Post @table { title: String! } +\`\`\` + +##### Many To Many Relation + +You can define a many-to-many relation with a join table. + +\`\`\`graphql +type Membership @table(key: ["group", "user"]) { + group: Group! + user: User! + role: String! @default(value: "member") +} + +type Group @table { name: String! } +type User @table { name: String! } +\`\`\` + +When Data Connect sees a table with two reference field as its primary key, it +knows this is a join table, so expands the many-to-many query field. + +\`\`\`graphql +type Group @table { + name: String! + # Generated Fields: + # users_via_Membership: [User!]! + # memberships_on_group: [Membership!]! +} +type User @table { + name: String! + # Generated Fields: + # groups_via_Membership: [Group!]! + # memberships_on_user: [Membership!]! +} +\`\`\` + +###### Example: Traverse the Many-To-Many Relation + +\`\`\`graphql +query ($id: UUID!) { + group(id: $id) { + users: users_via_Membership { + name + } + } +} +\`\`\` + +###### Example: Traverse to the Join Table + +\`\`\`graphql +query ($id: UUID!) { + group(id: $id) { + memberships: memberships_on_group { + user { name } + role + } + } +} +\`\`\` + +##### One To One Relation + +You can even define a one-to-one relation with the help of [\`@unique\`](directive.md#unique) or \`@table(key)\`. + +\`\`\`graphql +type User @table { + name: String +} +type Account @table { + user: User! @unique +} +# Alternatively, use primary key constraint. +# type Account @table(key: "user") { +# user: User! +# } +\`\`\` + +###### Example: Transerse the Reference Field + +\`\`\`graphql +query ($id: UUID!) { + account(id: $id) { + user { id } + } +} +\`\`\` + +###### Example: Reverse Traverse the Reference field + +\`\`\`graphql +query ($id: UUID!) { + user(id: $id) { + account_on_user { id } + } +} +\`\`\` + +##### Customizations + +- \`@ref(constraintName)\` can customize the SQL foreign key constraint name (\`table_name_ref_field_fkey\` above). +- \`@ref(fields)\` can customize the foreign key field names. +- \`@ref(references)\` can customize the constraint to reference other columns. + By default, \`@ref(references)\` is the primary key of the [\`@ref\`](directive.md#ref) table. + Other fields with [\`@unique\`](directive.md#unique) may also be referred in the foreign key constraint. + +| Argument | Type | Description | +|---|---|---| +| \`constraintName\` | [\`String\`](scalar.md#String) | The SQL database foreign key constraint name. Defaults to snake_case \`{table_name}_{field_name}_fkey\`. | +| \`fields\` | [\`[String!]\`](scalar.md#String) | Foreign key fields. Defaults to \`{tableName}{PrimaryIdName}\`. | +| \`references\` | [\`[String!]\`](scalar.md#String) | The fields that the foreign key references in the other table. Defaults to its primary key. | + +### @table on \`OBJECT\` {:#table} +Defines a relational database table. + +In this example, we defined one table with a field named \`myField\`. + +\`\`\`graphql +type TableName @table { + myField: String +} +\`\`\` +Data Connect adds an implicit \`id\` primary key column. So the above schema is equivalent to: + +\`\`\`graphql +type TableName @table(key: "id") { + id: String @default(expr: "uuidV4()") + myField: String +} +\`\`\` + +Data Connect generates the following SQL table and CRUD operations to use it. + +\`\`\`sql +CREATE TABLE "public"."table_name" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "my_field" text NULL, + PRIMARY KEY ("id") +) +\`\`\` + + * You can lookup a row: \`query ($id: UUID!) { tableName(id: $id) { myField } } \` + * You can find rows using: \`query tableNames(limit: 20) { myField }\` + * You can insert a row: \`mutation { tableName_insert(data: {myField: "foo"}) }\` + * You can update a row: \`mutation ($id: UUID!) { tableName_update(id: $id, data: {myField: "bar"}) }\` + * You can delete a row: \`mutation ($id: UUID!) { tableName_delete(id: $id) }\` + +##### Customizations + +- \`@table(singular)\` and \`@table(plural)\` can customize the singular and plural name. +- \`@table(name)\` can customize the Postgres table name. +- \`@table(key)\` can customize the primary key field name and type. + +For example, the \`User\` table often has a \`uid\` as its primary key. + +\`\`\`graphql +type User @table(key: "uid") { + uid: String! + name: String +} +\`\`\` + + * You can securely lookup a row: \`query { user(key: {uid_expr: "auth.uid"}) { name } } \` + * You can securely insert a row: \`mutation { user_insert(data: {uid_expr: "auth.uid" name: "Fred"}) }\` + * You can securely update a row: \`mutation { user_update(key: {uid_expr: "auth.uid"}, data: {name: "New Name"}) }\` + * You can securely delete a row: \`mutation { user_delete(key: {uid_expr: "auth.uid"}) }\` + +[\`@table\`](directive.md#table) type can be configured further with: + + - Custom SQL data types for columns. See [\`@col\`](directive.md#col). + - Add SQL indexes. See [\`@index\`](directive.md#index). + - Add SQL unique constraints. See [\`@unique\`](directive.md#unique). + - Add foreign key constraints to define relations. See [\`@ref\`](directive.md#ref). + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | Configures the SQL database table name. Defaults to snake_case like \`table_name\`. | +| \`singular\` | [\`String\`](scalar.md#String) | Configures the singular name. Defaults to the camelCase like \`tableName\`. | +| \`plural\` | [\`String\`](scalar.md#String) | Configures the plural name. Defaults to infer based on English plural pattern like \`tableNames\`. | +| \`key\` | [\`[String!]\`](scalar.md#String) | Defines the primary key of the table. Defaults to a single field named \`id\`. If not present already, Data Connect adds an implicit field \`id: UUID! @default(expr: "uuidV4()")\`. | + +### @unique on \`FIELD_DEFINITION\` | \`OBJECT\` {:#unique} +Defines unique constraints on [\`@table\`](directive.md#table). + +For example, + +\`\`\`graphql +type User @table { + phoneNumber: Int64 @unique +} +type UserProfile @table { + user: User! @unique + address: String @unique +} +\`\`\` + +- [\`@unique\`](directive.md#unique) on a [\`@col\`](directive.md#col) field adds a single-column unique constraint. +- [\`@unique\`](directive.md#unique) on a [\`@table\`](directive.md#table) type adds a composite unique constraint. +- [\`@unique\`](directive.md#unique) on a [\`@ref\`](directive.md#ref) defines a one-to-one relation. It adds unique constraint + on \`@ref(fields)\`. + +[\`@unique\`](directive.md#unique) ensures those fields can uniquely identify a row, so other [\`@table\`](directive.md#table) +type may define \`@ref(references)\` to refer to fields that has a unique constraint. + +| Argument | Type | Description | +|---|---|---| +| \`indexName\` | [\`String\`](scalar.md#String) | Configures the SQL database unique constraint name. If not overridden, Data Connect generates the unique constraint name: - \`table_name_first_field_second_field_uidx\` - \`table_name_only_field_name_uidx\` | +| \`fields\` | [\`[String!]\`](scalar.md#String) | Only allowed and required when used on OBJECT, this specifies the fields to create a unique constraint on. | + +### @view on \`OBJECT\` {:#view} +Defines a relational database Raw SQLview. + +Data Connect generates GraphQL queries with WHERE and ORDER BY clauses. +However, not all SQL features has native GraphQL equivalent. + +You can write **an arbitrary SQL SELECT statement**. Data Connect +would map Graphql fields on [\`@view\`](directive.md#view) type to columns in your SELECT statement. + +* Scalar GQL fields (camelCase) should match a SQL column (snake_case) + in the SQL SELECT statement. +* Reference GQL field can point to another [\`@table\`](directive.md#table) type. Similar to foreign key + defined with [\`@ref\`](directive.md#ref) on a [\`@table\`](directive.md#table) type, a [\`@view\`](directive.md#view) type establishes a relation + when \`@ref(fields)\` match \`@ref(references)\` on the target table. + +In this example, you can use \`@view(sql)\` to define an aggregation view on existing +table. + +\`\`\`graphql +type User @table { + name: String + score: Int +} +type UserAggregation @view(sql: """ + SELECT + COUNT(*) as count, + SUM(score) as sum, + AVG(score) as average, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY score) AS median, + (SELECT id FROM "user" LIMIT 1) as example_id + FROM "user" +""") { + count: Int + sum: Int + average: Float + median: Float + example: User + exampleId: UUID +} +\`\`\` + +###### Example: Query Raw SQL View + +\`\`\`graphql +query { + userAggregations { + count sum average median + exampleId example { id } + } +} +\`\`\` + +##### One-to-One View + +An one-to-one companion [\`@view\`](directive.md#view) can be handy if you want to argument a [\`@table\`](directive.md#table) +with additional implied content. + +\`\`\`graphql +type Restaurant @table { + name: String! +} +type Review @table { + restaurant: Restaurant! + rating: Int! +} +type RestaurantStats @view(sql: """ + SELECT + restaurant_id, + COUNT(*) AS review_count, + AVG(rating) AS average_rating + FROM review + GROUP BY restaurant_id +""") { + restaurant: Restaurant @unique + reviewCount: Int + averageRating: Float +} +\`\`\` + +In this example, [\`@unique\`](directive.md#unique) convey the assumption that each \`Restaurant\` should +have only one \`RestaurantStats\` object. + +###### Example: Query One-to-One View + +\`\`\`graphql +query ListRestaurants { + restaurants { + name + stats: restaurantStats_on_restaurant { + reviewCount + averageRating + } + } +} +\`\`\` + +###### Example: Filter based on One-to-One View + +\`\`\`graphql +query BestRestaurants($minAvgRating: Float, $minReviewCount: Int) { + restaurants(where: { + restaurantStats_on_restaurant: { + averageRating: {ge: $minAvgRating} + reviewCount: {ge: $minReviewCount} + } + }) { name } +} +\`\`\` + +##### Customizations + +- One of \`@view(sql)\` or \`@view(name)\` should be defined. + \`@view(name)\` can refer to a persisted SQL view in the Postgres schema. +- \`@view(singular)\` and \`@view(plural)\` can customize the singular and plural name. + +[\`@view\`](directive.md#view) type can be configured further: + + - [\`@unique\`](directive.md#unique) lets you define one-to-one relation. + - [\`@col\`](directive.md#col) lets you customize SQL column mapping. For example, \`@col(name: "column_in_select")\`. + +##### Limitations + +Raw SQL view doesn't have a primary key, so it doesn't support lookup. Other +[\`@table\`](directive.md#table) or [\`@view\`](directive.md#view) cannot have [\`@ref\`](directive.md#ref) to a view either. + +View cannot be mutated. You can perform CRUD operations on the underlying +table to alter its content. + +**Important: Data Connect doesn't parse and validate SQL** + +- If the SQL view is invalid or undefined, related requests may fail. +- If the SQL view return incompatible types. Firebase Data Connect may surface + errors. +- If a field doesn't have a corresponding column in the SQL SELECT statement, + it will always be \`null\`. +- There is no way to ensure VIEW to TABLE [\`@ref\`](directive.md#ref) constraint. +- All fields must be nullable in case they aren't found in the SELECT statement + or in the referenced table. + +**Important: You should always test [\`@view\`](directive.md#view)!** + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | The SQL view name. If neither \`name\` nor \`sql\` are provided, defaults to the snake_case of the singular type name. \`name\` and \`sql\` cannot be specified at the same time. | +| \`sql\` | [\`String\`](scalar.md#String) | SQL \`SELECT\` statement used as the basis for this type. SQL SELECT columns should use snake_case. GraphQL fields should use camelCase. \`name\` and \`sql\` cannot be specified at the same time. | +| \`singular\` | [\`String\`](scalar.md#String) | Configures the singular name. Defaults to the camelCase like \`viewName\`. | +| \`plural\` | [\`String\`](scalar.md#String) | Configures the plural name. Defaults to infer based on English plural pattern like \`viewNames\`. | +`.trim(); diff --git a/src/mcp/util/dataconnect/converter.ts b/src/mcp/util/dataconnect/converter.ts new file mode 100644 index 00000000000..29939e2a03c --- /dev/null +++ b/src/mcp/util/dataconnect/converter.ts @@ -0,0 +1,68 @@ +import { dump } from "js-yaml"; +import { + Schema, + Connector, + Source, + GraphqlResponseError, + GraphqlResponse, + isGraphQLResponse, +} from "../../../dataconnect/types"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { mcpError } from "../../util"; + +export function schemaToText(s: Schema): string { + return ( + dump({ + name: s.name, + datasources: s.datasources, + }) + + "\n\n" + + sourceToText(s.source) + ); +} + +export function connectorToText(s: Connector): string { + return ( + dump({ + name: s.name, + }) + + "\n\n" + + sourceToText(s.source) + ); +} + +export function sourceToText(s: Source): string { + let output = ""; + s.files?.forEach((f) => { + output += `\n# ${f.path}`; + output += "\n```graphql\n"; + output += `${f.content.trim()}\n`; + output += "```\n"; + }); + return output; +} + +export function graphqlResponseToToolResponse( + g: GraphqlResponse | GraphqlResponseError, +): CallToolResult { + if (isGraphQLResponse(g)) { + const isError = g.errors?.length > 0; + const contentString = `${isError ? "A GraphQL error occurred while executing the operation:" : ""}${JSON.stringify(g, null, 2)}`; + return { + isError, + content: [{ type: "text", text: contentString }], + }; + } else { + return mcpError(JSON.stringify(g, null, 2)); + } +} + +export function parseVariables(unparsedVariables?: string): Record { + try { + const variables = JSON.parse(unparsedVariables || "{}"); + if (typeof variables !== "object") throw new Error("not an object"); + return variables; + } catch (e) { + throw new Error("Provided variables string `" + unparsedVariables + "` is not valid JSON."); + } +} diff --git a/src/mcp/util/dataconnect/emulator.ts b/src/mcp/util/dataconnect/emulator.ts new file mode 100644 index 00000000000..b0a75d60ac8 --- /dev/null +++ b/src/mcp/util/dataconnect/emulator.ts @@ -0,0 +1,16 @@ +import { Emulators } from "../../../emulator/types"; +import { Client } from "../../../apiv2"; +import { DATACONNECT_API_VERSION } from "../../../dataconnect/dataplaneClient"; +import type { FirebaseMcpServer } from "../../index"; + +export async function getDataConnectEmulatorClient(host: FirebaseMcpServer): Promise { + const emulatorUrl = await host.getEmulatorUrl(Emulators.DATACONNECT); + + const apiClient = new Client({ + urlPrefix: emulatorUrl, + apiVersion: DATACONNECT_API_VERSION, + auth: false, + }); + + return apiClient; +} From 9d4189f3d7d0df250ef611af02b0463cc762dbf9 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 10:30:43 -0700 Subject: [PATCH 04/10] Update src/mcp/tools/dataconnect/compile.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/mcp/tools/dataconnect/compile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index ebb1d47ad27..511b0e6603a 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -37,7 +37,7 @@ export const compile = tool( content: [ { type: "text", - text: `The following errors were encountered while compiling Data Connect from directory \`${serviceInfo.sourceDirectory}\`:\n\n${errors}}`, + text: `The following errors were encountered while compiling Data Connect from directory \`${serviceInfo.sourceDirectory}\`:\n\n${errors}`, }, ], isError: true, From fdbd439293eeb41cd1efdc3844e0e8034fa9b205 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 10:31:14 -0700 Subject: [PATCH 05/10] remove extra logging --- src/mcp/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 8f4fb7cb95c..f6bafbfb5d4 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -361,14 +361,6 @@ export class FirebaseMcpServer { const hasActiveProject = !!(await this.getProjectId()); await this.trackGA4("mcp_list_prompts"); const skipAutoAuthForStudio = isFirebaseStudio(); - this.log( - "debug", - `availablePrompts: ${this.availablePrompts.map((p) => p.mcp.name).join(", ")}`, - ); - this.log( - "debug", - `availablePrompts: ${this.availablePrompts.map((p) => p.mcp.name).join(", ")}`, - ); return { prompts: this.availablePrompts.map((p) => ({ name: p.mcp.name, From 7d1b504bba36cd6957f091fdd88e61211453982e Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 10:31:52 -0700 Subject: [PATCH 06/10] Update src/mcp/prompts/dataconnect/schema.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/mcp/prompts/dataconnect/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/prompts/dataconnect/schema.ts b/src/mcp/prompts/dataconnect/schema.ts index f5d5e51adb1..6082b75b7c1 100644 --- a/src/mcp/prompts/dataconnect/schema.ts +++ b/src/mcp/prompts/dataconnect/schema.ts @@ -63,7 +63,7 @@ ${prompt} 1. If Data Connect is marked as \`\`, first run the \`firebase_init\` tool with \`{dataconnect: {}}\` arguments to initialize it. 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 +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.`, From 5ff2dc678b9f464c4ca429aa83433aa12ee2f90a Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 11:19:52 -0700 Subject: [PATCH 07/10] Update src/mcp/tools/dataconnect/compile.ts Co-authored-by: Joe Hanley --- src/mcp/tools/dataconnect/compile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index 511b0e6603a..62f936c705f 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -7,7 +7,7 @@ export const compile = tool( { name: "compile", description: - "Use this to compile Firebase Data Connect schema and/or operations and check for build errors.", + "Use this to compile Firebase Data Connect schema, operations, and/or connectors and check for build errors.", inputSchema: z.object({ error_filter: z .enum(["all", "schema", "operations"]) From ce98e24119587635ea8632a08ce8cf1baee13ece Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 11:20:44 -0700 Subject: [PATCH 08/10] pr feedback --- src/experiments.ts | 2 +- src/mcp/prompts/dataconnect/schema.ts | 1 - src/mcp/util/dataconnect/compile.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/experiments.ts b/src/experiments.ts index a1010f0e5b2..1084b5324bb 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -150,7 +150,7 @@ export const ALL_EXPERIMENTS = experiments({ mcpalpha: { shortDescription: "Opt-in to early MCP features before they're widely released.", default: false, - public: false, + public: true, }, apptesting: { shortDescription: "Adds experimental App Testing feature", diff --git a/src/mcp/prompts/dataconnect/schema.ts b/src/mcp/prompts/dataconnect/schema.ts index f5d5e51adb1..1267e64851f 100644 --- a/src/mcp/prompts/dataconnect/schema.ts +++ b/src/mcp/prompts/dataconnect/schema.ts @@ -1,4 +1,3 @@ -import { z } from "zod"; import { prompt } from "../../prompt"; import { loadAll } from "../../../dataconnect/loadAll"; import type { ServiceInfo } from "../../../dataconnect/types"; diff --git a/src/mcp/util/dataconnect/compile.ts b/src/mcp/util/dataconnect/compile.ts index c81165d35af..27aee94c2e4 100644 --- a/src/mcp/util/dataconnect/compile.ts +++ b/src/mcp/util/dataconnect/compile.ts @@ -1,5 +1,4 @@ import { prettify } from "../../../dataconnect/graphqlError"; -import { ServiceInfo } from "../../../dataconnect/types"; import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; export async function compileErrors( From b7775dfe62d0e858433eb949bd230caa9daa2c3d Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 15 Aug 2025 11:21:03 -0700 Subject: [PATCH 09/10] Update src/mcp/tools/dataconnect/compile.ts Co-authored-by: Joe Hanley --- src/mcp/tools/dataconnect/compile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index 62f936c705f..4230fdff5c8 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -10,7 +10,7 @@ export const compile = tool( "Use this to compile Firebase Data Connect schema, operations, and/or connectors and check for build errors.", inputSchema: z.object({ error_filter: z - .enum(["all", "schema", "operations"]) + .enum(["all", "schema", "connectors"]) .describe("filter errors to a specific type only. defaults to `all` if omitted.") .optional(), service_id: z From 6b06187b1143dde8acc45e1a5ea62a70fad31686 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Thu, 28 Aug 2025 13:39:49 -0700 Subject: [PATCH 10/10] duplicate loadAll --- src/dataconnect/loadAll.ts | 7 ------- src/mcp/prompts/dataconnect/schema.ts | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 src/dataconnect/loadAll.ts diff --git a/src/dataconnect/loadAll.ts b/src/dataconnect/loadAll.ts deleted file mode 100644 index 76460d1891c..00000000000 --- a/src/dataconnect/loadAll.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { load, readFirebaseJson } from "./load"; -import type { Config } from "../config"; - -export function loadAll(projectId: string, config: Config) { - const configs = readFirebaseJson(config); - return Promise.all(configs.map((c) => load(projectId, config, c.source))); -} diff --git a/src/mcp/prompts/dataconnect/schema.ts b/src/mcp/prompts/dataconnect/schema.ts index af43867faea..cbc1dfd9028 100644 --- a/src/mcp/prompts/dataconnect/schema.ts +++ b/src/mcp/prompts/dataconnect/schema.ts @@ -1,5 +1,5 @@ import { prompt } from "../../prompt"; -import { loadAll } from "../../../dataconnect/loadAll"; +import { loadAll } from "../../../dataconnect/load"; import type { ServiceInfo } from "../../../dataconnect/types"; import { BUILTIN_SDL, MAIN_INSTRUCTIONS } from "../../util/dataconnect/content"; import { compileErrors } from "../../util/dataconnect/compile";