From f2b45b4da9067bb8a920a5cfcde50317ca9f762e Mon Sep 17 00:00:00 2001 From: le-michael Date: Fri, 6 Jun 2025 07:48:05 -0700 Subject: [PATCH 1/2] (mcp) Support more filter functions and multiple order bys --- src/mcp/tools/firestore/query_collection.ts | 101 +++----------------- src/mcp/tools/firestore/schema.ts | 66 +++++++++++++ 2 files changed, 79 insertions(+), 88 deletions(-) create mode 100644 src/mcp/tools/firestore/schema.ts diff --git a/src/mcp/tools/firestore/query_collection.ts b/src/mcp/tools/firestore/query_collection.ts index e8e9fafa746..84cfe0d0f04 100644 --- a/src/mcp/tools/firestore/query_collection.ts +++ b/src/mcp/tools/firestore/query_collection.ts @@ -2,8 +2,9 @@ import { z } from "zod"; import { tool } from "../../tool.js"; import { mcpError, toContent } from "../../util.js"; import { queryCollection, StructuredQuery } from "../../../gcp/firestore.js"; -import { convertInputToValue, firestoreDocumentToJson } from "./converter.js"; +import { firestoreDocumentToJson } from "./converter.js"; import { Emulators } from "../../../emulator/types.js"; +import { CompositeFilter, Order } from "./schema.js"; export const query_collection = tool( { @@ -20,56 +21,12 @@ export const query_collection = tool( .describe( "A collection path (e.g. `collectionName/` or `parentCollection/parentDocument/collectionName`)", ), - filters: z - .object({ - compare_value: z - .object({ - string_value: z.string().optional().describe("The string value to compare against."), - boolean_value: z - .string() - .optional() - .describe("The boolean value to compare against."), - string_array_value: z - .array(z.string()) - .optional() - .describe("The string value to compare against."), - integer_value: z - .number() - .optional() - .describe("The integer value to compare against."), - double_value: z.number().optional().describe("The double value to compare against."), - }) - .describe("One and only one value may be specified per filters object."), - field: z.string().describe("the field searching against"), - op: z - .enum([ - "OPERATOR_UNSPECIFIED", - "LESS_THAN", - "LESS_THAN_OR_EQUAL", - "GREATER_THAN", - "GREATER_THAN_OR_EQUAL", - "EQUAL", - "NOT_EQUAL", - "ARRAY_CONTAINS", - "ARRAY_CONTAINS_ANY", - "IN", - "NOT_IN", - ]) - .describe("the equality evaluator to use"), - }) - .array() - .describe("the multiple filters to use in querying against the existing collection."), - order: z - .object({ - orderBy: z.string().describe("the field to order by"), - orderByDirection: z - .enum(["ASCENDING", "DESCENDING"]) - .describe("the direction to order values"), - }) + filter: CompositeFilter.optional().describe( + "Optional filters to apply to the Firestore query", + ), + orderBy: Order.array() .optional() - .describe( - "Specifies the field and direction to order the results. If not provided, the order is undefined.", - ), + .describe("Optional ordering to apply to the Firestore query."), limit: z .number() .describe("The maximum amount of records to return. Default is 10.") @@ -86,7 +43,7 @@ export const query_collection = tool( }, }, async ( - { collection_path, filters, order, limit, database, use_emulator }, + { collection_path, filter, orderBy, limit, database, use_emulator }, { projectId, host }, ) => { // database ??= "(default)"; @@ -97,41 +54,13 @@ export const query_collection = tool( const structuredQuery: StructuredQuery = { from: [{ collectionId: collection_path, allDescendants: false }], }; - if (filters) { + if (filter) { structuredQuery.where = { - compositeFilter: { - op: "AND", - filters: filters.map((f) => { - if ( - f.compare_value.boolean_value && - f.compare_value.double_value && - f.compare_value.integer_value && - f.compare_value.string_array_value && - f.compare_value.string_value - ) { - throw mcpError("One and only one value may be specified per filters object."); - } - const out = Object.entries(f.compare_value).filter(([, value]) => { - return value !== null && value !== undefined; - }); - return { - fieldFilter: { - field: { fieldPath: f.field }, - op: f.op, - value: convertInputToValue(out[0][1]), - }, - }; - }), - }, + compositeFilter: filter, }; } - if (order) { - structuredQuery.orderBy = [ - { - field: { fieldPath: order.orderBy }, - direction: order.orderByDirection, - }, - ]; + if (orderBy) { + structuredQuery.orderBy = orderBy; } structuredQuery.limit = limit ? limit : 10; @@ -141,11 +70,7 @@ export const query_collection = tool( } const { documents } = await queryCollection(projectId, structuredQuery, database, emulatorUrl); - const docs = documents.map(firestoreDocumentToJson); - - const docsContent = toContent(docs); - - return docsContent; + return toContent(docs); }, ); diff --git a/src/mcp/tools/firestore/schema.ts b/src/mcp/tools/firestore/schema.ts new file mode 100644 index 00000000000..f3b85148275 --- /dev/null +++ b/src/mcp/tools/firestore/schema.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; + +export const FieldReference = z.object({ + fieldPath: z + .string() + .describe("A reference to a field in a document. e.g. field, field.nested_field"), +}); + +export const Value = z + .union([ + z.object({ nullValue: z.null() }), + z.object({ booleanValue: z.boolean() }), + z.object({ integerValue: z.string().describe("A 64 bit int") }), + z.object({ doubleValue: z.number() }), + z.object({ + timestampValue: z.string().describe( + `Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. + Offsets other than "Z" are also accepted. + Examples: "2014-10-02T15:01:23Z", "2014-10-02T15:01:23.045123456Z" or "2014-10-02T15:01:23+05:30".`, + ), + }), + z.object({ stringValue: z.string() }), + z.object({ bytesValue: z.string().describe("A base64-encoded string.") }), + ]) + .describe("A firestore value. Only one value field can be set per value object."); + +// Recursive types are not supported so we define the array value separately. +export const ArrayValue = z.object({ arrayValue: z.object({ values: Value.array() }) }); + +export const UnaryFilter = z.object({ + op: z.enum(["IS_NAN", "IS_NULL", "IS_NOT_NAN", "IS_NOT_NULL"]), + field: FieldReference, +}); + +export const FieldFilter = z.object({ + field: FieldReference, + op: z.enum([ + "LESS_THAN", + "LESS_THAN_OR_EQUAL", + "GREATER_THAN", + "GREATER_THAN_OR_EQUAL", + "EQUAL", + "NOT_EQUAL", + "ARRAY_CONTAINS", + "IN", + "ARRAY_CONTAINS_ANY", + "NOT_IN", + ]), + value: z.union([Value, ArrayValue]), +}); + +export const Filter = z.object({ + unaryFilter: UnaryFilter.optional(), + fieldFilter: FieldFilter.optional(), +}).describe("Only one filter field can be set per filter object."); + +// Recursive types are not supported so we define the composite filter separately. +export const CompositeFilter = z.object({ + op: z.enum(["AND", "OR"]), + filters: Filter.array(), +}); + +export const Order = z.object({ + field: FieldReference.describe("The field to order by."), + direction: z.enum(["ASCENDING", "DESCENDING"]).describe("The direction to order by."), +}); From 448f561b7bfabfeacb4af535191efadcd8ba614a Mon Sep 17 00:00:00 2001 From: le-michael Date: Fri, 6 Jun 2025 08:10:52 -0700 Subject: [PATCH 2/2] Fix lint errors --- src/mcp/tools/firestore/schema.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mcp/tools/firestore/schema.ts b/src/mcp/tools/firestore/schema.ts index f3b85148275..91e1a1e8b7d 100644 --- a/src/mcp/tools/firestore/schema.ts +++ b/src/mcp/tools/firestore/schema.ts @@ -49,10 +49,12 @@ export const FieldFilter = z.object({ value: z.union([Value, ArrayValue]), }); -export const Filter = z.object({ - unaryFilter: UnaryFilter.optional(), - fieldFilter: FieldFilter.optional(), -}).describe("Only one filter field can be set per filter object."); +export const Filter = z + .object({ + unaryFilter: UnaryFilter.optional(), + fieldFilter: FieldFilter.optional(), + }) + .describe("Only one filter field can be set per filter object."); // Recursive types are not supported so we define the composite filter separately. export const CompositeFilter = z.object({