diff --git a/src/tools/mongodb/create/createSearchIndex.ts b/src/tools/mongodb/create/createSearchIndex.ts new file mode 100644 index 00000000..5d727dbb --- /dev/null +++ b/src/tools/mongodb/create/createSearchIndex.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; +import { ToolArgs, OperationType } from "../../tool.js"; + +export class CreateSearchIndexTool extends MongoDBToolBase { + protected name = "create-search-index"; + protected description = "Create an Atlas Search index for a collection"; + protected argsShape = { + ...DbOperationArgs, + name: z.string().optional().describe("The name of the index"), + type: z.enum(["search", "vectorSearch"]).optional().default("search").describe("The type of the index"), + analyzer: z + .string() + .optional() + .default("lucene.standard") + .describe( + "The analyzer to use for the index. Can be one of the built-in lucene analyzers (`lucene.standard`, `lucene.simple`, `lucene.whitespace`, `lucene.keyword`), a language-specific analyzer, such as `lucene.cjk` or `lucene.czech`, or a custom analyzer defined in the Atlas UI." + ), + mappings: z.object({ + dynamic: z + .boolean() + .optional() + .default(false) + .describe( + "Enables or disables dynamic mapping of fields for this index. If set to true, Atlas Search recursively indexes all dynamically indexable fields. If set to false, you must specify individual fields to index using mappings.fields." + ), + fields: z + .record( + z.string().describe("The field name"), + z + .object({ + type: z + .enum([ + "autocomplete", + "boolean", + "date", + "document", + "embeddedDocuments", + "geo", + "knnVector", + "number", + "objectId", + "string", + "token", + "uuid", + ]) + .describe("The field type"), + }) + .passthrough() + + .describe( + "The field index definition. It must contain the field type, as well as any additional options for that field type." + ) + ) + .optional() + .describe("The field mapping definitions. If `dynamic` is set to false, this is required."), + }), + }; + + protected operationType: OperationType = "create"; + + protected async execute({ + database, + collection, + name, + type, + analyzer, + mappings, + }: ToolArgs): Promise { + const provider = await this.ensureConnected(); + const indexes = await provider.createSearchIndexes(database, collection, [ + { + name, + type, + definition: { + analyzer, + mappings, + }, + }, + ]); + + return { + content: [ + { + text: `Created the index "${indexes[0]}" on collection "${collection}" in database "${database}"`, + type: "text", + }, + ], + }; + } +} diff --git a/src/tools/mongodb/delete/dropIndex.ts b/src/tools/mongodb/delete/dropIndex.ts new file mode 100644 index 00000000..2cfb703a --- /dev/null +++ b/src/tools/mongodb/delete/dropIndex.ts @@ -0,0 +1,57 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; +import { ToolArgs, OperationType } from "../../tool.js"; +import { z } from "zod"; +import { MongoServerError } from "mongodb"; + +export class DropIndexTool extends MongoDBToolBase { + protected name = "drop-index"; + protected description = "Removes an index from a collection."; + protected argsShape = { + ...DbOperationArgs, + name: z.string().describe("The name of the index to drop"), + }; + protected operationType: OperationType = "delete"; + + protected async execute({ database, collection, name }: ToolArgs): Promise { + const provider = await this.ensureConnected(); + await provider.mongoClient.db(database).collection(collection).dropIndex(name); + try { + await provider.dropSearchIndex(database, collection, name); + } catch (error) { + if (error instanceof MongoServerError && error.codeName === "SearchNotEnabled") { + // If search is not enabled (e.g. due to connecting to a non-Atlas cluster), we can ignore the error + // and return an empty array for search indexes. + } else { + throw error; + } + } + + return { + content: [ + { + text: `Successfully dropped index "${name}" in "${database}.${collection}"`, + type: "text", + }, + ], + }; + } + + protected handleError( + error: unknown, + args: ToolArgs + ): Promise | CallToolResult { + if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") { + return { + content: [ + { + text: `Cannot drop index "${args.name}" because the namespace "${args.database}.${args.collection}" does not exist.`, + type: "text", + }, + ], + }; + } + + return super.handleError(error, args); + } +} diff --git a/src/tools/mongodb/read/collectionIndexes.ts b/src/tools/mongodb/read/collectionIndexes.ts index cc0a141b..2303b6a0 100644 --- a/src/tools/mongodb/read/collectionIndexes.ts +++ b/src/tools/mongodb/read/collectionIndexes.ts @@ -1,6 +1,8 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import { ToolArgs, OperationType } from "../../tool.js"; +import { Document } from "bson"; +import { MongoServerError } from "mongodb"; export class CollectionIndexesTool extends MongoDBToolBase { protected name = "collection-indexes"; @@ -12,18 +14,37 @@ export class CollectionIndexesTool extends MongoDBToolBase { const provider = await this.ensureConnected(); const indexes = await provider.getIndexes(database, collection); + let searchIndexes: Document[]; + try { + searchIndexes = await provider.getSearchIndexes(database, collection); + } catch (error) { + if (error instanceof MongoServerError && error.codeName === "SearchNotEnabled") { + // If search is not enabled (e.g. due to connecting to a non-Atlas cluster), we can ignore the error + // and return an empty array for search indexes. + searchIndexes = []; + } else { + throw error; + } + } + return { content: [ { - text: `Found ${indexes.length} indexes in the collection "${collection}":`, + text: `Found ${indexes.length + searchIndexes.length} indexes in the collection "${collection}":`, type: "text", }, - ...(indexes.map((indexDefinition) => { + ...indexes.map((indexDefinition) => { return { text: `Name "${indexDefinition.name}", definition: ${JSON.stringify(indexDefinition.key)}`, type: "text", - }; - }) as { text: string; type: "text" }[]), + } as const; + }), + ...searchIndexes.map((indexDefinition) => { + return { + text: `Search index name: "${indexDefinition.name}", status: ${indexDefinition.status}, definition: ${JSON.stringify(indexDefinition.latestDefinition)}`, + type: "text", + } as const; + }), ], }; } diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index 523f45ca..40240bda 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -1,7 +1,7 @@ // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled // import { ConnectTool } from "./metadata/connect.js"; import { ListCollectionsTool } from "./metadata/listCollections.js"; -import { CollectionIndexesTool } from "./read/collectionIndexes.js"; +import { CollectionIndexesTool as ListIndexesTool } from "./read/collectionIndexes.js"; import { ListDatabasesTool } from "./metadata/listDatabases.js"; import { CreateIndexTool } from "./create/createIndex.js"; import { CollectionSchemaTool } from "./metadata/collectionSchema.js"; @@ -19,27 +19,35 @@ import { DropCollectionTool } from "./delete/dropCollection.js"; import { ExplainTool } from "./metadata/explain.js"; import { CreateCollectionTool } from "./create/createCollection.js"; import { LogsTool } from "./metadata/logs.js"; +import { CreateSearchIndexTool } from "./create/createSearchIndex.js"; +import { DropIndexTool } from "./delete/dropIndex.js"; export const MongoDbTools = [ // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled // ConnectTool, + CreateCollectionTool, ListCollectionsTool, - ListDatabasesTool, - CollectionIndexesTool, - CreateIndexTool, CollectionSchemaTool, - FindTool, - InsertManyTool, - DeleteManyTool, CollectionStorageSizeTool, - CountTool, - DbStatsTool, - AggregateTool, - UpdateManyTool, RenameCollectionTool, - DropDatabaseTool, DropCollectionTool, - ExplainTool, - CreateCollectionTool, + + ListDatabasesTool, + DropDatabaseTool, + DbStatsTool, LogsTool, + + FindTool, + AggregateTool, + CountTool, + ExplainTool, + + InsertManyTool, + DeleteManyTool, + UpdateManyTool, + + CreateIndexTool, + CreateSearchIndexTool, + ListIndexesTool, + DropIndexTool, ]; diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index bacc89b9..3b74b9f2 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -224,3 +224,7 @@ export function validateThrowsForInvalidArguments( export function expectDefined(arg: T): asserts arg is Exclude { expect(arg).toBeDefined(); } + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/tests/integration/tools/atlas-search/atlasSearchHelpers.ts b/tests/integration/tools/atlas-search/atlasSearchHelpers.ts new file mode 100644 index 00000000..5e1d2b25 --- /dev/null +++ b/tests/integration/tools/atlas-search/atlasSearchHelpers.ts @@ -0,0 +1,67 @@ +import { ObjectId } from "bson"; +import { defaultTestConfig, IntegrationTest, setupIntegrationTest } from "../../helpers.js"; +import { waitClusterState, withProject } from "../atlas/atlasHelpers.js"; + +export function describeWithAtlasSearch( + name: string, + fn: (integration: IntegrationTest & { connectMcpClient: () => Promise }) => void +): void { + const describeFn = + process.env.MDB_MCP_API_CLIENT_ID?.length && process.env.MDB_MCP_API_CLIENT_SECRET?.length + ? describe + : describe.skip; + + describeFn("atlas-search", () => { + const integration = setupIntegrationTest(() => ({ + ...defaultTestConfig, + apiClientId: process.env.MDB_MCP_API_CLIENT_ID, + apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET, + })); + + describe(name, () => { + withProject(integration, ({ getProjectId }) => { + const clusterName = `ClusterTest-${new ObjectId()}`; + beforeAll(async () => { + const projectId = getProjectId(); + + await integration.mcpClient().callTool({ + name: "atlas-create-free-cluster", + arguments: { + projectId, + name: clusterName, + region: "US_EAST_1", + }, + }); + + await waitClusterState(integration.mcpServer().session, projectId, clusterName, "IDLE"); + await integration.mcpServer().session.apiClient.createProjectIpAccessList({ + params: { + path: { + groupId: projectId, + }, + }, + body: [ + { + comment: "MCP test", + cidrBlock: "0.0.0.0/0", + }, + ], + }); + }); + + fn({ + ...integration, + connectMcpClient: async () => { + await integration.mcpClient().callTool({ + name: "atlas-connect-cluster", + arguments: { projectId: getProjectId(), clusterName }, + }); + + expect(integration.mcpServer().session.connectedAtlasCluster).toBeDefined(); + expect(integration.mcpServer().session.serviceProvider).toBeDefined(); + }, + }); + }); + }); + }); +} diff --git a/tests/integration/tools/atlas-search/read/collectionIndexes.test.ts b/tests/integration/tools/atlas-search/read/collectionIndexes.test.ts new file mode 100644 index 00000000..3c35a054 --- /dev/null +++ b/tests/integration/tools/atlas-search/read/collectionIndexes.test.ts @@ -0,0 +1,48 @@ +import { ObjectId } from "bson"; +import { expectDefined, getResponseElements } from "../../../helpers.js"; +import { describeWithAtlasSearch } from "../atlasSearchHelpers.js"; + +describeWithAtlasSearch("collectionIndexes tool", (integration) => { + it("can inspect search indexes", async () => { + await integration.connectMcpClient(); + + const provider = integration.mcpServer().session.serviceProvider; + expectDefined(provider); + + const database = new ObjectId().toString(); + + await provider.mongoClient + .db(database) + .collection("coll1") + .insertMany([ + { name: "Alice", age: 30 }, + { name: "Bob", age: 25 }, + { name: "Charlie", age: 35 }, + ]); + + const name = await provider.mongoClient + .db(database) + .collection("coll1") + .createSearchIndex({ + name: "searchIndex1", + definition: { + mappings: { + dynamic: true, + }, + analyzer: "lucene.danish", + }, + }); + + const response = await integration.mcpClient().callTool({ + name: "collection-indexes", + arguments: { database, collection: "coll1" }, + }); + + const elements = getResponseElements(response.content); + expect(elements).toHaveLength(3); + expect(elements[0].text).toEqual(`Found 2 indexes in the collection "coll1":`); + expect(elements[1].text).toEqual('Name "_id_", definition: {"_id":1}'); + expect(elements[2].text).toContain(`Search index name: "${name}"`); + expect(elements[2].text).toContain('"analyzer":"lucene.danish"'); + }); +}); diff --git a/tests/integration/tools/atlas/accessLists.test.ts b/tests/integration/tools/atlas/accessLists.test.ts index a194a351..b10ea4f2 100644 --- a/tests/integration/tools/atlas/accessLists.test.ts +++ b/tests/integration/tools/atlas/accessLists.test.ts @@ -22,23 +22,6 @@ describeWithAtlas("ip access lists", (integration) => { values.push(ipInfo.currentIpv4Address); }); - afterAll(async () => { - const apiClient = integration.mcpServer().session.apiClient; - - const projectId = getProjectId(); - - for (const value of values) { - await apiClient.deleteProjectIpAccessList({ - params: { - path: { - groupId: projectId, - entryValue: value, - }, - }, - }); - } - }); - describe("atlas-create-access-list", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index aecf0479..884b5371 100644 --- a/tests/integration/tools/atlas/atlasHelpers.ts +++ b/tests/integration/tools/atlas/atlasHelpers.ts @@ -1,11 +1,12 @@ import { ObjectId } from "mongodb"; import { Group } from "../../../../src/common/atlas/openapi.js"; import { ApiClient } from "../../../../src/common/atlas/apiClient.js"; -import { setupIntegrationTest, IntegrationTest, defaultTestConfig } from "../../helpers.js"; +import { setupIntegrationTest, IntegrationTest, defaultTestConfig, sleep } from "../../helpers.js"; +import { Session } from "../../../../src/session.js"; export type IntegrationTestFunction = (integration: IntegrationTest) => void; -export function describeWithAtlas(name: string, fn: IntegrationTestFunction) { +export function describeWithAtlas(name: string, fn: IntegrationTestFunction): void { const testDefinition = () => { const integration = setupIntegrationTest(() => ({ ...defaultTestConfig, @@ -21,7 +22,8 @@ export function describeWithAtlas(name: string, fn: IntegrationTestFunction) { if (!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length) { return describe.skip("atlas", testDefinition); } - return describe("atlas", testDefinition); + + describe("atlas", testDefinition); } interface ProjectTestArgs { @@ -30,20 +32,40 @@ interface ProjectTestArgs { type ProjectTestFunction = (args: ProjectTestArgs) => void; -export function withProject(integration: IntegrationTest, fn: ProjectTestFunction) { - return describe("project", () => { +export function withProject(integration: IntegrationTest, fn: ProjectTestFunction): void { + describe("with project", () => { let projectId: string = ""; + const projectName = `testProj-${new ObjectId()}`; beforeAll(async () => { const apiClient = integration.mcpServer().session.apiClient; - const group = await createProject(apiClient); + const group = await createProject(apiClient, projectName); projectId = group.id || ""; }); afterAll(async () => { const apiClient = integration.mcpServer().session.apiClient; + const clusters = await apiClient.listClusters({ + params: { + path: { + groupId: projectId, + }, + }, + }); + + const deletePromises = + clusters?.results?.map((cluster) => { + if (cluster.name) { + return deleteAndWaitCluster(integration.mcpServer().session, projectId, cluster.name); + } + + return Promise.resolve(); + }) ?? []; + + await Promise.all(deletePromises); + await apiClient.deleteProject({ params: { path: { @@ -53,12 +75,8 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio }); }); - const args = { + fn({ getProjectId: () => projectId, - }; - - describe("with project", () => { - fn(args); }); }); } @@ -81,11 +99,7 @@ export function parseTable(text: string): Record[] { }); } -export const randomId = new ObjectId().toString(); - -async function createProject(apiClient: ApiClient): Promise { - const projectName: string = `testProj-` + randomId; - +async function createProject(apiClient: ApiClient, projectName: string): Promise { const orgs = await apiClient.listOrganizations(); if (!orgs?.results?.length || !orgs.results[0].id) { throw new Error("No orgs found"); @@ -104,3 +118,46 @@ async function createProject(apiClient: ApiClient): Promise { return group; } + +export async function waitClusterState(session: Session, projectId: string, clusterName: string, state: string) { + while (true) { + const cluster = await session.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + if (cluster?.stateName === state) { + return; + } + await sleep(1000); + } +} + +async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string) { + await session.apiClient.deleteCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + while (true) { + try { + await session.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + await sleep(1000); + } catch { + break; + } + } +} diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index f9e07943..468477ac 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -1,66 +1,11 @@ -import { Session } from "../../../../src/session.js"; +import { ObjectId } from "bson"; import { expectDefined } from "../../helpers.js"; -import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js"; +import { describeWithAtlas, withProject, waitClusterState } from "./atlasHelpers.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string) { - await session.apiClient.deleteCluster({ - params: { - path: { - groupId: projectId, - clusterName, - }, - }, - }); - while (true) { - try { - await session.apiClient.getCluster({ - params: { - path: { - groupId: projectId, - clusterName, - }, - }, - }); - await sleep(1000); - } catch { - break; - } - } -} - -async function waitClusterState(session: Session, projectId: string, clusterName: string, state: string) { - while (true) { - const cluster = await session.apiClient.getCluster({ - params: { - path: { - groupId: projectId, - clusterName, - }, - }, - }); - if (cluster?.stateName === state) { - return; - } - await sleep(1000); - } -} - describeWithAtlas("clusters", (integration) => { withProject(integration, ({ getProjectId }) => { - const clusterName = "ClusterTest-" + randomId; - - afterAll(async () => { - const projectId = getProjectId(); - - const session: Session = integration.mcpServer().session; - - await deleteAndWaitCluster(session, projectId, clusterName); - }); + const clusterName = `ClusterTest-${new ObjectId()}`; describe("atlas-create-free-cluster", () => { it("should have correct metadata", async () => { diff --git a/tests/integration/tools/atlas/dbUsers.test.ts b/tests/integration/tools/atlas/dbUsers.test.ts index 892bb89e..7f575fbe 100644 --- a/tests/integration/tools/atlas/dbUsers.test.ts +++ b/tests/integration/tools/atlas/dbUsers.test.ts @@ -1,26 +1,11 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { Session } from "../../../../src/session.js"; -import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js"; +import { describeWithAtlas, withProject } from "./atlasHelpers.js"; import { expectDefined } from "../../helpers.js"; +import { ObjectId } from "bson"; describeWithAtlas("db users", (integration) => { - const userName = "testuser-" + randomId; + const userName = `testuser-${new ObjectId()}`; withProject(integration, ({ getProjectId }) => { - afterAll(async () => { - const projectId = getProjectId(); - - const session: Session = integration.mcpServer().session; - await session.apiClient.deleteDatabaseUser({ - params: { - path: { - groupId: projectId, - username: userName, - databaseName: "admin", - }, - }, - }); - }); - describe("atlas-create-db-user", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); diff --git a/tests/integration/tools/atlas/orgs.test.ts b/tests/integration/tools/atlas/orgs.test.ts index 83143404..d5bd8f18 100644 --- a/tests/integration/tools/atlas/orgs.test.ts +++ b/tests/integration/tools/atlas/orgs.test.ts @@ -18,7 +18,7 @@ describeWithAtlas("orgs", (integration) => { expect(response.content).toHaveLength(1); const data = parseTable(response.content[0].text as string); expect(data).toHaveLength(1); - expect(data[0]["Organization Name"]).toEqual("MongoDB MCP Test"); + expect(data[0]["Organization Name"]).toBeDefined(); }); }); }); diff --git a/tests/integration/tools/atlas/projects.test.ts b/tests/integration/tools/atlas/projects.test.ts index 7d773c7e..09a14570 100644 --- a/tests/integration/tools/atlas/projects.test.ts +++ b/tests/integration/tools/atlas/projects.test.ts @@ -3,10 +3,8 @@ import { ObjectId } from "mongodb"; import { parseTable, describeWithAtlas } from "./atlasHelpers.js"; import { expectDefined } from "../../helpers.js"; -const randomId = new ObjectId().toString(); - describeWithAtlas("projects", (integration) => { - const projName = "testProj-" + randomId; + const projName = `testProj-${new ObjectId()}`; afterAll(async () => { const session = integration.mcpServer().session; diff --git a/tests/integration/tools/mongodb/delete/dropIndex.test.ts b/tests/integration/tools/mongodb/delete/dropIndex.test.ts new file mode 100644 index 00000000..b565409f --- /dev/null +++ b/tests/integration/tools/mongodb/delete/dropIndex.test.ts @@ -0,0 +1,114 @@ +import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js"; + +import { + getResponseContent, + databaseCollectionParameters, + validateToolMetadata, + validateThrowsForInvalidArguments, +} from "../../../helpers.js"; + +describeWithMongoDB("dropIndex tool", (integration) => { + validateToolMetadata(integration, "drop-index", "Removes an index from a collection.", [ + ...databaseCollectionParameters, + { + name: "name", + type: "string", + description: "The name of the index to drop", + required: true, + }, + ]); + + validateThrowsForInvalidArguments(integration, "drop-index", [ + {}, + { collection: "bar", name: "_id_" }, + { database: "test", name: "_id_" }, + { collection: "bar", database: "test" }, + { collection: "bar", database: 123, name: "_id_" }, + { collection: [], database: "test", name: "_id_" }, + { collection: "bar", database: "test", name: {} }, + ]); + + it("returns an error when dropping from non-existing collection", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "drop-index", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + name: "_id_", + }, + }); + + const content = getResponseContent(response.content); + expect(content).toContain( + `Cannot drop index "_id_" because the namespace "${integration.randomDbName()}.coll1" does not exist.` + ); + }); + + it("returns an error when dropping a non-existent index", async () => { + await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1"); + + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "drop-index", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + name: "non-existent-index", + }, + }); + + const content = getResponseContent(response.content); + expect(content).toContain("index not found with name [non-existent-index]"); + }); + + it("removes an existing index", async () => { + await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1"); + await integration + .mongoClient() + .db(integration.randomDbName()) + .collection("coll1") + .createIndex({ a: 1 }, { name: "index-a" }); + + let indexes = await integration + .mongoClient() + .db(integration.randomDbName()) + .collection("coll1") + .listIndexes() + .toArray(); + expect(indexes).toHaveLength(2); + expect(indexes[0]).toHaveProperty("name", "_id_"); + expect(indexes[1]).toHaveProperty("name", "index-a"); + + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "drop-index", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + name: "index-a", + }, + }); + const content = getResponseContent(response.content); + expect(content).toContain(`Successfully dropped index "index-a" in "${integration.randomDbName()}.coll1"`); + indexes = await integration + .mongoClient() + .db(integration.randomDbName()) + .collection("coll1") + .listIndexes() + .toArray(); + expect(indexes).toHaveLength(1); + expect(indexes[0]).toHaveProperty("name", "_id_"); + }); + + validateAutoConnectBehavior(integration, "drop-index", () => { + return { + args: { + database: integration.randomDbName(), + collection: "coll1", + name: "index-a", + }, + expectedResponse: `Cannot drop index "_id_" because the namespace "${integration.randomDbName()}.coll1" does not exist.`, + }; + }); +}); diff --git a/tests/integration/tools/mongodb/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts index 39ae86fa..610a4b6f 100644 --- a/tests/integration/tools/mongodb/mongodbHelpers.ts +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -19,7 +19,7 @@ export function describeWithMongoDB( fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise }) => void, getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig, describeFn = describe -) { +): void { describeFn(name, () => { const mdbIntegration = setupMongoDBIntegrationTest(); const integration = setupIntegrationTest(() => ({ @@ -76,8 +76,6 @@ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest { let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs"); for (let i = 0; i < 10; i++) { try { - // TODO: Fix this type once mongodb-runner is updated. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call mongoCluster = await MongoCluster.start({ tmpDir: dbsDir, logDir: path.join(tmpDir, "mongodb-runner", "logs"),