Skip to content

chore: add integration tests for list-databases #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export default {
],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
setupFilesAfterEnv: ["jest-extended/all"],
};
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
6 changes: 3 additions & 3 deletions src/tools/mongodb/tools.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
158 changes: 103 additions & 55 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,76 +7,124 @@ 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<void>;
}> {
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);
});
await server.connect(serverTransport);
await client.connect(clientTransport);

return {
client,
server,
teardown: async () => {
await client.close();
await server.close();
},

afterEach(async () => {
await client?.close();
client = undefined;

await server?.close();
server = undefined;
});

return () => {
if (!client) {
throw new Error("beforeEach() hook not ran yet");
}

return client;
};
}

export async function runMongoDB(): Promise<runner.MongoCluster> {
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 {
return getResponseElements(content)
.map((item) => item.text)
.join("\n");
}

export function validateToolResponse(content: unknown): string {
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<void> {
await client.callTool({
name: "connect",
arguments: { connectionStringOrClusterName: cluster.connectionString },
});
}
22 changes: 6 additions & 16 deletions tests/integration/server.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>;

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();
Expand Down
Loading
Loading