Skip to content

Commit c692587

Browse files
authored
chore: add integration tests for list-databases (#74)
1 parent f12d8aa commit c692587

File tree

12 files changed

+287
-190
lines changed

12 files changed

+287
-190
lines changed

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export default {
1616
],
1717
},
1818
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
19+
setupFilesAfterEnv: ["jest-extended/all"],
1920
};

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"globals": "^16.0.0",
4646
"jest": "^29.7.0",
4747
"jest-environment-node": "^29.7.0",
48+
"jest-extended": "^4.0.2",
4849
"mongodb-runner": "^5.8.2",
4950
"openapi-types": "^12.1.3",
5051
"openapi-typescript": "^7.6.1",
@@ -55,9 +56,9 @@
5556
"typescript-eslint": "^8.29.1"
5657
},
5758
"dependencies": {
59+
"@modelcontextprotocol/sdk": "^1.8.0",
5860
"@mongodb-js/devtools-connect": "^3.7.2",
5961
"@mongosh/service-provider-node-driver": "^3.6.0",
60-
"@modelcontextprotocol/sdk": "^1.8.0",
6162
"bson": "^6.10.3",
6263
"mongodb": "^6.15.0",
6364
"mongodb-log-writer": "^2.4.1",

src/tools/mongodb/createIndex.ts renamed to src/tools/mongodb/create/createIndex.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js";
4-
import { ToolArgs } from "../tool.js";
3+
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
4+
import { ToolArgs } from "../../tool.js";
55
import { IndexDirection } from "mongodb";
66

77
export class CreateIndexTool extends MongoDBToolBase {

src/tools/mongodb/connect.ts renamed to src/tools/mongodb/metadata/connect.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { DbOperationType, MongoDBToolBase } from "./mongodbTool.js";
4-
import { ToolArgs } from "../tool.js";
5-
import { ErrorCodes, MongoDBError } from "../../errors.js";
6-
import config from "../../config.js";
3+
import { DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
4+
import { ToolArgs } from "../../tool.js";
5+
import { ErrorCodes, MongoDBError } from "../../../errors.js";
6+
import config from "../../../config.js";
77

88
export class ConnectTool extends MongoDBToolBase {
99
protected name = "connect";

src/tools/mongodb/collectionIndexes.ts renamed to src/tools/mongodb/read/collectionIndexes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2-
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js";
3-
import { ToolArgs } from "../tool.js";
2+
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
3+
import { ToolArgs } from "../../tool.js";
44

55
export class CollectionIndexesTool extends MongoDBToolBase {
66
protected name = "collection-indexes";

src/tools/mongodb/tools.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { ConnectTool } from "./connect.js";
1+
import { ConnectTool } from "./metadata/connect.js";
22
import { ListCollectionsTool } from "./metadata/listCollections.js";
3-
import { CollectionIndexesTool } from "./collectionIndexes.js";
3+
import { CollectionIndexesTool } from "./read/collectionIndexes.js";
44
import { ListDatabasesTool } from "./metadata/listDatabases.js";
5-
import { CreateIndexTool } from "./createIndex.js";
5+
import { CreateIndexTool } from "./create/createIndex.js";
66
import { CollectionSchemaTool } from "./metadata/collectionSchema.js";
77
import { InsertOneTool } from "./create/insertOne.js";
88
import { FindTool } from "./read/find.js";

tests/integration/helpers.ts

Lines changed: 103 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,76 +7,124 @@ import fs from "fs/promises";
77
import { Session } from "../../src/session.js";
88
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
99

10-
export async function setupIntegrationTest(): Promise<{
11-
client: Client;
12-
server: Server;
13-
teardown: () => Promise<void>;
14-
}> {
15-
const clientTransport = new InMemoryTransport();
16-
const serverTransport = new InMemoryTransport();
17-
18-
await serverTransport.start();
19-
await clientTransport.start();
20-
21-
clientTransport.output.pipeTo(serverTransport.input);
22-
serverTransport.output.pipeTo(clientTransport.input);
23-
24-
const client = new Client(
25-
{
26-
name: "test-client",
27-
version: "1.2.3",
28-
},
29-
{
30-
capabilities: {},
31-
}
32-
);
33-
34-
const server = new Server({
35-
mcpServer: new McpServer({
36-
name: "test-server",
37-
version: "1.2.3",
38-
}),
39-
session: new Session(),
10+
export function jestTestMCPClient(): () => Client {
11+
let client: Client | undefined;
12+
let server: Server | undefined;
13+
14+
beforeEach(async () => {
15+
const clientTransport = new InMemoryTransport();
16+
const serverTransport = new InMemoryTransport();
17+
18+
await serverTransport.start();
19+
await clientTransport.start();
20+
21+
clientTransport.output.pipeTo(serverTransport.input);
22+
serverTransport.output.pipeTo(clientTransport.input);
23+
24+
client = new Client(
25+
{
26+
name: "test-client",
27+
version: "1.2.3",
28+
},
29+
{
30+
capabilities: {},
31+
}
32+
);
33+
34+
server = new Server({
35+
mcpServer: new McpServer({
36+
name: "test-server",
37+
version: "1.2.3",
38+
}),
39+
session: new Session(),
40+
});
41+
await server.connect(serverTransport);
42+
await client.connect(clientTransport);
4043
});
41-
await server.connect(serverTransport);
42-
await client.connect(clientTransport);
43-
44-
return {
45-
client,
46-
server,
47-
teardown: async () => {
48-
await client.close();
49-
await server.close();
50-
},
44+
45+
afterEach(async () => {
46+
await client?.close();
47+
client = undefined;
48+
49+
await server?.close();
50+
server = undefined;
51+
});
52+
53+
return () => {
54+
if (!client) {
55+
throw new Error("beforeEach() hook not ran yet");
56+
}
57+
58+
return client;
5159
};
5260
}
5361

54-
export async function runMongoDB(): Promise<runner.MongoCluster> {
55-
const tmpDir = path.join(__dirname, "..", "tmp");
56-
await fs.mkdir(tmpDir, { recursive: true });
62+
export function jestTestCluster(): () => runner.MongoCluster {
63+
let cluster: runner.MongoCluster | undefined;
5764

58-
try {
59-
const cluster = await MongoCluster.start({
60-
tmpDir: path.join(tmpDir, "mongodb-runner", "dbs"),
61-
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
62-
topology: "standalone",
63-
});
65+
function runMongodb() {}
66+
67+
beforeAll(async function () {
68+
// Downloading Windows executables in CI takes a long time because
69+
// they include debug symbols...
70+
const tmpDir = path.join(__dirname, "..", "tmp");
71+
await fs.mkdir(tmpDir, { recursive: true });
72+
73+
// On Windows, we may have a situation where mongod.exe is not fully released by the OS
74+
// before we attempt to run it again, so we add a retry.
75+
const dbsDir = path.join(tmpDir, "mongodb-runner", `dbs`);
76+
for (let i = 0; i < 10; i++) {
77+
try {
78+
cluster = await MongoCluster.start({
79+
tmpDir: dbsDir,
80+
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
81+
topology: "standalone",
82+
});
83+
84+
return;
85+
} catch (err) {
86+
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
87+
await new Promise((resolve) => setTimeout(resolve, 1000));
88+
}
89+
}
90+
}, 120_000);
91+
92+
afterAll(async function () {
93+
await cluster?.close();
94+
cluster = undefined;
95+
});
96+
97+
return () => {
98+
if (!cluster) {
99+
throw new Error("beforeAll() hook not ran yet");
100+
}
64101

65102
return cluster;
66-
} catch (err) {
67-
throw err;
68-
}
103+
};
104+
}
105+
106+
export function getResponseContent(content: unknown): string {
107+
return getResponseElements(content)
108+
.map((item) => item.text)
109+
.join("\n");
69110
}
70111

71-
export function validateToolResponse(content: unknown): string {
112+
export function getResponseElements(content: unknown): { type: string; text: string }[] {
72113
expect(Array.isArray(content)).toBe(true);
73114

74-
const response = content as Array<{ type: string; text: string }>;
115+
const response = content as { type: string; text: string }[];
75116
for (const item of response) {
76117
expect(item).toHaveProperty("type");
77118
expect(item).toHaveProperty("text");
78119
expect(item.type).toBe("text");
79120
}
80121

81-
return response.map((item) => item.text).join("\n");
122+
return response;
123+
}
124+
125+
export async function connect(client: Client, cluster: runner.MongoCluster): Promise<void> {
126+
await client.callTool({
127+
name: "connect",
128+
arguments: { connectionStringOrClusterName: cluster.connectionString },
129+
});
82130
}

tests/integration/server.test.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,29 @@
1-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import { setupIntegrationTest } from "./helpers.js";
1+
import { jestTestMCPClient } from "./helpers.js";
32

43
describe("Server integration test", () => {
5-
let client: Client;
6-
let teardown: () => Promise<void>;
7-
8-
beforeEach(async () => {
9-
({ client, teardown } = await setupIntegrationTest());
10-
});
11-
12-
afterEach(async () => {
13-
await teardown();
14-
});
4+
const client = jestTestMCPClient();
155

166
describe("list capabilities", () => {
177
it("should return positive number of tools", async () => {
18-
const tools = await client.listTools();
8+
const tools = await client().listTools();
199
expect(tools).toBeDefined();
2010
expect(tools.tools.length).toBeGreaterThan(0);
2111
});
2212

2313
it("should return no resources", async () => {
24-
await expect(() => client.listResources()).rejects.toMatchObject({
14+
await expect(() => client().listResources()).rejects.toMatchObject({
2515
message: "MCP error -32601: Method not found",
2616
});
2717
});
2818

2919
it("should return no prompts", async () => {
30-
await expect(() => client.listPrompts()).rejects.toMatchObject({
20+
await expect(() => client().listPrompts()).rejects.toMatchObject({
3121
message: "MCP error -32601: Method not found",
3222
});
3323
});
3424

3525
it("should return capabilities", async () => {
36-
const capabilities = client.getServerCapabilities();
26+
const capabilities = client().getServerCapabilities();
3727
expect(capabilities).toBeDefined();
3828
expect(capabilities?.completions).toBeUndefined();
3929
expect(capabilities?.experimental).toBeUndefined();

0 commit comments

Comments
 (0)