From eb1dbc7416240d12b7b126b01e8c605a8df72309 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 17 Apr 2025 01:29:07 +0200 Subject: [PATCH 1/5] chore: add integration tests for create-index --- src/tools/mongodb/create/createIndex.ts | 11 +- .../tools/mongodb/create/createIndext.test.ts | 213 ++++++++++++++++++ 2 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 tests/integration/tools/mongodb/create/createIndext.test.ts diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index d14abc78..beffaf86 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -10,22 +10,29 @@ export class CreateIndexTool extends MongoDBToolBase { protected argsShape = { ...DbOperationArgs, keys: z.record(z.string(), z.custom()).describe("The index definition"), + name: z.string().optional().describe("The name of the index"), }; protected operationType: OperationType = "create"; - protected async execute({ database, collection, keys }: ToolArgs): Promise { + protected async execute({ + database, + collection, + keys, + name, + }: ToolArgs): Promise { const provider = await this.ensureConnected(); const indexes = await provider.createIndexes(database, collection, [ { key: keys, + name, }, ]); return { content: [ { - text: `Created the index \`${indexes[0]}\``, + text: `Created the index "${indexes[0]}" on collection "${collection}" in database "${database}"`, type: "text", }, ], diff --git a/tests/integration/tools/mongodb/create/createIndext.test.ts b/tests/integration/tools/mongodb/create/createIndext.test.ts new file mode 100644 index 00000000..8b7a3a34 --- /dev/null +++ b/tests/integration/tools/mongodb/create/createIndext.test.ts @@ -0,0 +1,213 @@ +import { + getResponseContent, + validateParameters, + dbOperationParameters, + setupIntegrationTest, +} from "../../../helpers.js"; +import { McpError } from "@modelcontextprotocol/sdk/types.js"; +import { ObjectId } from "bson"; +import { IndexDirection } from "mongodb"; + +describe("createIndex tool", () => { + const integration = setupIntegrationTest(); + + let dbName: string; + beforeEach(() => { + dbName = new ObjectId().toString(); + }); + + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const listCollections = tools.find((tool) => tool.name === "create-index")!; + expect(listCollections).toBeDefined(); + expect(listCollections.description).toBe("Create an index for a collection"); + + validateParameters(listCollections, [ + ...dbOperationParameters, + { + name: "keys", + type: "object", + description: "The index definition", + required: true, + }, + { + name: "name", + type: "string", + description: "The name of the index", + required: false, + }, + ]); + }); + + describe("with invalid arguments", () => { + const args = [ + {}, + { collection: "bar", database: 123, keys: { foo: 1 } }, + { collection: "bar", database: "test", keys: { foo: 5 } }, + { collection: [], database: "test", keys: { foo: 1 } }, + { collection: "bar", database: "test", keys: { foo: 1 }, name: 123 }, + { collection: "bar", database: "test", keys: "foo", name: "my-index" }, + ]; + for (const arg of args) { + it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => { + await integration.connectMcpClient(); + try { + await integration.mcpClient().callTool({ name: "create-index", 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 create-index"); + } + }); + } + }); + + const validateIndex = async (collection: string, expected: { name: string; key: object }[]) => { + const mongoClient = integration.mongoClient(); + const collections = await mongoClient.db(dbName).listCollections().toArray(); + expect(collections).toHaveLength(1); + expect(collections[0].name).toEqual("coll1"); + const indexes = await mongoClient.db(dbName).collection(collection).indexes(); + expect(indexes).toHaveLength(expected.length + 1); + expect(indexes[0].name).toEqual("_id_"); + for (const index of expected) { + const foundIndex = indexes.find((i) => i.name === index.name); + expect(foundIndex).toBeDefined(); + expect(foundIndex!.key).toEqual(index.key); + } + }; + + it("creates the namespace if necessary", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 }, name: "my-index" }, + }); + + const content = getResponseContent(response.content); + expect(content).toEqual(`Created the index "my-index" on collection "coll1" in database "${dbName}"`); + + await validateIndex("coll1", [{ name: "my-index", key: { prop1: 1 } }]); + }); + + it("generates a name if not provided", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + }); + + const content = getResponseContent(response.content); + expect(content).toEqual(`Created the index "prop1_1" on collection "coll1" in database "${dbName}"`); + await validateIndex("coll1", [{ name: "prop1_1", key: { prop1: 1 } }]); + }); + + it("can create multiple indexes in the same collection", async () => { + await integration.connectMcpClient(); + let response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + ); + + response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop2: -1 } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop2_-1" on collection "coll1" in database "${dbName}"` + ); + + await validateIndex("coll1", [ + { name: "prop1_1", key: { prop1: 1 } }, + { name: "prop2_-1", key: { prop2: -1 } }, + ]); + }); + + it("can create multiple indexes on the same property", async () => { + await integration.connectMcpClient(); + let response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + ); + + response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: -1 } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop1_-1" on collection "coll1" in database "${dbName}"` + ); + + await validateIndex("coll1", [ + { name: "prop1_1", key: { prop1: 1 } }, + { name: "prop1_-1", key: { prop1: -1 } }, + ]); + }); + + it("doesn't duplicate indexes", async () => { + await integration.connectMcpClient(); + let response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + ); + + response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + ); + + await validateIndex("coll1", [{ name: "prop1_1", key: { prop1: 1 } }]); + }); + + const testCases: { name: string; direction: IndexDirection }[] = [ + { name: "descending", direction: -1 }, + { name: "ascending", direction: 1 }, + { name: "hashed", direction: "hashed" }, + { name: "text", direction: "text" }, + { name: "geoHaystack", direction: "2dsphere" }, + { name: "geo2d", direction: "2d" }, + ]; + + for (const { name, direction } of testCases) { + it(`creates ${name} index`, async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { database: dbName, collection: "coll1", keys: { prop1: direction } }, + }); + + expect(getResponseContent(response.content)).toEqual( + `Created the index "prop1_${direction}" on collection "coll1" in database "${dbName}"` + ); + + let expectedKey: object = { prop1: direction }; + if (direction === "text") { + expectedKey = { + _fts: "text", + _ftsx: 1, + }; + } + await validateIndex("coll1", [{ name: `prop1_${direction}`, key: expectedKey }]); + }); + } +}); From 7e885e6983558a472c7ec0efe5d1fd7dc848b564 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 17 Apr 2025 01:56:19 +0200 Subject: [PATCH 2/5] add tests for automatic connections --- src/tools/mongodb/metadata/connect.ts | 32 +++---- src/tools/mongodb/mongodbTool.ts | 3 +- tests/integration/helpers.ts | 10 ++- .../mongodb/create/createCollection.test.ts | 64 +++++++++----- ...eateIndext.test.ts => createIndex.test.ts} | 87 +++++++++++++------ .../mongodb/metadata/listCollections.test.ts | 32 ++++++- .../mongodb/metadata/listDatabases.test.ts | 20 ++++- .../tools/mongodb/read/count.test.ts | 23 +++++ 8 files changed, 200 insertions(+), 71 deletions(-) rename tests/integration/tools/mongodb/create/{createIndext.test.ts => createIndex.test.ts} (68%) diff --git a/src/tools/mongodb/metadata/connect.ts b/src/tools/mongodb/metadata/connect.ts index aa8222bf..45f22231 100644 --- a/src/tools/mongodb/metadata/connect.ts +++ b/src/tools/mongodb/metadata/connect.ts @@ -37,25 +37,21 @@ export class ConnectTool extends MongoDBToolBase { let connectionString: string; - if (typeof connectionStringOrClusterName === "string") { - if ( - connectionStringOrClusterName.startsWith("mongodb://") || - connectionStringOrClusterName.startsWith("mongodb+srv://") - ) { - connectionString = connectionStringOrClusterName; - } else { - // TODO: - return { - content: [ - { - type: "text", - text: `Connecting via cluster name not supported yet. Please provide a connection string.`, - }, - ], - }; - } + if ( + connectionStringOrClusterName.startsWith("mongodb://") || + connectionStringOrClusterName.startsWith("mongodb+srv://") + ) { + connectionString = connectionStringOrClusterName; } else { - throw new MongoDBError(ErrorCodes.InvalidParams, "Invalid connection options"); + // TODO: + return { + content: [ + { + type: "text", + text: `Connecting via cluster name not supported yet. Please provide a connection string.`, + }, + ], + }; } try { diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 8fdc6399..79e32db1 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -19,9 +19,10 @@ export abstract class MongoDBToolBase extends ToolBase { protected category: ToolCategory = "mongodb"; protected async ensureConnected(): Promise { - const provider = this.session.serviceProvider; + let provider = this.session.serviceProvider; if (!provider && config.connectionString) { await this.connectToMongoDB(config.connectionString); + provider = this.session.serviceProvider; } if (!provider) { diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 2fc112d0..f8f797d0 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -6,8 +6,9 @@ import path from "path"; import fs from "fs/promises"; import { Session } from "../../src/session.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { MongoClient } from "mongodb"; +import { MongoClient, ObjectId } from "mongodb"; import { toIncludeAllMembers } from "jest-extended"; +import config from "../../src/config.js"; interface ParameterInfo { name: string; @@ -23,6 +24,7 @@ export function setupIntegrationTest(): { mongoClient: () => MongoClient; connectionString: () => string; connectMcpClient: () => Promise; + randomDbName: () => string; } { let mongoCluster: runner.MongoCluster | undefined; let mongoClient: MongoClient | undefined; @@ -30,6 +32,8 @@ export function setupIntegrationTest(): { let mcpClient: Client | undefined; let mcpServer: Server | undefined; + let randomDbName: string; + beforeEach(async () => { const clientTransport = new InMemoryTransport(); const serverTransport = new InMemoryTransport(); @@ -59,6 +63,7 @@ export function setupIntegrationTest(): { }); await mcpServer.connect(serverTransport); await mcpClient.connect(clientTransport); + randomDbName = new ObjectId().toString(); }); afterEach(async () => { @@ -70,6 +75,8 @@ export function setupIntegrationTest(): { await mongoClient?.close(); mongoClient = undefined; + + config.connectionString = undefined; }); beforeAll(async function () { @@ -144,6 +151,7 @@ export function setupIntegrationTest(): { arguments: { connectionStringOrClusterName: getConnectionString() }, }); }, + randomDbName: () => randomDbName, }; } diff --git a/tests/integration/tools/mongodb/create/createCollection.test.ts b/tests/integration/tools/mongodb/create/createCollection.test.ts index 090a1851..bc983c2a 100644 --- a/tests/integration/tools/mongodb/create/createCollection.test.ts +++ b/tests/integration/tools/mongodb/create/createCollection.test.ts @@ -7,6 +7,7 @@ import { import { toIncludeSameMembers } from "jest-extended"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { ObjectId } from "bson"; +import config from "../../../../../src/config.js"; describe("createCollection tool", () => { const integration = setupIntegrationTest(); @@ -48,69 +49,90 @@ describe("createCollection tool", () => { describe("with non-existent database", () => { it("creates a new collection", async () => { const mongoClient = integration.mongoClient(); - let collections = await mongoClient.db("foo").listCollections().toArray(); + let collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(0); await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "create-collection", - arguments: { database: "foo", collection: "bar" }, + arguments: { database: integration.randomDbName(), collection: "bar" }, }); const content = getResponseContent(response.content); - expect(content).toEqual('Collection "bar" created in database "foo".'); + expect(content).toEqual(`Collection "bar" created in database "${integration.randomDbName()}".`); - collections = await mongoClient.db("foo").listCollections().toArray(); + collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(1); expect(collections[0].name).toEqual("bar"); }); }); describe("with existing database", () => { - let dbName: string; - beforeEach(() => { - dbName = new ObjectId().toString(); - }); - it("creates new collection", async () => { const mongoClient = integration.mongoClient(); - await mongoClient.db(dbName).createCollection("collection1"); - let collections = await mongoClient.db(dbName).listCollections().toArray(); + await mongoClient.db(integration.randomDbName()).createCollection("collection1"); + let collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(1); await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "create-collection", - arguments: { database: dbName, collection: "collection2" }, + arguments: { database: integration.randomDbName(), collection: "collection2" }, }); const content = getResponseContent(response.content); - expect(content).toEqual(`Collection "collection2" created in database "${dbName}".`); - collections = await mongoClient.db(dbName).listCollections().toArray(); + expect(content).toEqual(`Collection "collection2" created in database "${integration.randomDbName()}".`); + collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(2); expect(collections.map((c) => c.name)).toIncludeSameMembers(["collection1", "collection2"]); }); it("does nothing if collection already exists", async () => { const mongoClient = integration.mongoClient(); - await mongoClient.db(dbName).collection("collection1").insertOne({}); - let collections = await mongoClient.db(dbName).listCollections().toArray(); + await mongoClient.db(integration.randomDbName()).collection("collection1").insertOne({}); + let collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(1); - let documents = await mongoClient.db(dbName).collection("collection1").find({}).toArray(); + let documents = await mongoClient + .db(integration.randomDbName()) + .collection("collection1") + .find({}) + .toArray(); expect(documents).toHaveLength(1); await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "create-collection", - arguments: { database: dbName, collection: "collection1" }, + arguments: { database: integration.randomDbName(), collection: "collection1" }, }); const content = getResponseContent(response.content); - expect(content).toEqual(`Collection "collection1" created in database "${dbName}".`); - collections = await mongoClient.db(dbName).listCollections().toArray(); + expect(content).toEqual(`Collection "collection1" created in database "${integration.randomDbName()}".`); + collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(1); expect(collections[0].name).toEqual("collection1"); // Make sure we didn't drop the existing collection - documents = await mongoClient.db(dbName).collection("collection1").find({}).toArray(); + documents = await mongoClient.db(integration.randomDbName()).collection("collection1").find({}).toArray(); expect(documents).toHaveLength(1); }); }); + + describe("when not connected", () => { + it("connects automatically if connection string is configured", async () => { + config.connectionString = integration.connectionString(); + + const response = await integration.mcpClient().callTool({ + name: "create-collection", + arguments: { database: integration.randomDbName(), collection: "new-collection" }, + }); + const content = getResponseContent(response.content); + expect(content).toEqual(`Collection "new-collection" created in database "${integration.randomDbName()}".`); + }); + + it("throw an error if connection string is not configured", async () => { + const response = await integration.mcpClient().callTool({ + name: "create-collection", + arguments: { database: integration.randomDbName(), collection: "new-collection" }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("You need to connect to a MongoDB instance before you can access its data."); + }); + }); }); diff --git a/tests/integration/tools/mongodb/create/createIndext.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts similarity index 68% rename from tests/integration/tools/mongodb/create/createIndext.test.ts rename to tests/integration/tools/mongodb/create/createIndex.test.ts index 8b7a3a34..82f23734 100644 --- a/tests/integration/tools/mongodb/create/createIndext.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -7,15 +7,11 @@ import { import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { ObjectId } from "bson"; import { IndexDirection } from "mongodb"; +import config from "../../../../../src/config.js"; describe("createIndex tool", () => { const integration = setupIntegrationTest(); - let dbName: string; - beforeEach(() => { - dbName = new ObjectId().toString(); - }); - it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); const listCollections = tools.find((tool) => tool.name === "create-index")!; @@ -66,10 +62,10 @@ describe("createIndex tool", () => { const validateIndex = async (collection: string, expected: { name: string; key: object }[]) => { const mongoClient = integration.mongoClient(); - const collections = await mongoClient.db(dbName).listCollections().toArray(); + const collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); expect(collections).toHaveLength(1); expect(collections[0].name).toEqual("coll1"); - const indexes = await mongoClient.db(dbName).collection(collection).indexes(); + const indexes = await mongoClient.db(integration.randomDbName()).collection(collection).indexes(); expect(indexes).toHaveLength(expected.length + 1); expect(indexes[0].name).toEqual("_id_"); for (const index of expected) { @@ -83,11 +79,18 @@ describe("createIndex tool", () => { await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 }, name: "my-index" }, + arguments: { + database: integration.randomDbName(), + collection: "coll1", + keys: { prop1: 1 }, + name: "my-index", + }, }); const content = getResponseContent(response.content); - expect(content).toEqual(`Created the index "my-index" on collection "coll1" in database "${dbName}"`); + expect(content).toEqual( + `Created the index "my-index" on collection "coll1" in database "${integration.randomDbName()}"` + ); await validateIndex("coll1", [{ name: "my-index", key: { prop1: 1 } }]); }); @@ -96,11 +99,13 @@ describe("createIndex tool", () => { await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: 1 } }, }); const content = getResponseContent(response.content); - expect(content).toEqual(`Created the index "prop1_1" on collection "coll1" in database "${dbName}"`); + expect(content).toEqual( + `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"` + ); await validateIndex("coll1", [{ name: "prop1_1", key: { prop1: 1 } }]); }); @@ -108,20 +113,20 @@ describe("createIndex tool", () => { await integration.connectMcpClient(); let response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: 1 } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"` ); response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop2: -1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop2: -1 } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop2_-1" on collection "coll1" in database "${dbName}"` + `Created the index "prop2_-1" on collection "coll1" in database "${integration.randomDbName()}"` ); await validateIndex("coll1", [ @@ -134,20 +139,20 @@ describe("createIndex tool", () => { await integration.connectMcpClient(); let response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: 1 } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"` ); response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: -1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: -1 } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop1_-1" on collection "coll1" in database "${dbName}"` + `Created the index "prop1_-1" on collection "coll1" in database "${integration.randomDbName()}"` ); await validateIndex("coll1", [ @@ -160,20 +165,20 @@ describe("createIndex tool", () => { await integration.connectMcpClient(); let response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: 1 } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"` ); response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: 1 } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: 1 } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop1_1" on collection "coll1" in database "${dbName}"` + `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"` ); await validateIndex("coll1", [{ name: "prop1_1", key: { prop1: 1 } }]); @@ -193,11 +198,11 @@ describe("createIndex tool", () => { await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "create-index", - arguments: { database: dbName, collection: "coll1", keys: { prop1: direction } }, + arguments: { database: integration.randomDbName(), collection: "coll1", keys: { prop1: direction } }, }); expect(getResponseContent(response.content)).toEqual( - `Created the index "prop1_${direction}" on collection "coll1" in database "${dbName}"` + `Created the index "prop1_${direction}" on collection "coll1" in database "${integration.randomDbName()}"` ); let expectedKey: object = { prop1: direction }; @@ -210,4 +215,36 @@ describe("createIndex tool", () => { await validateIndex("coll1", [{ name: `prop1_${direction}`, key: expectedKey }]); }); } + + describe("when not connected", () => { + it("connects automatically if connection string is configured", async () => { + config.connectionString = integration.connectionString(); + + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + keys: { prop1: 1 }, + }, + }); + const content = getResponseContent(response.content); + expect(content).toEqual( + `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"` + ); + }); + + it("throw an error if connection string is not configured", async () => { + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + keys: { prop1: 1 }, + }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("You need to connect to a MongoDB instance before you can access its data."); + }); + }); }); diff --git a/tests/integration/tools/mongodb/metadata/listCollections.test.ts b/tests/integration/tools/mongodb/metadata/listCollections.test.ts index 5842bf02..47f1e3de 100644 --- a/tests/integration/tools/mongodb/metadata/listCollections.test.ts +++ b/tests/integration/tools/mongodb/metadata/listCollections.test.ts @@ -1,6 +1,8 @@ import { getResponseElements, getResponseContent, validateParameters, setupIntegrationTest } from "../../../helpers.js"; import { toIncludeSameMembers } from "jest-extended"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; +import config from "../../../../../src/config.js"; +import { ObjectId } from "bson"; describe("listCollections tool", () => { const integration = setupIntegrationTest(); @@ -52,22 +54,22 @@ describe("listCollections tool", () => { describe("with existing database", () => { it("returns collections", async () => { const mongoClient = integration.mongoClient(); - await mongoClient.db("my-db").createCollection("collection-1"); + await mongoClient.db(integration.randomDbName()).createCollection("collection-1"); await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ name: "list-collections", - arguments: { database: "my-db" }, + arguments: { database: integration.randomDbName() }, }); const items = getResponseElements(response.content); expect(items).toHaveLength(1); expect(items[0].text).toContain('Name: "collection-1"'); - await mongoClient.db("my-db").createCollection("collection-2"); + await mongoClient.db(integration.randomDbName()).createCollection("collection-2"); const response2 = await integration.mcpClient().callTool({ name: "list-collections", - arguments: { database: "my-db" }, + arguments: { database: integration.randomDbName() }, }); const items2 = getResponseElements(response2.content); expect(items2).toHaveLength(2); @@ -77,4 +79,26 @@ describe("listCollections tool", () => { ]); }); }); + + describe("when not connected", () => { + it("connects automatically if connection string is configured", async () => { + config.connectionString = integration.connectionString(); + + const response = await integration + .mcpClient() + .callTool({ name: "list-collections", arguments: { database: integration.randomDbName() } }); + const content = getResponseContent(response.content); + expect(content).toEqual( + `No collections found for database "${integration.randomDbName()}". To create a collection, use the "create-collection" tool.` + ); + }); + + it("throw an error if connection string is not configured", async () => { + const response = await integration + .mcpClient() + .callTool({ name: "list-collections", arguments: { database: integration.randomDbName() } }); + const content = getResponseContent(response.content); + expect(content).toContain("You need to connect to a MongoDB instance before you can access its data."); + }); + }); }); diff --git a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts index 5c3b5f48..33ca83d7 100644 --- a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +++ b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts @@ -1,4 +1,5 @@ -import { getResponseElements, getParameters, setupIntegrationTest } from "../../../helpers.js"; +import config from "../../../../../src/config.js"; +import { getResponseElements, getParameters, setupIntegrationTest, getResponseContent } from "../../../helpers.js"; import { toIncludeSameMembers } from "jest-extended"; describe("listDatabases tool", () => { @@ -14,6 +15,23 @@ describe("listDatabases tool", () => { expect(parameters).toHaveLength(0); }); + describe("when not connected", () => { + it("connects automatically if connection string is configured", async () => { + config.connectionString = integration.connectionString(); + + const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} }); + const dbNames = getDbNames(response.content); + + expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]); + }); + + it("throw an error if connection string is not configured", async () => { + const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} }); + const content = getResponseContent(response.content); + expect(content).toContain("You need to connect to a MongoDB instance before you can access its data."); + }); + }); + describe("with no preexisting databases", () => { it("returns only the system databases", async () => { await integration.connectMcpClient(); diff --git a/tests/integration/tools/mongodb/read/count.test.ts b/tests/integration/tools/mongodb/read/count.test.ts index ee47ab65..b775dfdd 100644 --- a/tests/integration/tools/mongodb/read/count.test.ts +++ b/tests/integration/tools/mongodb/read/count.test.ts @@ -7,6 +7,7 @@ import { import { toIncludeSameMembers } from "jest-extended"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { ObjectId } from "mongodb"; +import config from "../../../../../src/config.js"; describe("count tool", () => { const integration = setupIntegrationTest(); @@ -112,4 +113,26 @@ describe("count tool", () => { }); } }); + + describe("when not connected", () => { + it("connects automatically if connection string is configured", async () => { + config.connectionString = integration.connectionString(); + + const response = await integration.mcpClient().callTool({ + name: "count", + arguments: { database: randomDbName, collection: "coll1" }, + }); + const content = getResponseContent(response.content); + expect(content).toEqual('Found 0 documents in the collection "coll1"'); + }); + + it("throw an error if connection string is not configured", async () => { + const response = await integration.mcpClient().callTool({ + name: "count", + arguments: { database: randomDbName, collection: "coll1" }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("You need to connect to a MongoDB instance before you can access its data."); + }); + }); }); From 3fc81e68b45e6eb051128134ff1965418076aec2 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 17 Apr 2025 10:50:52 +0200 Subject: [PATCH 3/5] fix lint --- src/tools/mongodb/metadata/connect.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/mongodb/metadata/connect.ts b/src/tools/mongodb/metadata/connect.ts index 45f22231..4ed8ea3c 100644 --- a/src/tools/mongodb/metadata/connect.ts +++ b/src/tools/mongodb/metadata/connect.ts @@ -2,7 +2,6 @@ import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { MongoDBToolBase } from "../mongodbTool.js"; import { ToolArgs, OperationType } from "../../tool.js"; -import { ErrorCodes, MongoDBError } from "../../../errors.js"; import config from "../../../config.js"; import { MongoError as DriverError } from "mongodb"; From 3d9564b35e16a05895ce3324b3229ab5c9574079 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 17 Apr 2025 10:54:48 +0200 Subject: [PATCH 4/5] fix copy-paste error --- .../integration/tools/mongodb/create/createIndex.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/integration/tools/mongodb/create/createIndex.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts index 82f23734..34c6f37c 100644 --- a/tests/integration/tools/mongodb/create/createIndex.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -5,7 +5,6 @@ import { setupIntegrationTest, } from "../../../helpers.js"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; -import { ObjectId } from "bson"; import { IndexDirection } from "mongodb"; import config from "../../../../../src/config.js"; @@ -14,11 +13,11 @@ describe("createIndex tool", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const listCollections = tools.find((tool) => tool.name === "create-index")!; - expect(listCollections).toBeDefined(); - expect(listCollections.description).toBe("Create an index for a collection"); + const createIndex = tools.find((tool) => tool.name === "create-index")!; + expect(createIndex).toBeDefined(); + expect(createIndex.description).toBe("Create an index for a collection"); - validateParameters(listCollections, [ + validateParameters(createIndex, [ ...dbOperationParameters, { name: "keys", From f9f65ed76f08727cfd77865e6974ba2ddecacadd Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 22 Apr 2025 11:33:42 +0200 Subject: [PATCH 5/5] CR comments --- src/tools/mongodb/metadata/connect.ts | 4 +++- src/tools/mongodb/mongodbTool.ts | 8 +++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tools/mongodb/metadata/connect.ts b/src/tools/mongodb/metadata/connect.ts index 4ed8ea3c..98d5b015 100644 --- a/src/tools/mongodb/metadata/connect.ts +++ b/src/tools/mongodb/metadata/connect.ts @@ -42,7 +42,9 @@ export class ConnectTool extends MongoDBToolBase { ) { connectionString = connectionStringOrClusterName; } else { - // TODO: + // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/19 + // We don't support connecting via cluster name since we'd need to obtain the user credentials + // and fill in the connection string. return { content: [ { diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 79e32db1..520d10d5 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -19,17 +19,15 @@ export abstract class MongoDBToolBase extends ToolBase { protected category: ToolCategory = "mongodb"; protected async ensureConnected(): Promise { - let provider = this.session.serviceProvider; - if (!provider && config.connectionString) { + if (!this.session.serviceProvider && config.connectionString) { await this.connectToMongoDB(config.connectionString); - provider = this.session.serviceProvider; } - if (!provider) { + if (!this.session.serviceProvider) { throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } - return provider; + return this.session.serviceProvider; } protected handleError(error: unknown): Promise | CallToolResult {