find.test.ts•18 kB
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Document, Collection } from "mongodb";
import {
getResponseContent,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
expectDefined,
defaultTestConfig,
} from "../../../helpers.js";
import * as constants from "../../../../../src/helpers/constants.js";
import { describeWithMongoDB, getDocsFromUntrustedContent, validateAutoConnectBehavior } from "../mongodbHelpers.js";
export async function freshInsertDocuments({
collection,
count,
documentMapper = (index): Document => ({ value: index }),
}: {
collection: Collection<Document>;
count: number;
documentMapper?: (index: number) => Document;
}): Promise<void> {
await collection.drop();
const documents = Array.from({ length: count }).map((_, idx) => documentMapper(idx));
await collection.insertMany(documents);
}
describeWithMongoDB("find tool with default configuration", (integration) => {
validateToolMetadata(integration, "find", "Run a find query against a MongoDB collection", [
...databaseCollectionParameters,
{
name: "filter",
description: "The query filter, matching the syntax of the query argument of db.collection.find()",
type: "object",
required: false,
},
{
name: "projection",
description: "The projection, matching the syntax of the projection argument of db.collection.find()",
type: "object",
required: false,
},
{
name: "limit",
description: "The maximum number of documents to return",
type: "number",
required: false,
},
{
name: "sort",
description:
"A document, describing the sort order, matching the syntax of the sort argument of cursor.sort(). The keys of the object are the fields to sort on, while the values are the sort directions (1 for ascending, -1 for descending).",
type: "object",
required: false,
},
{
name: "responseBytesLimit",
description: `The maximum number of bytes to return in the response. This value is capped by the server's configured maxBytesPerQuery and cannot be exceeded. Note to LLM: If the entire query result is required, use the "export" tool instead of increasing this limit.`,
type: "number",
required: false,
},
]);
validateThrowsForInvalidArguments(integration, "find", [
{},
{ database: 123, collection: "bar" },
{ database: "test", collection: [] },
{ database: "test", collection: "bar", filter: "{ $gt: { foo: 5 } }" },
{ database: "test", collection: "bar", projection: "name" },
{ database: "test", collection: "bar", limit: "10" },
{ database: "test", collection: "bar", sort: [], limit: 10 },
]);
it("returns 0 when database doesn't exist", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: { database: "non-existent", collection: "foos" },
});
const content = getResponseContent(response.content);
expect(content).toEqual('Query on collection "foos" resulted in 0 documents. Returning 0 documents.');
});
it("returns 0 when collection doesn't exist", async () => {
await integration.connectMcpClient();
const mongoClient = integration.mongoClient();
await mongoClient.db(integration.randomDbName()).collection("bar").insertOne({});
const response = await integration.mcpClient().callTool({
name: "find",
arguments: { database: integration.randomDbName(), collection: "non-existent" },
});
const content = getResponseContent(response.content);
expect(content).toEqual('Query on collection "non-existent" resulted in 0 documents. Returning 0 documents.');
});
describe("with existing database", () => {
beforeEach(async () => {
await freshInsertDocuments({
collection: integration.mongoClient().db(integration.randomDbName()).collection("foo"),
count: 10,
});
});
const testCases: {
name: string;
filter?: unknown;
limit?: number;
projection?: unknown;
sort?: unknown;
expected: unknown[];
}[] = [
{
name: "returns all documents when no filter is provided",
expected: Array(10)
.fill(0)
.map((_, index) => ({ _id: expect.any(Object) as unknown, value: index })),
},
{
name: "returns documents matching the filter",
filter: { value: { $gt: 5 } },
expected: Array(4)
.fill(0)
.map((_, index) => ({ _id: expect.any(Object) as unknown, value: index + 6 })),
},
{
name: "returns documents matching the filter with projection",
filter: { value: { $gt: 5 } },
projection: { value: 1, _id: 0 },
expected: Array(4)
.fill(0)
.map((_, index) => ({ value: index + 6 })),
},
{
name: "returns documents matching the filter with limit",
filter: { value: { $gt: 5 } },
limit: 2,
expected: [
{ _id: expect.any(Object) as unknown, value: 6 },
{ _id: expect.any(Object) as unknown, value: 7 },
],
},
{
name: "returns documents matching the filter with sort",
filter: {},
sort: { value: -1 },
expected: Array(10)
.fill(0)
.map((_, index) => ({ _id: expect.any(Object) as unknown, value: index }))
.reverse(),
},
];
for (const { name, filter, limit, projection, sort, expected } of testCases) {
it(name, async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter,
limit,
projection,
sort,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in ${expected.length} documents.`);
const docs = getDocsFromUntrustedContent(content);
for (let i = 0; i < expected.length; i++) {
expect(docs[i]).toEqual(expected[i]);
}
});
}
it("returns all documents when no filter is provided", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: { database: integration.randomDbName(), collection: "foo" },
});
const content = getResponseContent(response);
expect(content).toContain('Query on collection "foo" resulted in 10 documents.');
const docs = getDocsFromUntrustedContent(content);
expect(docs.length).toEqual(10);
for (let i = 0; i < 10; i++) {
expect((docs[i] as { value: number }).value).toEqual(i);
}
});
it("can find objects by $oid", async () => {
await integration.connectMcpClient();
const fooObject = await integration
.mongoClient()
.db(integration.randomDbName())
.collection("foo")
.findOne();
expectDefined(fooObject);
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: { _id: { $oid: fooObject._id } },
},
});
const content = getResponseContent(response);
expect(content).toContain('Query on collection "foo" resulted in 1 documents.');
const docs = getDocsFromUntrustedContent(content);
expect(docs.length).toEqual(1);
expect((docs[0] as { value: number }).value).toEqual(fooObject.value);
});
it("can find objects by date", async () => {
await integration.connectMcpClient();
await integration
.mongoClient()
.db(integration.randomDbName())
.collection("foo_with_dates")
.insertMany([
{ date: new Date("2025-05-10"), idx: 0 },
{ date: new Date("2025-05-11"), idx: 1 },
]);
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo_with_dates",
filter: { date: { $gt: { $date: "2025-05-10" } } }, // only 2025-05-11 will match
},
});
const content = getResponseContent(response);
expect(content).toContain(
'Query on collection "foo_with_dates" resulted in 1 documents. Returning 1 documents.'
);
const docs = getDocsFromUntrustedContent<{ date: Date }>(content);
expect(docs.length).toEqual(1);
expect(docs[0]?.date.toISOString()).toContain("2025-05-11");
});
});
validateAutoConnectBehavior(integration, "find", () => {
return {
args: { database: integration.randomDbName(), collection: "coll1" },
expectedResponse: 'Query on collection "coll1" resulted in 0 documents.',
};
});
describe("when counting documents exceed the configured count maxTimeMS", () => {
beforeEach(async () => {
await freshInsertDocuments({
collection: integration.mongoClient().db(integration.randomDbName()).collection("foo"),
count: 10,
});
});
afterEach(() => {
vi.resetAllMocks();
});
it("should abort count operation and respond with indeterminable count", async () => {
vi.spyOn(constants, "QUERY_COUNT_MAX_TIME_MS_CAP", "get").mockReturnValue(0.1);
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: { database: integration.randomDbName(), collection: "foo" },
});
const content = getResponseContent(response);
expect(content).toContain('Query on collection "foo" resulted in indeterminable number of documents.');
const docs = getDocsFromUntrustedContent(content);
expect(docs.length).toEqual(10);
});
});
});
describeWithMongoDB(
"find tool with configured max documents per query",
(integration) => {
beforeEach(async () => {
await freshInsertDocuments({
collection: integration.mongoClient().db(integration.randomDbName()).collection("foo"),
count: 1000,
});
});
afterEach(() => {
vi.resetAllMocks();
});
it("should return documents limited to the provided limit when provided limit < configured limit", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: {},
limit: 8,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in 8 documents.`);
expect(content).toContain(`Returning 8 documents.`);
});
it("should return documents limited to the configured max limit when provided limit > configured limit", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: {},
limit: 10000,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in 1000 documents.`);
expect(content).toContain(
`Returning 10 documents while respecting the applied limits of server's configured - maxDocumentsPerQuery.`
);
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, maxDocumentsPerQuery: 10 }),
}
);
describeWithMongoDB(
"find tool with configured max bytes per query",
(integration) => {
beforeEach(async () => {
await freshInsertDocuments({
collection: integration.mongoClient().db(integration.randomDbName()).collection("foo"),
count: 1000,
});
});
it("should return only the documents that could fit in configured maxBytesPerQuery limit", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: {},
limit: 1000,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in 1000 documents.`);
expect(content).toContain(
`Returning 3 documents while respecting the applied limits of server's configured - maxDocumentsPerQuery, server's configured - maxBytesPerQuery`
);
});
it("should return only the documents that could fit in provided responseBytesLimit", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: {},
limit: 1000,
responseBytesLimit: 50,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in 1000 documents.`);
expect(content).toContain(
`Returning 1 documents while respecting the applied limits of server's configured - maxDocumentsPerQuery, tool's parameter - responseBytesLimit.`
);
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, maxBytesPerQuery: 100 }),
}
);
describeWithMongoDB(
"find tool with disabled max limit and max bytes per query",
(integration) => {
beforeEach(async () => {
await freshInsertDocuments({
collection: integration.mongoClient().db(integration.randomDbName()).collection("foo"),
count: 1000,
});
});
it("should return documents limited to the provided limit", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: {},
limit: 8,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in 8 documents.`);
expect(content).toContain(`Returning 8 documents.`);
});
it("should return documents limited to the responseBytesLimit", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "find",
arguments: {
database: integration.randomDbName(),
collection: "foo",
filter: {},
limit: 1000,
responseBytesLimit: 50,
},
});
const content = getResponseContent(response);
expect(content).toContain(`Query on collection "foo" resulted in 1000 documents.`);
expect(content).toContain(
`Returning 1 documents while respecting the applied limits of tool's parameter - responseBytesLimit.`
);
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, maxDocumentsPerQuery: -1, maxBytesPerQuery: -1 }),
}
);