Skip to content

Commit 8d60a76

Browse files
committed
poc/data/consolidated-find
1 parent c70cb89 commit 8d60a76

File tree

7 files changed

+214
-29
lines changed

7 files changed

+214
-29
lines changed

src/common/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum ErrorCodes {
22
NotConnectedToMongoDB = 1_000_000,
33
MisconfiguredConnectionString = 1_000_001,
44
ForbiddenCollscan = 1_000_002,
5+
InvalidArguments = 1_000_003,
56
}
67

78
export class MongoDBError extends Error {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4+
import { ToolArgs, OperationType } from "../../tool.js";
5+
import { EJSON } from "bson";
6+
import { SortDirection } from "mongodb";
7+
import { ErrorCodes, MongoDBError } from "../../../common/errors.js";
8+
import { ExplainTool } from "../metadata/explain.js";
9+
10+
export const FindToolArgs = {
11+
filter: z
12+
.record(z.string(), z.unknown())
13+
.optional()
14+
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
15+
projection: z
16+
.record(z.string(), z.unknown())
17+
.optional()
18+
.describe("The projection, matching the syntax of the projection argument of db.collection.find(). Only used when filter is provided."),
19+
limit: z.number().optional().default(10).describe("The maximum number of documents to return. Only used when filter is provided."),
20+
sort: z
21+
.record(z.string(), z.custom<SortDirection>())
22+
.optional()
23+
.describe("A document, describing the sort order, matching the syntax of the sort argument of cursor.sort(). Only used when filter is provided."),
24+
pipeline: z.array(z.record(z.string(), z.unknown())).optional().describe("An array of aggregation stages to execute, matching the syntax of the aggregation pipeline in db.collection.aggregate(). It's ignored in filter is provided."),
25+
explain: z.boolean().optional().default(false).describe("If true, returns the explain plan of the query or aggregation pipeline."),
26+
count: z.boolean().optional().default(false).describe("If true, returns the count of the documents matching the filter. Only used when filter is provided. For aggregation pipelines, use the $count stage in the pipeline instead."),
27+
};
28+
29+
export class MongoDbFindTool extends MongoDBToolBase {
30+
public name = "mongodb-find";
31+
protected description = `
32+
This tool retrieves documents, the explain plan or the count of documents from MongoDB collections based on an specific filter or an aggregation pipeline, excluding $out and $merge stages.
33+
`;
34+
35+
protected argsShape = {
36+
...DbOperationArgs,
37+
...FindToolArgs,
38+
};
39+
public operationType: OperationType = "read";
40+
41+
protected async execute({
42+
database,
43+
collection,
44+
filter,
45+
projection,
46+
limit,
47+
sort,
48+
pipeline,
49+
explain,
50+
count,
51+
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
52+
const provider = await this.ensureConnected();
53+
54+
if (filter) {
55+
if (count && !explain) {
56+
const count = await provider.count(database, collection, filter);
57+
58+
return {
59+
content: [
60+
{
61+
text: `Found ${count} documents in the collection "${collection}"`,
62+
type: "text",
63+
},
64+
],
65+
};
66+
} else if (count) {
67+
const result = await provider.runCommandWithCheck(database, {
68+
explain: {
69+
count: collection,
70+
query: filter,
71+
},
72+
verbosity: ExplainTool.defaultVerbosity,
73+
});
74+
75+
return {
76+
content: [
77+
{
78+
text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`count\` operation in "${database}.${collection}". This information can be used to understand how the query was executed and to optimize the query performance.`,
79+
type: "text",
80+
},
81+
{
82+
text: JSON.stringify(result),
83+
type: "text",
84+
},
85+
],
86+
};
87+
} else if (explain) {
88+
const result = await provider
89+
.find(database, collection, filter, { projection, limit, sort })
90+
.explain(ExplainTool.defaultVerbosity);
91+
92+
return {
93+
content: [
94+
{
95+
text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`find\` operation in "${database}.${collection}". This information can be used to understand how the query was executed and to optimize the query performance.`,
96+
type: "text",
97+
},
98+
{
99+
text: JSON.stringify(result),
100+
type: "text",
101+
},
102+
],
103+
};
104+
} else {
105+
const documents = await provider.find(database, collection, filter, { projection, limit, sort }).toArray();
106+
107+
const content: Array<{ text: string; type: "text" }> = [
108+
{
109+
text: `Found ${documents.length} documents in the collection "${collection}":`,
110+
type: "text",
111+
},
112+
...documents.map((doc) => {
113+
return {
114+
text: EJSON.stringify(doc),
115+
type: "text",
116+
} as { text: string; type: "text" };
117+
}),
118+
];
119+
120+
return { content };
121+
}
122+
} else if (pipeline) {
123+
if (explain) {
124+
const result = await provider
125+
.aggregate(
126+
database,
127+
collection,
128+
pipeline,
129+
{},
130+
{
131+
writeConcern: undefined,
132+
}
133+
)
134+
.explain(ExplainTool.defaultVerbosity);
135+
136+
return {
137+
content: [
138+
{
139+
text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`aggregate\` operation in "${database}.${collection}". This information can be used to understand how the query was executed and to optimize the query performance.`,
140+
type: "text",
141+
},
142+
{
143+
text: JSON.stringify(result),
144+
type: "text",
145+
},
146+
],
147+
};
148+
} else {
149+
const documents = await provider.aggregate(database, collection, pipeline).toArray();
150+
151+
const content: Array<{ text: string; type: "text" }> = [
152+
{
153+
text: `Found ${documents.length} documents in the collection "${collection}":`,
154+
type: "text",
155+
},
156+
...documents.map((doc) => {
157+
return {
158+
text: EJSON.stringify(doc),
159+
type: "text",
160+
} as { text: string; type: "text" };
161+
}),
162+
];
163+
164+
return {
165+
content,
166+
};
167+
}
168+
} else {
169+
const documents = await provider.find(database, collection, {}, { projection, limit, sort }).toArray();
170+
171+
const content: Array<{ text: string; type: "text" }> = [
172+
{
173+
text: `Found ${documents.length} documents in the collection "${collection}":`,
174+
type: "text",
175+
},
176+
...documents.map((doc) => {
177+
return {
178+
text: EJSON.stringify(doc),
179+
type: "text",
180+
} as { text: string; type: "text" };
181+
}),
182+
];
183+
184+
return { content };
185+
}
186+
}
187+
}

src/tools/mongodb/tools.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { DropCollectionTool } from "./delete/dropCollection.js";
1818
import { ExplainTool } from "./metadata/explain.js";
1919
import { CreateCollectionTool } from "./create/createCollection.js";
2020
import { LogsTool } from "./metadata/logs.js";
21+
import { MongoDbFindTool } from "./consolidated/mongodb-find.js";
2122

2223
export const MongoDbTools = [
2324
ConnectTool,
@@ -26,18 +27,19 @@ export const MongoDbTools = [
2627
CollectionIndexesTool,
2728
CreateIndexTool,
2829
CollectionSchemaTool,
29-
FindTool,
30+
//FindTool,
3031
InsertManyTool,
3132
DeleteManyTool,
3233
CollectionStorageSizeTool,
33-
CountTool,
34+
//CountTool,
3435
DbStatsTool,
35-
AggregateTool,
36+
//AggregateTool,
3637
UpdateManyTool,
3738
RenameCollectionTool,
3839
DropDatabaseTool,
3940
DropCollectionTool,
40-
ExplainTool,
41+
//ExplainTool,
4142
CreateCollectionTool,
4243
LogsTool,
44+
MongoDbFindTool,
4345
];

tests/accuracy/aggregate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describeAccuracyTests([
55
prompt: "Group all the movies in 'mflix.movies' namespace by 'release_year' and give me a count of them",
66
expectedToolCalls: [
77
{
8-
toolName: "aggregate",
8+
toolName: "mongodb-find",
99
parameters: {
1010
pipeline: { $group: { _id: "$release_year", count: { $sum: 1 } } },
1111
},

tests/accuracy/count.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ function callsCountToolWithEmptyQuery(prompt: string, database = "mflix", collec
66
prompt: prompt,
77
expectedToolCalls: [
88
{
9-
toolName: "count",
9+
toolName: "mongodb-find",
1010
parameters: {
1111
database,
1212
collection,
13+
count: true,
1314
},
1415
},
1516
],
@@ -20,17 +21,18 @@ function callsCountToolWithQuery(
2021
prompt: string,
2122
database = "mflix",
2223
collection = "movies",
23-
query: Record<string, unknown> = {}
24+
filter: Record<string, unknown> = {}
2425
): AccuracyTestConfig {
2526
return {
2627
prompt: prompt,
2728
expectedToolCalls: [
2829
{
29-
toolName: "count",
30+
toolName: "mongodb-find",
3031
parameters: {
3132
database,
3233
collection,
33-
query,
34+
filter,
35+
count: true,
3436
},
3537
},
3638
],

tests/accuracy/explain.test.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js";
22
import { AccuracyTestConfig } from "./sdk/describeAccuracyTests.js";
33

4-
function callsExplain(prompt: string, method: Record<string, unknown>): AccuracyTestConfig {
4+
function callsExplain(prompt: string, config: Record<string, unknown>): AccuracyTestConfig {
55
return {
66
prompt: prompt,
77
expectedToolCalls: [
@@ -10,7 +10,8 @@ function callsExplain(prompt: string, method: Record<string, unknown>): Accuracy
1010
parameters: {
1111
database: "mflix",
1212
collection: "movies",
13-
method: [method],
13+
...config,
14+
explain: true,
1415
},
1516
},
1617
],
@@ -19,30 +20,22 @@ function callsExplain(prompt: string, method: Record<string, unknown>): Accuracy
1920

2021
const callsExplainWithFind = (prompt: string) =>
2122
callsExplain(prompt, {
22-
name: "find",
23-
arguments: {
24-
filter: { release_year: 2020 },
25-
},
23+
filter: { release_year: 2020 },
2624
});
2725

2826
const callsExplainWithAggregate = (prompt: string) =>
2927
callsExplain(prompt, {
30-
name: "aggregate",
31-
arguments: {
32-
pipeline: [
28+
pipeline: [
3329
{
3430
$match: { release_year: 2020 },
3531
},
36-
],
37-
},
32+
],
3833
});
3934

4035
const callsExplainWithCount = (prompt: string) =>
4136
callsExplain(prompt, {
42-
name: "count",
43-
arguments: {
44-
query: { release_year: 2020 },
45-
},
37+
filter: { release_year: 2020 },
38+
count: true,
4639
});
4740

4841
/**

tests/accuracy/find.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function callsFindNoFilter(prompt: string, database = "mflix", collection = "mov
66
prompt: prompt,
77
expectedToolCalls: [
88
{
9-
toolName: "find",
9+
toolName: "mongodb-find",
1010
parameters: {
1111
database,
1212
collection,
@@ -21,7 +21,7 @@ function callsFindWithFilter(prompt: string, filter: Record<string, unknown>): A
2121
prompt: prompt,
2222
expectedToolCalls: [
2323
{
24-
toolName: "find",
24+
toolName: "mongodb-find",
2525
parameters: {
2626
database: "mflix",
2727
collection: "movies",
@@ -37,7 +37,7 @@ function callsFindWithProjection(prompt: string, projection: Record<string, numb
3737
prompt: prompt,
3838
expectedToolCalls: [
3939
{
40-
toolName: "find",
40+
toolName: "mongodb-find",
4141
parameters: {
4242
database: "mflix",
4343
collection: "movies",
@@ -57,7 +57,7 @@ function callsFindWithProjectionAndFilters(
5757
prompt: prompt,
5858
expectedToolCalls: [
5959
{
60-
toolName: "find",
60+
toolName: "mongodb-find",
6161
parameters: {
6262
database: "mflix",
6363
collection: "movies",
@@ -79,7 +79,7 @@ function callsFindWithFilterSortAndLimit(
7979
prompt: prompt,
8080
expectedToolCalls: [
8181
{
82-
toolName: "find",
82+
toolName: "mongodb-find",
8383
parameters: {
8484
database: "mflix",
8585
collection: "movies",

0 commit comments

Comments
 (0)