diff --git a/eslint.config.js b/eslint.config.js index da617263..b6263450 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -30,6 +30,6 @@ export default defineConfig([ // TODO: Configure tests and scripts to work with this. ignores: ["eslint.config.js", "jest.config.js", "tests/**/*.ts", "scripts/**/*.ts"], }), - globalIgnores(["node_modules", "dist", "src/common/atlas/openapi.d.ts"]), + globalIgnores(["node_modules", "dist", "src/common/atlas/openapi.d.ts", "coverage"]), eslintConfigPrettier, ]); diff --git a/src/tools/mongodb/read/count.ts b/src/tools/mongodb/read/count.ts index f9778011..188648d5 100644 --- a/src/tools/mongodb/read/count.ts +++ b/src/tools/mongodb/read/count.ts @@ -30,7 +30,7 @@ export class CountTool extends MongoDBToolBase { return { content: [ { - text: `Found ${count} documents in the collection \`${collection}\``, + text: `Found ${count} documents in the collection "${collection}"`, type: "text", }, ], diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 97a4bf0d..24870e15 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -13,6 +13,7 @@ interface ParameterInfo { name: string; type: string; description: string; + required: boolean; } type ToolInfo = Awaited>["tools"][number]; @@ -180,10 +181,16 @@ export function getParameters(tool: ToolInfo): ParameterInfo[] { name: key, type: typedValue.type, description: typedValue.description, + required: (tool.inputSchema.required as string[])?.includes(key) ?? false, }; }); } +export const dbOperationParameters: ParameterInfo[] = [ + { name: "database", type: "string", description: "Database name", required: true }, + { name: "collection", type: "string", description: "Collection name", required: true }, +]; + export function validateParameters(tool: ToolInfo, parameters: ParameterInfo[]): void { const toolParameters = getParameters(tool); expect(toolParameters).toHaveLength(parameters.length); diff --git a/tests/integration/tools/mongodb/create/createCollection.test.ts b/tests/integration/tools/mongodb/create/createCollection.test.ts index c8031d2c..1bf216e8 100644 --- a/tests/integration/tools/mongodb/create/createCollection.test.ts +++ b/tests/integration/tools/mongodb/create/createCollection.test.ts @@ -4,6 +4,7 @@ import { jestTestMCPClient, getResponseContent, validateParameters, + dbOperationParameters, } from "../../../helpers.js"; import { toIncludeSameMembers } from "jest-extended"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; @@ -21,18 +22,7 @@ describe("createCollection tool", () => { "Creates a new collection in a database. If the database doesn't exist, it will be created automatically." ); - validateParameters(listCollections, [ - { - name: "database", - description: "Database name", - type: "string", - }, - { - name: "collection", - description: "Collection name", - type: "string", - }, - ]); + validateParameters(listCollections, dbOperationParameters); }); describe("with invalid arguments", () => { diff --git a/tests/integration/tools/mongodb/metadata/connect.test.ts b/tests/integration/tools/mongodb/metadata/connect.test.ts index 2871eec3..a3314f99 100644 --- a/tests/integration/tools/mongodb/metadata/connect.test.ts +++ b/tests/integration/tools/mongodb/metadata/connect.test.ts @@ -17,6 +17,7 @@ describe("Connect tool", () => { name: "connectionStringOrClusterName", description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format) or cluster name", type: "string", + required: false, }, ]); }); diff --git a/tests/integration/tools/mongodb/metadata/listCollections.test.ts b/tests/integration/tools/mongodb/metadata/listCollections.test.ts index 6d422513..f8127178 100644 --- a/tests/integration/tools/mongodb/metadata/listCollections.test.ts +++ b/tests/integration/tools/mongodb/metadata/listCollections.test.ts @@ -4,7 +4,6 @@ import { jestTestCluster, jestTestMCPClient, getResponseContent, - getParameters, validateParameters, } from "../../../helpers.js"; import { toIncludeSameMembers } from "jest-extended"; @@ -20,7 +19,9 @@ describe("listCollections tool", () => { expect(listCollections).toBeDefined(); expect(listCollections.description).toBe("List all collections for a given database"); - validateParameters(listCollections, [{ name: "database", description: "Database name", type: "string" }]); + validateParameters(listCollections, [ + { name: "database", description: "Database name", type: "string", required: true }, + ]); }); describe("with invalid arguments", () => { diff --git a/tests/integration/tools/mongodb/read/count.test.ts b/tests/integration/tools/mongodb/read/count.test.ts new file mode 100644 index 00000000..a807b9a6 --- /dev/null +++ b/tests/integration/tools/mongodb/read/count.test.ts @@ -0,0 +1,118 @@ +import { + connect, + jestTestCluster, + jestTestMCPClient, + getResponseContent, + validateParameters, + dbOperationParameters, +} from "../../../helpers.js"; +import { toIncludeSameMembers } from "jest-extended"; +import { McpError } from "@modelcontextprotocol/sdk/types.js"; +import { ObjectId } from "mongodb"; + +describe("count tool", () => { + const client = jestTestMCPClient(); + const cluster = jestTestCluster(); + + let randomDbName: string; + beforeEach(() => { + randomDbName = new ObjectId().toString(); + }); + + it("should have correct metadata", async () => { + const { tools } = await client().listTools(); + const listCollections = tools.find((tool) => tool.name === "count")!; + expect(listCollections).toBeDefined(); + expect(listCollections.description).toBe("Gets the number of documents in a MongoDB collection"); + + validateParameters(listCollections, [ + { + name: "query", + description: + "The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()", + type: "object", + required: false, + }, + ...dbOperationParameters, + ]); + }); + + describe("with invalid arguments", () => { + const args = [ + {}, + { database: 123, collection: "bar" }, + { foo: "bar", database: "test", collection: "bar" }, + { collection: [], database: "test" }, + { collection: "bar", database: "test", query: "{ $gt: { foo: 5 } }" }, + ]; + for (const arg of args) { + it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => { + await connect(client(), cluster()); + try { + await client().callTool({ name: "count", arguments: arg }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(McpError); + const mcpError = error as McpError; + expect(mcpError.code).toEqual(-32602); + expect(mcpError.message).toContain("Invalid arguments for tool count"); + } + }); + } + }); + + it("returns 0 when database doesn't exist", async () => { + await connect(client(), cluster()); + const response = await client().callTool({ + name: "count", + arguments: { database: "non-existent", collection: "foos" }, + }); + const content = getResponseContent(response.content); + expect(content).toEqual('Found 0 documents in the collection "foos"'); + }); + + it("returns 0 when collection doesn't exist", async () => { + await connect(client(), cluster()); + const mongoClient = cluster().getClient(); + await mongoClient.db(randomDbName).collection("bar").insertOne({}); + const response = await client().callTool({ + name: "count", + arguments: { database: randomDbName, collection: "non-existent" }, + }); + const content = getResponseContent(response.content); + expect(content).toEqual('Found 0 documents in the collection "non-existent"'); + }); + + describe("with existing database", () => { + beforeEach(async () => { + const mongoClient = cluster().getClient(); + await mongoClient + .db(randomDbName) + .collection("foo") + .insertMany([ + { name: "Peter", age: 5 }, + { name: "Parker", age: 10 }, + { name: "George", age: 15 }, + ]); + }); + + const testCases = [ + { filter: undefined, expectedCount: 3 }, + { filter: {}, expectedCount: 3 }, + { filter: { age: { $lt: 15 } }, expectedCount: 2 }, + { filter: { age: { $gt: 5 }, name: { $regex: "^P" } }, expectedCount: 1 }, + ]; + for (const testCase of testCases) { + it(`returns ${testCase.expectedCount} documents for filter ${JSON.stringify(testCase.filter)}`, async () => { + await connect(client(), cluster()); + const response = await client().callTool({ + name: "count", + arguments: { database: randomDbName, collection: "foo", query: testCase.filter }, + }); + + const content = getResponseContent(response.content); + expect(content).toEqual(`Found ${testCase.expectedCount} documents in the collection "foo"`); + }); + } + }); +});