Skip to content

Commit b3d669a

Browse files
feat: introduces drop-index tool for regular index
1. The tool has been marked to require user confirmation before execution. 2. Tests are added to confirm the negative and positive behaviour
1 parent 9d13e6b commit b3d669a

File tree

4 files changed

+190
-0
lines changed

4 files changed

+190
-0
lines changed

src/common/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export const defaultUserConfig: UserConfig = {
201201
"drop-database",
202202
"drop-collection",
203203
"delete-many",
204+
"drop-index",
204205
],
205206
transport: "stdio",
206207
httpPort: 3000,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
3+
import type { ToolArgs, OperationType } from "../../tool.js";
4+
import { CommonArgs } from "../../args.js";
5+
6+
export class DropIndexTool extends MongoDBToolBase {
7+
public name = "drop-index";
8+
protected description = "Drop an index for the provided database and collection.";
9+
protected argsShape = {
10+
...DbOperationArgs,
11+
indexName: CommonArgs.string().nonempty().describe("The name of the index to be dropped."),
12+
};
13+
public operationType: OperationType = "delete";
14+
15+
protected async execute({
16+
database,
17+
collection,
18+
indexName,
19+
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
20+
const provider = await this.ensureConnected();
21+
const result = await provider.runCommand(database, {
22+
dropIndexes: collection,
23+
index: indexName,
24+
});
25+
26+
return {
27+
content: [
28+
{
29+
text: `${result.ok ? "Successfully dropped" : "Failed to drop"} the index with name "${indexName}" from the provided namespace "${database}.${collection}".`,
30+
type: "text",
31+
},
32+
],
33+
isError: result.ok ? undefined : true,
34+
};
35+
}
36+
37+
protected getConfirmationMessage({ database, collection, indexName }: ToolArgs<typeof this.argsShape>): string {
38+
return (
39+
`You are about to drop the \`${indexName}\` index from the \`${database}.${collection}\` namespace:\n\n` +
40+
"This operation will permanently remove the index and might affect the performance of queries relying on this index.\n\n" +
41+
"**Do you confirm the execution of the action?**"
42+
);
43+
}
44+
}

src/tools/mongodb/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import { CreateCollectionTool } from "./create/createCollection.js";
2020
import { LogsTool } from "./metadata/logs.js";
2121
import { ExportTool } from "./read/export.js";
2222
import { ListSearchIndexesTool } from "./search/listSearchIndexes.js";
23+
import { DropIndexTool } from "./delete/dropIndex.js";
2324

2425
export const MongoDbTools = [
2526
ConnectTool,
2627
ListCollectionsTool,
2728
ListDatabasesTool,
2829
CollectionIndexesTool,
30+
DropIndexTool,
2931
CreateIndexTool,
3032
CollectionSchemaTool,
3133
FindTool,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, beforeEach, it, afterEach, expect } from "vitest";
2+
import type { Collection } from "mongodb";
3+
import {
4+
databaseCollectionInvalidArgs,
5+
databaseCollectionParameters,
6+
defaultDriverOptions,
7+
defaultTestConfig,
8+
getResponseContent,
9+
setupIntegrationTest,
10+
validateThrowsForInvalidArguments,
11+
validateToolMetadata,
12+
} from "../../../helpers.js";
13+
import { describeWithMongoDB } from "../mongodbHelpers.js";
14+
import { createMockElicitInput } from "../../../../utils/elicitationMocks.js";
15+
import { Elicitation } from "../../../../../src/elicitation.js";
16+
17+
describeWithMongoDB("drop-index tool", (integration) => {
18+
let moviesCollection: Collection;
19+
let indexName: string;
20+
beforeEach(async () => {
21+
await integration.connectMcpClient();
22+
const client = integration.mongoClient();
23+
moviesCollection = client.db("mflix").collection("movies");
24+
await moviesCollection.insertMany([
25+
{
26+
name: "Movie1",
27+
year: 1994,
28+
},
29+
{
30+
name: "Movie2",
31+
year: 2001,
32+
},
33+
]);
34+
indexName = await moviesCollection.createIndex({ year: 1 });
35+
});
36+
37+
afterEach(async () => {
38+
try {
39+
await moviesCollection.dropIndex(indexName);
40+
} catch (error) {
41+
if (error instanceof Error && !error.message.includes("index not found with name")) {
42+
throw error;
43+
}
44+
}
45+
await moviesCollection.drop();
46+
});
47+
48+
validateToolMetadata(integration, "drop-index", "Drop an index for the provided database and collection.", [
49+
...databaseCollectionParameters,
50+
{
51+
name: "indexName",
52+
type: "string",
53+
description: "The name of the index to be dropped.",
54+
required: true,
55+
},
56+
]);
57+
58+
validateThrowsForInvalidArguments(integration, "drop-index", [
59+
...databaseCollectionInvalidArgs,
60+
{ database: "test", collection: "testColl", indexName: null },
61+
{ database: "test", collection: "testColl", indexName: undefined },
62+
{ database: "test", collection: "testColl", indexName: [] },
63+
{ database: "test", collection: "testColl", indexName: true },
64+
{ database: "test", collection: "testColl", indexName: false },
65+
{ database: "test", collection: "testColl", indexName: 0 },
66+
{ database: "test", collection: "testColl", indexName: 12 },
67+
{ database: "test", collection: "testColl", indexName: "" },
68+
]);
69+
70+
describe.each([
71+
{
72+
database: "mflix",
73+
collection: "non-existent",
74+
},
75+
{
76+
database: "non-db",
77+
collection: "non-coll",
78+
},
79+
])(
80+
"when attempting to delete an index from non-existent namespace - $database $collection",
81+
({ database, collection }) => {
82+
it("should fail with error", async () => {
83+
const response = await integration.mcpClient().callTool({
84+
name: "drop-index",
85+
arguments: { database, collection, indexName: "non-existent" },
86+
});
87+
expect(response.isError).toBe(true);
88+
const content = getResponseContent(response.content);
89+
expect(content).toEqual(`Error running drop-index: ns not found ${database}.${collection}`);
90+
});
91+
}
92+
);
93+
94+
describe("when attempting to delete an index that does not exist", () => {
95+
it("should fail with error", async () => {
96+
const response = await integration.mcpClient().callTool({
97+
name: "drop-index",
98+
arguments: { database: "mflix", collection: "movies", indexName: "non-existent" },
99+
});
100+
expect(response.isError).toBe(true);
101+
const content = getResponseContent(response.content);
102+
expect(content).toEqual(`Error running drop-index: index not found with name [non-existent]`);
103+
});
104+
});
105+
106+
describe("when attempting to delete an index that exists", () => {
107+
it("should succeed", async () => {
108+
const response = await integration.mcpClient().callTool({
109+
name: "drop-index",
110+
// The index is created in beforeEach
111+
arguments: { database: "mflix", collection: "movies", indexName: indexName },
112+
});
113+
expect(response.isError).toBe(undefined);
114+
const content = getResponseContent(response.content);
115+
expect(content).toEqual(
116+
`Successfully dropped the index with name "${indexName}" from the provided namespace "mflix.movies".`
117+
);
118+
});
119+
});
120+
});
121+
122+
describe("drop-search-index tool - when invoked via an elicitation enabled client", () => {
123+
const mockElicitInput = createMockElicitInput();
124+
const integration = setupIntegrationTest(
125+
() => defaultTestConfig,
126+
() => defaultDriverOptions,
127+
{ elicitInput: mockElicitInput }
128+
);
129+
130+
it("should ask for confirmation before proceeding with tool call", async () => {
131+
mockElicitInput.confirmYes();
132+
await integration.mcpClient().callTool({
133+
name: "drop-index",
134+
arguments: { database: "any", collection: "foo", indexName: "default" },
135+
});
136+
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
137+
expect(mockElicitInput.mock).toHaveBeenCalledWith({
138+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
139+
message: expect.stringContaining("You are about to drop the `default` index from the `any.foo` namespace"),
140+
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)