Skip to content

Commit ab1e3fa

Browse files
authored
chore: add tests for read operations (#102)
1 parent 673fca7 commit ab1e3fa

File tree

6 files changed

+413
-10
lines changed

6 files changed

+413
-10
lines changed

src/tools/mongodb/read/aggregate.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
5+
import { EJSON } from "bson";
56

67
export const AggregateArgs = {
78
pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
8-
limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
99
};
1010

1111
export class AggregateTool extends MongoDBToolBase {
@@ -27,12 +27,12 @@ export class AggregateTool extends MongoDBToolBase {
2727

2828
const content: Array<{ text: string; type: "text" }> = [
2929
{
30-
text: `Found ${documents.length} documents in the collection \`${collection}\`:`,
30+
text: `Found ${documents.length} documents in the collection "${collection}":`,
3131
type: "text",
3232
},
3333
...documents.map((doc) => {
3434
return {
35-
text: JSON.stringify(doc),
35+
text: EJSON.stringify(doc),
3636
type: "text",
3737
} as { text: string; type: "text" };
3838
}),

src/tools/mongodb/read/collectionIndexes.ts

+29-5
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,36 @@ export class CollectionIndexesTool extends MongoDBToolBase {
1313
const indexes = await provider.getIndexes(database, collection);
1414

1515
return {
16-
content: indexes.map((indexDefinition) => {
17-
return {
18-
text: `Field: ${indexDefinition.name}: ${JSON.stringify(indexDefinition.key)}`,
16+
content: [
17+
{
18+
text: `Found ${indexes.length} indexes in the collection "${collection}":`,
1919
type: "text",
20-
};
21-
}),
20+
},
21+
...(indexes.map((indexDefinition) => {
22+
return {
23+
text: `Name "${indexDefinition.name}", definition: ${JSON.stringify(indexDefinition.key)}`,
24+
type: "text",
25+
};
26+
}) as { text: string; type: "text" }[]),
27+
],
2228
};
2329
}
30+
31+
protected handleError(
32+
error: unknown,
33+
args: ToolArgs<typeof this.argsShape>
34+
): Promise<CallToolResult> | CallToolResult {
35+
if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
36+
return {
37+
content: [
38+
{
39+
text: `The indexes for "${args.database}.${args.collection}" cannot be determined because the collection does not exist.`,
40+
type: "text",
41+
},
42+
],
43+
};
44+
}
45+
46+
return super.handleError(error, args);
47+
}
2448
}

src/tools/mongodb/read/find.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55
import { SortDirection } from "mongodb";
6+
import { EJSON } from "bson";
67

78
export const FindArgs = {
89
filter: z
@@ -44,12 +45,12 @@ export class FindTool extends MongoDBToolBase {
4445

4546
const content: Array<{ text: string; type: "text" }> = [
4647
{
47-
text: `Found ${documents.length} documents in the collection \`${collection}\`:`,
48+
text: `Found ${documents.length} documents in the collection "${collection}":`,
4849
type: "text",
4950
},
5051
...documents.map((doc) => {
5152
return {
52-
text: JSON.stringify(doc),
53+
text: EJSON.stringify(doc),
5354
type: "text",
5455
} as { text: string; type: "text" };
5556
}),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
databaseCollectionParameters,
3+
validateToolMetadata,
4+
validateThrowsForInvalidArguments,
5+
getResponseElements,
6+
} from "../../../helpers.js";
7+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
8+
9+
describeWithMongoDB("aggregate tool", (integration) => {
10+
validateToolMetadata(integration, "aggregate", "Run an aggregation against a MongoDB collection", [
11+
...databaseCollectionParameters,
12+
{
13+
name: "pipeline",
14+
description: "An array of aggregation stages to execute",
15+
type: "array",
16+
required: true,
17+
},
18+
]);
19+
20+
validateThrowsForInvalidArguments(integration, "aggregate", [
21+
{},
22+
{ database: "test", collection: "foo" },
23+
{ database: test, pipeline: [] },
24+
{ database: "test", collection: "foo", pipeline: {} },
25+
{ database: "test", collection: "foo", pipeline: [], extra: "extra" },
26+
{ database: "test", collection: [], pipeline: [] },
27+
{ database: 123, collection: "foo", pipeline: [] },
28+
]);
29+
30+
it("can run aggragation on non-existent database", async () => {
31+
await integration.connectMcpClient();
32+
const response = await integration.mcpClient().callTool({
33+
name: "aggregate",
34+
arguments: { database: "non-existent", collection: "people", pipeline: [{ $match: { name: "Peter" } }] },
35+
});
36+
37+
const elements = getResponseElements(response.content);
38+
expect(elements).toHaveLength(1);
39+
expect(elements[0].text).toEqual('Found 0 documents in the collection "people":');
40+
});
41+
42+
it("can run aggragation on an empty collection", async () => {
43+
await integration.mongoClient().db(integration.randomDbName()).createCollection("people");
44+
45+
await integration.connectMcpClient();
46+
const response = await integration.mcpClient().callTool({
47+
name: "aggregate",
48+
arguments: {
49+
database: integration.randomDbName(),
50+
collection: "people",
51+
pipeline: [{ $match: { name: "Peter" } }],
52+
},
53+
});
54+
55+
const elements = getResponseElements(response.content);
56+
expect(elements).toHaveLength(1);
57+
expect(elements[0].text).toEqual('Found 0 documents in the collection "people":');
58+
});
59+
60+
it("can run aggragation on an existing collection", async () => {
61+
const mongoClient = integration.mongoClient();
62+
await mongoClient
63+
.db(integration.randomDbName())
64+
.collection("people")
65+
.insertMany([
66+
{ name: "Peter", age: 5 },
67+
{ name: "Laura", age: 10 },
68+
{ name: "Søren", age: 15 },
69+
]);
70+
71+
await integration.connectMcpClient();
72+
const response = await integration.mcpClient().callTool({
73+
name: "aggregate",
74+
arguments: {
75+
database: integration.randomDbName(),
76+
collection: "people",
77+
pipeline: [{ $match: { age: { $gt: 8 } } }, { $sort: { name: -1 } }],
78+
},
79+
});
80+
81+
const elements = getResponseElements(response.content);
82+
expect(elements).toHaveLength(3);
83+
expect(elements[0].text).toEqual('Found 2 documents in the collection "people":');
84+
expect(JSON.parse(elements[1].text)).toEqual({ _id: expect.any(Object), name: "Søren", age: 15 });
85+
expect(JSON.parse(elements[2].text)).toEqual({ _id: expect.any(Object), name: "Laura", age: 10 });
86+
});
87+
88+
validateAutoConnectBehavior(integration, "aggregate", () => {
89+
return {
90+
args: {
91+
database: integration.randomDbName(),
92+
collection: "coll1",
93+
pipeline: [{ $match: { name: "Liva" } }],
94+
},
95+
expectedResponse: 'Found 0 documents in the collection "coll1"',
96+
};
97+
});
98+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { IndexDirection } from "mongodb";
2+
import {
3+
databaseCollectionParameters,
4+
validateToolMetadata,
5+
validateThrowsForInvalidArguments,
6+
getResponseElements,
7+
databaseCollectionInvalidArgs,
8+
} from "../../../helpers.js";
9+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
10+
11+
describeWithMongoDB("collectionIndexes tool", (integration) => {
12+
validateToolMetadata(
13+
integration,
14+
"collection-indexes",
15+
"Describe the indexes for a collection",
16+
databaseCollectionParameters
17+
);
18+
19+
validateThrowsForInvalidArguments(integration, "collection-indexes", databaseCollectionInvalidArgs);
20+
21+
it("can inspect indexes on non-existent database", async () => {
22+
await integration.connectMcpClient();
23+
const response = await integration.mcpClient().callTool({
24+
name: "collection-indexes",
25+
arguments: { database: "non-existent", collection: "people" },
26+
});
27+
28+
const elements = getResponseElements(response.content);
29+
expect(elements).toHaveLength(1);
30+
expect(elements[0].text).toEqual(
31+
'The indexes for "non-existent.people" cannot be determined because the collection does not exist.'
32+
);
33+
});
34+
35+
it("returns the _id index for a new collection", async () => {
36+
await integration.mongoClient().db(integration.randomDbName()).createCollection("people");
37+
38+
await integration.connectMcpClient();
39+
const response = await integration.mcpClient().callTool({
40+
name: "collection-indexes",
41+
arguments: {
42+
database: integration.randomDbName(),
43+
collection: "people",
44+
},
45+
});
46+
47+
const elements = getResponseElements(response.content);
48+
expect(elements).toHaveLength(2);
49+
expect(elements[0].text).toEqual('Found 1 indexes in the collection "people":');
50+
expect(elements[1].text).toEqual('Name "_id_", definition: {"_id":1}');
51+
});
52+
53+
it("returns all indexes for a collection", async () => {
54+
await integration.mongoClient().db(integration.randomDbName()).createCollection("people");
55+
56+
const indexTypes: IndexDirection[] = [-1, 1, "2d", "2dsphere", "text", "hashed"];
57+
for (const indexType of indexTypes) {
58+
await integration
59+
.mongoClient()
60+
.db(integration.randomDbName())
61+
.collection("people")
62+
.createIndex({ [`prop_${indexType}`]: indexType });
63+
}
64+
65+
await integration.connectMcpClient();
66+
const response = await integration.mcpClient().callTool({
67+
name: "collection-indexes",
68+
arguments: {
69+
database: integration.randomDbName(),
70+
collection: "people",
71+
},
72+
});
73+
74+
const elements = getResponseElements(response.content);
75+
expect(elements).toHaveLength(indexTypes.length + 2);
76+
expect(elements[0].text).toEqual(`Found ${indexTypes.length + 1} indexes in the collection "people":`);
77+
expect(elements[1].text).toEqual('Name "_id_", definition: {"_id":1}');
78+
79+
for (const indexType of indexTypes) {
80+
const index = elements.find((element) => element.text.includes(`prop_${indexType}`));
81+
expect(index).toBeDefined();
82+
83+
let expectedDefinition = JSON.stringify({ [`prop_${indexType}`]: indexType });
84+
if (indexType === "text") {
85+
expectedDefinition = '{"_fts":"text"';
86+
}
87+
88+
expect(index!.text).toContain(`definition: ${expectedDefinition}`);
89+
}
90+
});
91+
92+
validateAutoConnectBehavior(integration, "collection-indexes", () => {
93+
return {
94+
args: { database: integration.randomDbName(), collection: "coll1" },
95+
expectedResponse: `The indexes for "${integration.randomDbName()}.coll1" cannot be determined because the collection does not exist.`,
96+
};
97+
});
98+
});

0 commit comments

Comments
 (0)