Skip to content

(mcp) Support more filter operators and multiple order bys #8717

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
101 changes: 13 additions & 88 deletions src/mcp/tools/firestore/query_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand All @@ -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.")
Expand All @@ -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)";
Expand All @@ -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;

Expand All @@ -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);
},
);
68 changes: 68 additions & 0 deletions src/mcp/tools/firestore/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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."),
});