Skip to content

Commit d6b7db8

Browse files
committed
Merge branch 'atlas-list-performance-advisor-tool' into atlas-list-performance-advisor-base-tool
2 parents 4645d07 + c10955a commit d6b7db8

File tree

17 files changed

+214
-159
lines changed

17 files changed

+214
-159
lines changed

.github/copilot-instructions.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Project Overview
2+
3+
This project is a server implementing the MCP (Model Context Protocol) that allows users to interact with their MongoDB clusters
4+
and MongoDB Atlas accounts. It is built using TypeScript, Node.js and the official Anthropic
5+
@modelcontextprotocol/sdk SDK.
6+
7+
## Folder Structure
8+
9+
- `/src`: Contains the source code of the MCP Server.
10+
- `/src/tools`: Contains the implementation of MCP tools.
11+
- `/src/tools/atlas/`: Contains the implementation of MCP tools that are specific to MongoDB Atlas.
12+
- `/src/tools/mongodb/`: Contains the implementation of MCP tools that are specific to MongoDB clusters.
13+
- `/src/resources`: Contains the implementation of MCP Resources.
14+
- `/tests`: Contains the test code for the MCP Server.
15+
- `/tests/accuracy`: Contains the test code for the accuracy tests, that use different models to ensure that tools have reliable descriptions.
16+
- `/tests/integration`: Contains tests that start the MCP Server and interact with it to ensure that functionality is correct.
17+
- `/tests/unit`: Contains simple unit tests to cover specific functionality of the MCP Server.
18+
19+
## Libraries and Frameworks
20+
21+
- Zod for message and schema validation.
22+
- Express for the HTTP Transport implementation.
23+
- mongosh NodeDriverServiceProvider for connecting to MongoDB.
24+
- vitest for testing.
25+
- @modelcontextprotocol/sdk for the protocol implementation.
26+
27+
## Coding Standards
28+
29+
- For declarations, use types. For usage, rely on type inference unless it is not clear enough.
30+
- Always follow the eslint and prettier rule formats specified in `.eslint.config.js` and `.prettierrc.json`.
31+
- Use classes for stateful components and functions for stateless pure logic.
32+
- Use dependency injection to provide dependencies between components.
33+
- Avoid using global variables as much as possible.
34+
- New functionality MUST be under test.
35+
- Tools MUST HAVE integration tests.
36+
- Tools MUST HAVE unit tests.
37+
- Tools MAY HAVE accuracy tests.
38+
39+
## Architectural Guidelines and Best Practices
40+
41+
Every agent connected to the MCP Server has a Session object attached to it. The Session is the main entrypoint for
42+
dependencies to other components. Any component that MUST be used by either a tool or a resource MUST be provided
43+
through the Session.
44+
45+
### Guidelines for All Tools
46+
47+
- The name of the tool should describe an action: `create-collection`, `insert-many`.
48+
- The description MUST be a simple and accurate prompt that defines what the tool does in an unambiguous way.
49+
- All tools MUST provide a Zod schema that clearly specifies the API of the tool.
50+
- The Operation type MUST be clear:
51+
- `metadata`: Reads metadata for an entity (for example, a cluster). Example: CollectionSchema.
52+
- `read`: Reads information from a cluster or Atlas.
53+
- `create`: Creates resources, like a collection or a cluster.
54+
- `delete`: Deletes resources or documents, like collections, documents or clusters.
55+
- `update`: Modifies resources or documents, like collections, documents or clusters.
56+
- `connects`: Connects to a MongoDB cluster.
57+
- If a new tool is added, or the tool description is modified, the accuracy tests MUST be updated too.
58+
59+
### Guidelines for MongoDB Tools
60+
61+
- The tool category MUST be `mongodb`.
62+
- They MUST call `this.ensureConnected()` before attempting to query MongoDB.
63+
- They MUST return content sanitized using `formatUntrustedData`.
64+
- Documents should be serialized with `EJSON.stringify`.
65+
- Ensure there are proper timeout mechanisms to avoid long-running queries that can affect the server.
66+
- Tools that require elicitation MUST implement `getConfirmationMessage` and provide an easy-to-understand message for a human running the operation.
67+
- If a tool requires elicitation, it must be added to `src/common/config.ts` in the `confirmationRequiredTools` list in the defaultUserConfig.
68+
69+
### Guidelines for Atlas Tools
70+
71+
- The tool category MUST be `atlas`.

src/server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ export class Server {
239239
// Validate API client credentials
240240
if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
241241
try {
242+
if (!this.userConfig.apiBaseUrl.startsWith("https://")) {
243+
const message =
244+
"Failed to validate MongoDB Atlas the credentials from config: apiBaseUrl must start with https://";
245+
console.error(message);
246+
throw new Error(message);
247+
}
248+
242249
await this.session.apiClient.validateAccessToken();
243250
} catch (error) {
244251
if (this.userConfig.connectionString === undefined) {

src/tools/args.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z, type ZodString } from "zod";
2+
import { EJSON } from "bson";
23

34
const NO_UNICODE_REGEX = /^[\x20-\x7E]*$/;
45
export const NO_UNICODE_ERROR = "String cannot contain special characters or Unicode symbols";
@@ -68,3 +69,15 @@ export const AtlasArgs = {
6869
password: (): z.ZodString =>
6970
z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"),
7071
};
72+
73+
function toEJSON<T extends object | undefined>(value: T): T {
74+
if (!value) {
75+
return value;
76+
}
77+
78+
return EJSON.deserialize(value, { relaxed: false }) as T;
79+
}
80+
81+
export function zEJSON(): z.AnyZodObject {
82+
return z.object({}).passthrough().transform(toEJSON) as unknown as z.AnyZodObject;
83+
}

src/tools/mongodb/create/insertMany.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { z } from "zod";
22
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import type { ToolArgs, OperationType } from "../../tool.js";
5+
import { zEJSON } from "../../args.js";
56

67
export class InsertManyTool extends MongoDBToolBase {
78
public name = "insert-many";
89
protected description = "Insert an array of documents into a MongoDB collection";
910
protected argsShape = {
1011
...DbOperationArgs,
1112
documents: z
12-
.array(z.object({}).passthrough().describe("An individual MongoDB document"))
13+
.array(zEJSON().describe("An individual MongoDB document"))
1314
.describe(
1415
"The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()"
1516
),

src/tools/mongodb/delete/deleteMany.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
32
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
43
import type { ToolArgs, OperationType } from "../../tool.js";
54
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
65
import { EJSON } from "bson";
6+
import { zEJSON } from "../../args.js";
77

88
export class DeleteManyTool extends MongoDBToolBase {
99
public name = "delete-many";
1010
protected description = "Removes all documents that match the filter from a MongoDB collection";
1111
protected argsShape = {
1212
...DbOperationArgs,
13-
filter: z
14-
.object({})
15-
.passthrough()
13+
filter: zEJSON()
1614
.optional()
1715
.describe(
1816
"The query filter, specifying the deletion criteria. Matches the syntax of the filter argument of db.collection.deleteMany()"

src/tools/mongodb/metadata/explain.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { ToolArgs, OperationType } from "../../tool.js";
44
import { formatUntrustedData } from "../../tool.js";
55
import { z } from "zod";
66
import type { Document } from "mongodb";
7-
import { ExplainVerbosity } from "mongodb";
87
import { AggregateArgs } from "../read/aggregate.js";
98
import { FindArgs } from "../read/find.js";
109
import { CountArgs } from "../read/count.js";
@@ -34,16 +33,22 @@ export class ExplainTool extends MongoDBToolBase {
3433
])
3534
)
3635
.describe("The method and its arguments to run"),
36+
verbosity: z
37+
.enum(["queryPlanner", "queryPlannerExtended", "executionStats", "allPlansExecution"])
38+
.optional()
39+
.default("queryPlanner")
40+
.describe(
41+
"The verbosity of the explain plan, defaults to queryPlanner. If the user wants to know how fast is a query in execution time, use executionStats. It supports all verbosities as defined in the MongoDB Driver."
42+
),
3743
};
3844

3945
public operationType: OperationType = "metadata";
4046

41-
static readonly defaultVerbosity = ExplainVerbosity.queryPlanner;
42-
4347
protected async execute({
4448
database,
4549
collection,
4650
method: methods,
51+
verbosity,
4752
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
4853
const provider = await this.ensureConnected();
4954
const method = methods[0];
@@ -66,14 +71,12 @@ export class ExplainTool extends MongoDBToolBase {
6671
writeConcern: undefined,
6772
}
6873
)
69-
.explain(ExplainTool.defaultVerbosity);
74+
.explain(verbosity);
7075
break;
7176
}
7277
case "find": {
7378
const { filter, ...rest } = method.arguments;
74-
result = await provider
75-
.find(database, collection, filter as Document, { ...rest })
76-
.explain(ExplainTool.defaultVerbosity);
79+
result = await provider.find(database, collection, filter as Document, { ...rest }).explain(verbosity);
7780
break;
7881
}
7982
case "count": {
@@ -83,15 +86,15 @@ export class ExplainTool extends MongoDBToolBase {
8386
count: collection,
8487
query,
8588
},
86-
verbosity: ExplainTool.defaultVerbosity,
89+
verbosity,
8790
});
8891
break;
8992
}
9093
}
9194

9295
return {
9396
content: formatUntrustedData(
94-
`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.`,
97+
`Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in "${database}.${collection}". The execution plan was run with the following verbosity: "${verbosity}". This information can be used to understand how the query was executed and to optimize the query performance.`,
9598
JSON.stringify(result)
9699
),
97100
};

src/tools/mongodb/read/aggregate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { formatUntrustedData } from "../../tool.js";
66
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
77
import { EJSON } from "bson";
88
import { ErrorCodes, MongoDBError } from "../../../common/errors.js";
9+
import { zEJSON } from "../../args.js";
910

1011
export const AggregateArgs = {
11-
pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
12+
pipeline: z.array(zEJSON()).describe("An array of aggregation stages to execute"),
1213
};
1314

1415
export class AggregateTool extends MongoDBToolBase {

src/tools/mongodb/read/count.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
33
import type { ToolArgs, OperationType } from "../../tool.js";
4-
import { z } from "zod";
54
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
5+
import { zEJSON } from "../../args.js";
66

77
export const CountArgs = {
8-
query: z
9-
.object({})
10-
.passthrough()
8+
query: zEJSON()
119
.optional()
1210
.describe(
1311
"A filter/query parameter. Allows users to filter the documents to count. Matches the syntax of the filter argument of db.collection.count()."

src/tools/mongodb/read/find.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import { formatUntrustedData } from "../../tool.js";
66
import type { SortDirection } from "mongodb";
77
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
88
import { EJSON } from "bson";
9+
import { zEJSON } from "../../args.js";
910

1011
export const FindArgs = {
11-
filter: z
12-
.object({})
13-
.passthrough()
12+
filter: zEJSON()
1413
.optional()
1514
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
1615
projection: z

src/tools/mongodb/update/updateMany.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,21 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import type { ToolArgs, OperationType } from "../../tool.js";
55
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
6+
import { zEJSON } from "../../args.js";
67

78
export class UpdateManyTool extends MongoDBToolBase {
89
public name = "update-many";
910
protected description = "Updates all documents that match the specified filter for a collection";
1011
protected argsShape = {
1112
...DbOperationArgs,
12-
filter: z
13-
.object({})
14-
.passthrough()
13+
filter: zEJSON()
1514
.optional()
1615
.describe(
1716
"The selection criteria for the update, matching the syntax of the filter argument of db.collection.updateOne()"
1817
),
19-
update: z
20-
.object({})
21-
.passthrough()
22-
.describe("An update document describing the modifications to apply using update operator expressions"),
18+
update: zEJSON().describe(
19+
"An update document describing the modifications to apply using update operator expressions"
20+
),
2321
upsert: z
2422
.boolean()
2523
.optional()

0 commit comments

Comments
 (0)