Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 16 additions & 3 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,28 @@ export async function runMongoDB(): Promise<runner.MongoCluster> {
}
}

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<void> {
await client.callTool({
name: "connect",
arguments: { connectionStringOrClusterName: cluster.connectionString },
});
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
});
Expand All @@ -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);
});
Expand All @@ -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");
});
});
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand Down
74 changes: 74 additions & 0 deletions tests/integration/tools/mongodb/metadata/listDatabases.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>;

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;
});
}
Loading