Skip to content

Commit fe6d814

Browse files
gagiknirinchev
andauthored
feat: add explain command (#57)
Co-authored-by: Nikola Irinchev <[email protected]>
1 parent 9286078 commit fe6d814

File tree

5 files changed

+132
-32
lines changed

5 files changed

+132
-32
lines changed

src/tools/mongodb/metadata/explain.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
3+
import { ToolArgs } from "../../tool.js";
4+
import { z } from "zod";
5+
import { ExplainVerbosity, Document } from "mongodb";
6+
import { AggregateArgs } from "../read/aggregate.js";
7+
import { FindArgs } from "../read/find.js";
8+
import { CountArgs } from "../read/count.js";
9+
10+
export class ExplainTool extends MongoDBToolBase {
11+
protected name = "explain";
12+
protected description =
13+
"Returns statistics describing the execution of the winning plan chosen by the query optimizer for the evaluated method";
14+
15+
protected argsShape = {
16+
...DbOperationArgs,
17+
method: z
18+
.array(
19+
z.union([
20+
z.object({
21+
name: z.literal("aggregate"),
22+
arguments: z.object(AggregateArgs),
23+
}),
24+
z.object({
25+
name: z.literal("find"),
26+
arguments: z.object(FindArgs),
27+
}),
28+
z.object({
29+
name: z.literal("count"),
30+
arguments: z.object(CountArgs),
31+
}),
32+
])
33+
)
34+
.describe("The method and its arguments to run"),
35+
};
36+
37+
protected operationType: DbOperationType = "metadata";
38+
39+
static readonly defaultVerbosity = ExplainVerbosity.queryPlanner;
40+
41+
protected async execute({
42+
database,
43+
collection,
44+
method: methods,
45+
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
46+
const provider = await this.ensureConnected();
47+
const method = methods[0];
48+
49+
if (!method) {
50+
throw new Error("No method provided");
51+
}
52+
53+
let result: Document;
54+
switch (method.name) {
55+
case "aggregate": {
56+
const { pipeline } = method.arguments;
57+
result = await provider.aggregate(database, collection, pipeline).explain(ExplainTool.defaultVerbosity);
58+
break;
59+
}
60+
case "find": {
61+
const { filter, ...rest } = method.arguments;
62+
result = await provider
63+
.find(database, collection, filter as Document, { ...rest })
64+
.explain(ExplainTool.defaultVerbosity);
65+
break;
66+
}
67+
case "count": {
68+
const { query } = method.arguments;
69+
// This helper doesn't have explain() command but does have the argument explain
70+
result = (await provider.count(database, collection, query, {
71+
explain: ExplainTool.defaultVerbosity,
72+
})) as unknown as Document;
73+
break;
74+
}
75+
}
76+
77+
return {
78+
content: [
79+
{
80+
text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in \`${database}.${collection}\`. This information can be used to understand how the query was executed and to optimize the query performance.`,
81+
type: "text",
82+
},
83+
{
84+
text: JSON.stringify(result),
85+
type: "text",
86+
},
87+
],
88+
};
89+
}
90+
}

src/tools/mongodb/read/aggregate.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
3+
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs } from "../../tool.js";
55

6+
export const AggregateArgs = {
7+
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"),
9+
};
10+
611
export class AggregateTool extends MongoDBToolBase {
712
protected name = "aggregate";
813
protected description = "Run an aggregation against a MongoDB collection";
914
protected argsShape = {
10-
collection: z.string().describe("Collection name"),
11-
database: z.string().describe("Database name"),
12-
pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
13-
limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
15+
...DbOperationArgs,
16+
...AggregateArgs,
1417
};
1518
protected operationType: DbOperationType = "read";
1619

src/tools/mongodb/read/count.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbToo
33
import { ToolArgs } from "../../tool.js";
44
import { z } from "zod";
55

6+
export const CountArgs = {
7+
query: z
8+
.object({})
9+
.passthrough()
10+
.optional()
11+
.describe(
12+
"The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()"
13+
),
14+
};
15+
616
export class CountTool extends MongoDBToolBase {
717
protected name = "count";
818
protected description = "Gets the number of documents in a MongoDB collection";
919
protected argsShape = {
1020
...DbOperationArgs,
11-
query: z
12-
.object({})
13-
.passthrough()
14-
.optional()
15-
.describe(
16-
"The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()"
17-
),
21+
...CountArgs,
1822
};
1923

2024
protected operationType: DbOperationType = "metadata";

src/tools/mongodb/read/find.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
3+
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs } from "../../tool.js";
55
import { SortDirection } from "mongodb";
66

7+
export const FindArgs = {
8+
filter: z
9+
.object({})
10+
.passthrough()
11+
.optional()
12+
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
13+
projection: z
14+
.object({})
15+
.passthrough()
16+
.optional()
17+
.describe("The projection, matching the syntax of the projection argument of db.collection.find()"),
18+
limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
19+
sort: z
20+
.record(z.string(), z.custom<SortDirection>())
21+
.optional()
22+
.describe("A document, describing the sort order, matching the syntax of the sort argument of cursor.sort()"),
23+
};
24+
725
export class FindTool extends MongoDBToolBase {
826
protected name = "find";
927
protected description = "Run a find query against a MongoDB collection";
1028
protected argsShape = {
11-
collection: z.string().describe("Collection name"),
12-
database: z.string().describe("Database name"),
13-
filter: z
14-
.object({})
15-
.passthrough()
16-
.optional()
17-
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
18-
projection: z
19-
.object({})
20-
.passthrough()
21-
.optional()
22-
.describe("The projection, matching the syntax of the projection argument of db.collection.find()"),
23-
limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
24-
sort: z
25-
.record(z.string(), z.custom<SortDirection>())
26-
.optional()
27-
.describe(
28-
"A document, describing the sort order, matching the syntax of the sort argument of cursor.sort()"
29-
),
29+
...DbOperationArgs,
30+
...FindArgs,
3031
};
3132
protected operationType: DbOperationType = "read";
3233

src/tools/mongodb/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { UpdateManyTool } from "./update/updateMany.js";
1818
import { RenameCollectionTool } from "./update/renameCollection.js";
1919
import { DropDatabaseTool } from "./delete/dropDatabase.js";
2020
import { DropCollectionTool } from "./delete/dropCollection.js";
21+
import { ExplainTool } from "./metadata/explain.js";
2122

2223
export const MongoDbTools = [
2324
ConnectTool,
@@ -40,4 +41,5 @@ export const MongoDbTools = [
4041
RenameCollectionTool,
4142
DropDatabaseTool,
4243
DropCollectionTool,
44+
ExplainTool,
4345
];

0 commit comments

Comments
 (0)