From eaffdbea6253e6386c0ea80f7d3595a5b8692d4b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 14 Apr 2025 10:49:40 +0200 Subject: [PATCH 1/3] chore: add integration tests for list-databases --- jest.config.js | 1 + package-lock.json | 23 ++++++ package.json | 3 +- src/tools/mongodb/{ => create}/createIndex.ts | 4 +- src/tools/mongodb/{ => metadata}/connect.ts | 8 +- .../mongodb/{ => read}/collectionIndexes.ts | 4 +- src/tools/mongodb/tools.ts | 6 +- tests/integration/helpers.ts | 19 ++++- .../mongodb/{ => metadata}/connect.test.ts | 56 +++++++------- .../mongodb/metadata/listDatabases.test.ts | 74 +++++++++++++++++++ 10 files changed, 155 insertions(+), 43 deletions(-) rename src/tools/mongodb/{ => create}/createIndex.ts (94%) rename src/tools/mongodb/{ => metadata}/connect.ts (91%) rename src/tools/mongodb/{ => read}/collectionIndexes.ts (93%) rename tests/integration/tools/mongodb/{ => metadata}/connect.test.ts (63%) create mode 100644 tests/integration/tools/mongodb/metadata/listDatabases.test.ts diff --git a/jest.config.js b/jest.config.js index 88e80263..59baa966 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,4 +16,5 @@ export default { ], }, moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + setupFilesAfterEnv: ["jest-extended/all"], }; diff --git a/package-lock.json b/package-lock.json index f5e0be10..eb36763b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", + "jest-extended": "^4.0.2", "mongodb-runner": "^5.8.2", "openapi-types": "^12.1.3", "openapi-typescript": "^7.6.1", @@ -10050,6 +10051,28 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-extended": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", diff --git a/package.json b/package.json index bdbeed63..a4f6f264 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", + "jest-extended": "^4.0.2", "mongodb-runner": "^5.8.2", "openapi-types": "^12.1.3", "openapi-typescript": "^7.6.1", @@ -55,9 +56,9 @@ "typescript-eslint": "^8.29.1" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.8.0", "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", - "@modelcontextprotocol/sdk": "^1.8.0", "bson": "^6.10.3", "mongodb": "^6.15.0", "mongodb-log-writer": "^2.4.1", diff --git a/src/tools/mongodb/createIndex.ts b/src/tools/mongodb/create/createIndex.ts similarity index 94% rename from src/tools/mongodb/createIndex.ts rename to src/tools/mongodb/create/createIndex.ts index 30bc17af..455dc24f 100644 --- a/src/tools/mongodb/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; -import { ToolArgs } from "../tool.js"; +import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; +import { ToolArgs } from "../../tool.js"; import { IndexDirection } from "mongodb"; export class CreateIndexTool extends MongoDBToolBase { diff --git a/src/tools/mongodb/connect.ts b/src/tools/mongodb/metadata/connect.ts similarity index 91% rename from src/tools/mongodb/connect.ts rename to src/tools/mongodb/metadata/connect.ts index 66df62e3..408b31f8 100644 --- a/src/tools/mongodb/connect.ts +++ b/src/tools/mongodb/metadata/connect.ts @@ -1,9 +1,9 @@ import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; -import { ToolArgs } from "../tool.js"; -import { ErrorCodes, MongoDBError } from "../../errors.js"; -import config from "../../config.js"; +import { DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; +import { ToolArgs } from "../../tool.js"; +import { ErrorCodes, MongoDBError } from "../../../errors.js"; +import config from "../../../config.js"; export class ConnectTool extends MongoDBToolBase { protected name = "connect"; diff --git a/src/tools/mongodb/collectionIndexes.ts b/src/tools/mongodb/read/collectionIndexes.ts similarity index 93% rename from src/tools/mongodb/collectionIndexes.ts rename to src/tools/mongodb/read/collectionIndexes.ts index 4d8cae90..e3d4a0e9 100644 --- a/src/tools/mongodb/collectionIndexes.ts +++ b/src/tools/mongodb/read/collectionIndexes.ts @@ -1,6 +1,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; -import { ToolArgs } from "../tool.js"; +import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; +import { ToolArgs } from "../../tool.js"; export class CollectionIndexesTool extends MongoDBToolBase { protected name = "collection-indexes"; diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index ac22e095..d6627e74 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -1,8 +1,8 @@ -import { ConnectTool } from "./connect.js"; +import { ConnectTool } from "./metadata/connect.js"; import { ListCollectionsTool } from "./metadata/listCollections.js"; -import { CollectionIndexesTool } from "./collectionIndexes.js"; +import { CollectionIndexesTool } from "./read/collectionIndexes.js"; import { ListDatabasesTool } from "./metadata/listDatabases.js"; -import { CreateIndexTool } from "./createIndex.js"; +import { CreateIndexTool } from "./create/createIndex.js"; import { CollectionSchemaTool } from "./metadata/collectionSchema.js"; import { InsertOneTool } from "./create/insertOne.js"; import { FindTool } from "./read/find.js"; diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 207492da..177c58f3 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -68,15 +68,28 @@ export async function runMongoDB(): Promise { } } -export function validateToolResponse(content: unknown): string { +export function getResponseContent(content: unknown): string { + return getResponseElements(content) + .map((item) => item.text) + .join("\n"); +} + +export function getResponseElements(content: unknown): { type: string; text: string }[] { expect(Array.isArray(content)).toBe(true); - const response = content as Array<{ type: string; text: string }>; + const response = content as { type: string; text: string }[]; for (const item of response) { expect(item).toHaveProperty("type"); expect(item).toHaveProperty("text"); expect(item.type).toBe("text"); } - return response.map((item) => item.text).join("\n"); + return response; +} + +export async function connect(client: Client, cluster: runner.MongoCluster): Promise { + await client.callTool({ + name: "connect", + arguments: { connectionStringOrClusterName: cluster.connectionString }, + }); } diff --git a/tests/integration/tools/mongodb/connect.test.ts b/tests/integration/tools/mongodb/metadata/connect.test.ts similarity index 63% rename from tests/integration/tools/mongodb/connect.test.ts rename to tests/integration/tools/mongodb/metadata/connect.test.ts index 7e5dacec..98676666 100644 --- a/tests/integration/tools/mongodb/connect.test.ts +++ b/tests/integration/tools/mongodb/metadata/connect.test.ts @@ -1,8 +1,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { runMongoDB, setupIntegrationTest, validateToolResponse } from "../../helpers.js"; +import { runMongoDB, setupIntegrationTest, getResponseContent } from "../../../helpers.js"; import runner from "mongodb-runner"; -import config from "../../../../src/config.js"; +import config from "../../../../../src/config.js"; describe("Connect tool", () => { let client: Client; @@ -26,32 +26,32 @@ describe("Connect tool", () => { await cluster.close(); }); - describe("with default config", () => { - it("should have correct metadata", async () => { - const tools = await client.listTools(); - const connectTool = tools.tools.find((tool) => tool.name === "connect"); - expect(connectTool).toBeDefined(); - expect(connectTool!.description).toBe("Connect to a MongoDB instance"); - expect(connectTool!.inputSchema.type).toBe("object"); - expect(connectTool!.inputSchema.properties).toBeDefined(); - - const propertyNames = Object.keys(connectTool!.inputSchema.properties!); - expect(propertyNames).toHaveLength(1); - expect(propertyNames[0]).toBe("connectionStringOrClusterName"); - - const connectionStringOrClusterNameProp = connectTool!.inputSchema.properties![propertyNames[0]] as { - type: string; - description: string; - }; - expect(connectionStringOrClusterNameProp.type).toBe("string"); - expect(connectionStringOrClusterNameProp.description).toContain("MongoDB connection string"); - expect(connectionStringOrClusterNameProp.description).toContain("cluster name"); - }); + it("should have correct metadata", async () => { + const tools = await client.listTools(); + const connectTool = tools.tools.find((tool) => tool.name === "connect"); + expect(connectTool).toBeDefined(); + expect(connectTool!.description).toBe("Connect to a MongoDB instance"); + expect(connectTool!.inputSchema.type).toBe("object"); + expect(connectTool!.inputSchema.properties).toBeDefined(); + + const propertyNames = Object.keys(connectTool!.inputSchema.properties!); + expect(propertyNames).toHaveLength(1); + expect(propertyNames[0]).toBe("connectionStringOrClusterName"); + + const connectionStringOrClusterNameProp = connectTool!.inputSchema.properties![propertyNames[0]] as { + type: string; + description: string; + }; + expect(connectionStringOrClusterNameProp.type).toBe("string"); + expect(connectionStringOrClusterNameProp.description).toContain("MongoDB connection string"); + expect(connectionStringOrClusterNameProp.description).toContain("cluster name"); + }); + describe("with default config", () => { describe("without connection string", () => { it("prompts for connection string", async () => { const response = await client.callTool({ name: "connect", arguments: {} }); - const content = validateToolResponse(response.content); + const content = getResponseContent(response.content); expect(content).toContain("No connection details provided"); expect(content).toContain("mongodb://localhost:27017"); }); @@ -63,7 +63,7 @@ describe("Connect tool", () => { name: "connect", arguments: { connectionStringOrClusterName: cluster.connectionString }, }); - const content = validateToolResponse(response.content); + const content = getResponseContent(response.content); expect(content).toContain("Successfully connected"); expect(content).toContain(cluster.connectionString); }); @@ -75,7 +75,7 @@ describe("Connect tool", () => { name: "connect", arguments: { connectionStringOrClusterName: "mongodb://localhost:12345" }, }); - const content = validateToolResponse(response.content); + const content = getResponseContent(response.content); expect(content).toContain("Error running connect"); }); }); @@ -88,7 +88,7 @@ describe("Connect tool", () => { it("uses the connection string from config", async () => { const response = await client.callTool({ name: "connect", arguments: {} }); - const content = validateToolResponse(response.content); + const content = getResponseContent(response.content); expect(content).toContain("Successfully connected"); expect(content).toContain(cluster.connectionString); }); @@ -99,7 +99,7 @@ describe("Connect tool", () => { name: "connect", arguments: { connectionStringOrClusterName: newConnectionString }, }); - const content = validateToolResponse(response.content); + const content = getResponseContent(response.content); expect(content).toContain("Successfully connected"); expect(content).toContain(newConnectionString); }); diff --git a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts new file mode 100644 index 00000000..337e1d96 --- /dev/null +++ b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts @@ -0,0 +1,74 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { runMongoDB, setupIntegrationTest, getResponseElements, connect } from "../../../helpers.js"; +import runner from "mongodb-runner"; +import { MongoClient } from "mongodb"; +import { toIncludeSameMembers } from "jest-extended"; + +describe("listDatabases tool", () => { + let client: Client; + let serverClientTeardown: () => Promise; + + let cluster: runner.MongoCluster; + + beforeAll(async () => { + cluster = await runMongoDB(); + }, 60_000); + + beforeEach(async () => { + ({ client, teardown: serverClientTeardown } = await setupIntegrationTest()); + }); + + afterEach(async () => { + await serverClientTeardown?.(); + }); + + afterAll(async () => { + await cluster.close(); + }); + + it("should have correct metadata", async () => { + const tools = await client.listTools(); + const listDatabases = tools.tools.find((tool) => tool.name === "list-databases"); + expect(listDatabases).toBeDefined(); + expect(listDatabases!.description).toBe("List all databases for a MongoDB connection"); + expect(listDatabases!.inputSchema.type).toBe("object"); + expect(listDatabases!.inputSchema.properties).toBeDefined(); + + const propertyNames = Object.keys(listDatabases!.inputSchema.properties!); + expect(propertyNames).toHaveLength(0); + }); + + describe("with no preexisting databases", () => { + it("returns only the system databases", async () => { + await connect(client, cluster); + const response = await client.callTool({ name: "list-databases", arguments: {} }); + const dbNames = getDbNames(response.content); + + expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]); + }); + }); + + describe("with preexisting databases", () => { + it("returns their names and sizes", async () => { + const mongoClient = new MongoClient(cluster.connectionString); + await mongoClient.db("foo").collection("bar").insertOne({ test: "test" }); + await mongoClient.db("baz").collection("qux").insertOne({ test: "test" }); + await mongoClient.close(); + + await connect(client, cluster); + + const response = await client.callTool({ name: "list-databases", arguments: {} }); + const dbNames = getDbNames(response.content); + expect(dbNames).toIncludeSameMembers(["admin", "config", "local", "foo", "baz"]); + }); + }); +}); + +function getDbNames(content: unknown): (string | null)[] { + const responseItems = getResponseElements(content); + + return responseItems.map((item) => { + const match = item.text.match(/Name: (.*), Size: \d+ bytes/); + return match ? match[1] : null; + }); +} From fe92d1c50c4ecd32db9d785a898f82bdc7e18b2c Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 14 Apr 2025 13:04:38 +0200 Subject: [PATCH 2/3] PR feedback --- .../tools/mongodb/metadata/connect.test.ts | 14 +++++++------- .../tools/mongodb/metadata/listDatabases.test.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/integration/tools/mongodb/metadata/connect.test.ts b/tests/integration/tools/mongodb/metadata/connect.test.ts index 98676666..2e1ad0a8 100644 --- a/tests/integration/tools/mongodb/metadata/connect.test.ts +++ b/tests/integration/tools/mongodb/metadata/connect.test.ts @@ -27,18 +27,18 @@ describe("Connect tool", () => { }); it("should have correct metadata", async () => { - const tools = await client.listTools(); - const connectTool = tools.tools.find((tool) => tool.name === "connect"); + const { tools } = await client.listTools(); + const connectTool = tools.find((tool) => tool.name === "connect")!; expect(connectTool).toBeDefined(); - expect(connectTool!.description).toBe("Connect to a MongoDB instance"); - expect(connectTool!.inputSchema.type).toBe("object"); - expect(connectTool!.inputSchema.properties).toBeDefined(); + expect(connectTool.description).toBe("Connect to a MongoDB instance"); + expect(connectTool.inputSchema.type).toBe("object"); + expect(connectTool.inputSchema.properties).toBeDefined(); - const propertyNames = Object.keys(connectTool!.inputSchema.properties!); + const propertyNames = Object.keys(connectTool.inputSchema.properties!); expect(propertyNames).toHaveLength(1); expect(propertyNames[0]).toBe("connectionStringOrClusterName"); - const connectionStringOrClusterNameProp = connectTool!.inputSchema.properties![propertyNames[0]] as { + const connectionStringOrClusterNameProp = connectTool.inputSchema.properties![propertyNames[0]] as { type: string; description: string; }; diff --git a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts index 337e1d96..4a64f3d6 100644 --- a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +++ b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts @@ -27,14 +27,14 @@ describe("listDatabases tool", () => { }); it("should have correct metadata", async () => { - const tools = await client.listTools(); - const listDatabases = tools.tools.find((tool) => tool.name === "list-databases"); + const { tools } = await client.listTools(); + const listDatabases = tools.find((tool) => tool.name === "list-databases")!; expect(listDatabases).toBeDefined(); - expect(listDatabases!.description).toBe("List all databases for a MongoDB connection"); - expect(listDatabases!.inputSchema.type).toBe("object"); - expect(listDatabases!.inputSchema.properties).toBeDefined(); + expect(listDatabases.description).toBe("List all databases for a MongoDB connection"); + expect(listDatabases.inputSchema.type).toBe("object"); + expect(listDatabases.inputSchema.properties).toBeDefined(); - const propertyNames = Object.keys(listDatabases!.inputSchema.properties!); + const propertyNames = Object.keys(listDatabases.inputSchema.properties!); expect(propertyNames).toHaveLength(0); }); From 6f330a3637926c383e716da09a176a7d37f6a2f7 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 14 Apr 2025 13:43:02 +0200 Subject: [PATCH 3/3] Use hooks, clean up test setup a little --- tests/integration/helpers.ts | 139 +++++++++++------- tests/integration/server.test.ts | 22 +-- .../tools/mongodb/metadata/connect.test.ts | 48 ++---- .../mongodb/metadata/listDatabases.test.ts | 38 ++--- 4 files changed, 116 insertions(+), 131 deletions(-) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 177c58f3..11963fc2 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -7,65 +7,100 @@ import fs from "fs/promises"; import { Session } from "../../src/session.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -export async function setupIntegrationTest(): Promise<{ - client: Client; - server: Server; - teardown: () => Promise; -}> { - const clientTransport = new InMemoryTransport(); - const serverTransport = new InMemoryTransport(); - - await serverTransport.start(); - await clientTransport.start(); - - clientTransport.output.pipeTo(serverTransport.input); - serverTransport.output.pipeTo(clientTransport.input); - - const client = new Client( - { - name: "test-client", - version: "1.2.3", - }, - { - capabilities: {}, - } - ); - - const server = new Server({ - mcpServer: new McpServer({ - name: "test-server", - version: "1.2.3", - }), - session: new Session(), +export function jestTestMCPClient(): () => Client { + let client: Client | undefined; + let server: Server | undefined; + + beforeEach(async () => { + const clientTransport = new InMemoryTransport(); + const serverTransport = new InMemoryTransport(); + + await serverTransport.start(); + await clientTransport.start(); + + clientTransport.output.pipeTo(serverTransport.input); + serverTransport.output.pipeTo(clientTransport.input); + + client = new Client( + { + name: "test-client", + version: "1.2.3", + }, + { + capabilities: {}, + } + ); + + server = new Server({ + mcpServer: new McpServer({ + name: "test-server", + version: "1.2.3", + }), + session: new Session(), + }); + await server.connect(serverTransport); + await client.connect(clientTransport); + }); + + afterEach(async () => { + await client?.close(); + client = undefined; + + await server?.close(); + server = undefined; }); - await server.connect(serverTransport); - await client.connect(clientTransport); - - return { - client, - server, - teardown: async () => { - await client.close(); - await server.close(); - }, + + return () => { + if (!client) { + throw new Error("beforeEach() hook not ran yet"); + } + + return client; }; } -export async function runMongoDB(): Promise { - const tmpDir = path.join(__dirname, "..", "tmp"); - await fs.mkdir(tmpDir, { recursive: true }); +export function jestTestCluster(): () => runner.MongoCluster { + let cluster: runner.MongoCluster | undefined; - try { - const cluster = await MongoCluster.start({ - tmpDir: path.join(tmpDir, "mongodb-runner", "dbs"), - logDir: path.join(tmpDir, "mongodb-runner", "logs"), - topology: "standalone", - }); + function runMongodb() {} + + beforeAll(async function () { + // Downloading Windows executables in CI takes a long time because + // they include debug symbols... + const tmpDir = path.join(__dirname, "..", "tmp"); + await fs.mkdir(tmpDir, { recursive: true }); + + // On Windows, we may have a situation where mongod.exe is not fully released by the OS + // before we attempt to run it again, so we add a retry. + const dbsDir = path.join(tmpDir, "mongodb-runner", `dbs`); + for (let i = 0; i < 10; i++) { + try { + cluster = await MongoCluster.start({ + tmpDir: dbsDir, + logDir: path.join(tmpDir, "mongodb-runner", "logs"), + topology: "standalone", + }); + + return; + } catch (err) { + console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + }, 120_000); + + afterAll(async function () { + await cluster?.close(); + cluster = undefined; + }); + + return () => { + if (!cluster) { + throw new Error("beforeAll() hook not ran yet"); + } return cluster; - } catch (err) { - throw err; - } + }; } export function getResponseContent(content: unknown): string { diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index 1d804943..572c6711 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -1,39 +1,29 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { setupIntegrationTest } from "./helpers.js"; +import { jestTestMCPClient } from "./helpers.js"; describe("Server integration test", () => { - let client: Client; - let teardown: () => Promise; - - beforeEach(async () => { - ({ client, teardown } = await setupIntegrationTest()); - }); - - afterEach(async () => { - await teardown(); - }); + const client = jestTestMCPClient(); describe("list capabilities", () => { it("should return positive number of tools", async () => { - const tools = await client.listTools(); + const tools = await client().listTools(); expect(tools).toBeDefined(); expect(tools.tools.length).toBeGreaterThan(0); }); it("should return no resources", async () => { - await expect(() => client.listResources()).rejects.toMatchObject({ + await expect(() => client().listResources()).rejects.toMatchObject({ message: "MCP error -32601: Method not found", }); }); it("should return no prompts", async () => { - await expect(() => client.listPrompts()).rejects.toMatchObject({ + await expect(() => client().listPrompts()).rejects.toMatchObject({ message: "MCP error -32601: Method not found", }); }); it("should return capabilities", async () => { - const capabilities = client.getServerCapabilities(); + const capabilities = client().getServerCapabilities(); expect(capabilities).toBeDefined(); expect(capabilities?.completions).toBeUndefined(); expect(capabilities?.experimental).toBeUndefined(); diff --git a/tests/integration/tools/mongodb/metadata/connect.test.ts b/tests/integration/tools/mongodb/metadata/connect.test.ts index 2e1ad0a8..2030b9dd 100644 --- a/tests/integration/tools/mongodb/metadata/connect.test.ts +++ b/tests/integration/tools/mongodb/metadata/connect.test.ts @@ -1,33 +1,13 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { runMongoDB, setupIntegrationTest, getResponseContent } from "../../../helpers.js"; -import runner from "mongodb-runner"; +import { getResponseContent, jestTestMCPClient, jestTestCluster } from "../../../helpers.js"; import config from "../../../../../src/config.js"; describe("Connect tool", () => { - let client: Client; - let serverClientTeardown: () => Promise; - - let cluster: runner.MongoCluster; - - beforeAll(async () => { - cluster = await runMongoDB(); - }, 60_000); - - beforeEach(async () => { - ({ client, teardown: serverClientTeardown } = await setupIntegrationTest()); - }); - - afterEach(async () => { - await serverClientTeardown?.(); - }); - - afterAll(async () => { - await cluster.close(); - }); + const client = jestTestMCPClient(); + const cluster = jestTestCluster(); it("should have correct metadata", async () => { - const { tools } = await client.listTools(); + const { tools } = await client().listTools(); const connectTool = tools.find((tool) => tool.name === "connect")!; expect(connectTool).toBeDefined(); expect(connectTool.description).toBe("Connect to a MongoDB instance"); @@ -50,7 +30,7 @@ describe("Connect tool", () => { describe("with default config", () => { describe("without connection string", () => { it("prompts for connection string", async () => { - const response = await client.callTool({ name: "connect", arguments: {} }); + const response = await client().callTool({ name: "connect", arguments: {} }); const content = getResponseContent(response.content); expect(content).toContain("No connection details provided"); expect(content).toContain("mongodb://localhost:27017"); @@ -59,19 +39,19 @@ describe("Connect tool", () => { describe("with connection string", () => { it("connects to the database", async () => { - const response = await client.callTool({ + const response = await client().callTool({ name: "connect", - arguments: { connectionStringOrClusterName: cluster.connectionString }, + arguments: { connectionStringOrClusterName: cluster().connectionString }, }); const content = getResponseContent(response.content); expect(content).toContain("Successfully connected"); - expect(content).toContain(cluster.connectionString); + expect(content).toContain(cluster().connectionString); }); }); describe("with invalid connection string", () => { it("returns error message", async () => { - const response = await client.callTool({ + const response = await client().callTool({ name: "connect", arguments: { connectionStringOrClusterName: "mongodb://localhost:12345" }, }); @@ -83,19 +63,19 @@ describe("Connect tool", () => { describe("with connection string in config", () => { beforeEach(async () => { - config.connectionString = cluster.connectionString; + config.connectionString = cluster().connectionString; }); it("uses the connection string from config", async () => { - const response = await client.callTool({ name: "connect", arguments: {} }); + const response = await client().callTool({ name: "connect", arguments: {} }); const content = getResponseContent(response.content); expect(content).toContain("Successfully connected"); - expect(content).toContain(cluster.connectionString); + expect(content).toContain(cluster().connectionString); }); it("prefers connection string from arguments", async () => { - const newConnectionString = `${cluster.connectionString}?appName=foo-bar`; - const response = await client.callTool({ + const newConnectionString = `${cluster().connectionString}?appName=foo-bar`; + const response = await client().callTool({ name: "connect", arguments: { connectionStringOrClusterName: newConnectionString }, }); diff --git a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts index 4a64f3d6..153eca13 100644 --- a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +++ b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts @@ -1,33 +1,13 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { runMongoDB, setupIntegrationTest, getResponseElements, connect } from "../../../helpers.js"; -import runner from "mongodb-runner"; +import { getResponseElements, connect, jestTestCluster, jestTestMCPClient } from "../../../helpers.js"; import { MongoClient } from "mongodb"; import { toIncludeSameMembers } from "jest-extended"; describe("listDatabases tool", () => { - let client: Client; - let serverClientTeardown: () => Promise; - - let cluster: runner.MongoCluster; - - beforeAll(async () => { - cluster = await runMongoDB(); - }, 60_000); - - beforeEach(async () => { - ({ client, teardown: serverClientTeardown } = await setupIntegrationTest()); - }); - - afterEach(async () => { - await serverClientTeardown?.(); - }); - - afterAll(async () => { - await cluster.close(); - }); + const client = jestTestMCPClient(); + const cluster = jestTestCluster(); it("should have correct metadata", async () => { - const { tools } = await client.listTools(); + const { tools } = await client().listTools(); const listDatabases = tools.find((tool) => tool.name === "list-databases")!; expect(listDatabases).toBeDefined(); expect(listDatabases.description).toBe("List all databases for a MongoDB connection"); @@ -40,8 +20,8 @@ describe("listDatabases tool", () => { describe("with no preexisting databases", () => { it("returns only the system databases", async () => { - await connect(client, cluster); - const response = await client.callTool({ name: "list-databases", arguments: {} }); + await connect(client(), cluster()); + const response = await client().callTool({ name: "list-databases", arguments: {} }); const dbNames = getDbNames(response.content); expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]); @@ -50,14 +30,14 @@ describe("listDatabases tool", () => { describe("with preexisting databases", () => { it("returns their names and sizes", async () => { - const mongoClient = new MongoClient(cluster.connectionString); + const mongoClient = new MongoClient(cluster().connectionString); await mongoClient.db("foo").collection("bar").insertOne({ test: "test" }); await mongoClient.db("baz").collection("qux").insertOne({ test: "test" }); await mongoClient.close(); - await connect(client, cluster); + await connect(client(), cluster()); - const response = await client.callTool({ name: "list-databases", arguments: {} }); + const response = await client().callTool({ name: "list-databases", arguments: {} }); const dbNames = getDbNames(response.content); expect(dbNames).toIncludeSameMembers(["admin", "config", "local", "foo", "baz"]); });