Skip to content
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 src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export const defaultUserConfig: UserConfig = {
"drop-database",
"drop-collection",
"delete-many",
"drop-index",
],
transport: "stdio",
httpPort: 3000,
Expand Down
45 changes: 45 additions & 0 deletions src/tools/mongodb/delete/dropIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import z from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import { type ToolArgs, type OperationType, formatUntrustedData } from "../../tool.js";

export class DropIndexTool extends MongoDBToolBase {
public name = "drop-index";
protected description = "Drop an index for the provided database and collection.";
protected argsShape = {
...DbOperationArgs,
indexName: z.string().nonempty().describe("The name of the index to be dropped."),
};
public operationType: OperationType = "delete";

protected async execute({
database,
collection,
indexName,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const provider = await this.ensureConnected();
const result = await provider.runCommand(database, {
dropIndexes: collection,
index: indexName,
});

return {
content: formatUntrustedData(
`${result.ok ? "Successfully dropped" : "Failed to drop"} the index from the provided namespace.`,
JSON.stringify({
indexName,
namespace: `${database}.${collection}`,
})
),
isError: result.ok ? undefined : true,
};
}

protected getConfirmationMessage({ database, collection, indexName }: ToolArgs<typeof this.argsShape>): string {
return (
`You are about to drop the \`${indexName}\` index from the \`${database}.${collection}\` namespace:\n\n` +
"This operation will permanently remove the index and might affect the performance of queries relying on this index.\n\n" +
"**Do you confirm the execution of the action?**"
);
}
}
2 changes: 2 additions & 0 deletions src/tools/mongodb/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ import { CreateCollectionTool } from "./create/createCollection.js";
import { LogsTool } from "./metadata/logs.js";
import { ExportTool } from "./read/export.js";
import { ListSearchIndexesTool } from "./search/listSearchIndexes.js";
import { DropIndexTool } from "./delete/dropIndex.js";

export const MongoDbTools = [
ConnectTool,
ListCollectionsTool,
ListDatabasesTool,
CollectionIndexesTool,
DropIndexTool,
CreateIndexTool,
CollectionSchemaTool,
FindTool,
Expand Down
74 changes: 74 additions & 0 deletions tests/accuracy/dropIndex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js";
import { Matcher } from "./sdk/matcher.js";

// We don't want to delete actual indexes
const mockedTools = {
"drop-index": ({ indexName, database, collection }: Record<string, unknown>): CallToolResult => {
return {
content: [
{
text: `Successfully dropped the index with name "${String(indexName)}" from the provided namespace "${String(database)}.${String(collection)}".`,
type: "text",
},
],
};
},
} as const;

describeAccuracyTests([
{
prompt: "Delete the index called year_1 from mflix.movies namespace",
expectedToolCalls: [
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: "year_1",
},
},
],
mockedTools,
},
{
prompt: "First create a text index on field 'title' in 'mflix.movies' namespace and then drop all the indexes from 'mflix.movies' namespace",
expectedToolCalls: [
{
toolName: "create-index",
parameters: {
database: "mflix",
collection: "movies",
name: Matcher.anyOf(Matcher.undefined, Matcher.string()),
keys: {
title: "text",
},
},
},
{
toolName: "collection-indexes",
parameters: {
database: "mflix",
collection: "movies",
},
},
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: Matcher.string(),
},
},
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: Matcher.string(),
},
},
],
mockedTools,
},
]);
4 changes: 2 additions & 2 deletions tests/accuracy/sdk/accuracyTestingClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MCP_SERVER_CLI_SCRIPT } from "./constants.js";
import type { LLMToolCall } from "./accuracyResultStorage/resultStorage.js";
import type { VercelMCPClient, VercelMCPClientTools } from "./agent.js";

type ToolResultGeneratorFn = (...parameters: unknown[]) => CallToolResult | Promise<CallToolResult>;
type ToolResultGeneratorFn = (parameters: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
export type MockedTools = Record<string, ToolResultGeneratorFn>;

/**
Expand Down Expand Up @@ -44,7 +44,7 @@ export class AccuracyTestingClient {
try {
const toolResultGeneratorFn = this.mockedTools[toolName];
if (toolResultGeneratorFn) {
return await toolResultGeneratorFn(args);
return await toolResultGeneratorFn(args as Record<string, unknown>);
}

return await tool.execute(args, options);
Expand Down
181 changes: 181 additions & 0 deletions tests/integration/tools/mongodb/delete/dropIndex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { describe, beforeEach, it, afterEach, expect } from "vitest";
import type { Collection } from "mongodb";
import {
databaseCollectionInvalidArgs,
databaseCollectionParameters,
defaultDriverOptions,
defaultTestConfig,
getDataFromUntrustedContent,
getResponseContent,
setupIntegrationTest,
validateThrowsForInvalidArguments,
validateToolMetadata,
} from "../../../helpers.js";
import { describeWithMongoDB, setupMongoDBIntegrationTest } from "../mongodbHelpers.js";
import { createMockElicitInput } from "../../../../utils/elicitationMocks.js";
import { Elicitation } from "../../../../../src/elicitation.js";

describeWithMongoDB("drop-index tool", (integration) => {
let moviesCollection: Collection;
let indexName: string;
beforeEach(async () => {
await integration.connectMcpClient();
const client = integration.mongoClient();
moviesCollection = client.db("mflix").collection("movies");
await moviesCollection.insertMany([
{
name: "Movie1",
year: 1994,
},
{
name: "Movie2",
year: 2001,
},
]);
indexName = await moviesCollection.createIndex({ year: 1 });
});

afterEach(async () => {
await moviesCollection.drop();
});

validateToolMetadata(integration, "drop-index", "Drop an index for the provided database and collection.", [
...databaseCollectionParameters,
{
name: "indexName",
type: "string",
description: "The name of the index to be dropped.",
required: true,
},
]);

validateThrowsForInvalidArguments(integration, "drop-index", [
...databaseCollectionInvalidArgs,
{ database: "test", collection: "testColl", indexName: null },
{ database: "test", collection: "testColl", indexName: undefined },
{ database: "test", collection: "testColl", indexName: [] },
{ database: "test", collection: "testColl", indexName: true },
{ database: "test", collection: "testColl", indexName: false },
{ database: "test", collection: "testColl", indexName: 0 },
{ database: "test", collection: "testColl", indexName: 12 },
{ database: "test", collection: "testColl", indexName: "" },
]);

describe.each([
{
database: "mflix",
collection: "non-existent",
},
{
database: "non-db",
collection: "non-coll",
},
])(
"when attempting to delete an index from non-existent namespace - $database $collection",
({ database, collection }) => {
it("should fail with error", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: { database, collection, indexName: "non-existent" },
});
expect(response.isError).toBe(true);
const content = getResponseContent(response.content);
expect(content).toEqual(`Error running drop-index: ns not found ${database}.${collection}`);
});
}
);

describe("when attempting to delete an index that does not exist", () => {
it("should fail with error", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: { database: "mflix", collection: "movies", indexName: "non-existent" },
});
expect(response.isError).toBe(true);
const content = getResponseContent(response.content);
expect(content).toEqual(`Error running drop-index: index not found with name [non-existent]`);
});
});

describe("when attempting to delete an index that exists", () => {
it("should succeed", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
// The index is created in beforeEach
arguments: { database: "mflix", collection: "movies", indexName: indexName },
});
expect(response.isError).toBe(undefined);
const content = getResponseContent(response.content);
expect(content).toContain(`Successfully dropped the index from the provided namespace.`);
const data = getDataFromUntrustedContent(content);
expect(JSON.parse(data)).toMatchObject({ indexName, namespace: "mflix.movies" });
});
});
});

describe("drop-index tool - when invoked via an elicitation enabled client", () => {
const mockElicitInput = createMockElicitInput();
const mdbIntegration = setupMongoDBIntegrationTest();
const integration = setupIntegrationTest(
() => defaultTestConfig,
() => defaultDriverOptions,
{ elicitInput: mockElicitInput }
);
let moviesCollection: Collection;
let indexName: string;

beforeEach(async () => {
moviesCollection = mdbIntegration.mongoClient().db("mflix").collection("movies");
await moviesCollection.insertMany([
{ name: "Movie1", year: 1994 },
{ name: "Movie2", year: 2001 },
]);
indexName = await moviesCollection.createIndex({ year: 1 });
await integration.mcpClient().callTool({
name: "connect",
arguments: {
connectionString: mdbIntegration.connectionString(),
},
});
});

afterEach(async () => {
await moviesCollection.drop();
});

it("should ask for confirmation before proceeding with tool call", async () => {
expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2);
mockElicitInput.confirmYes();
await integration.mcpClient().callTool({
name: "drop-index",
arguments: { database: "mflix", collection: "movies", indexName },
});
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
expect(mockElicitInput.mock).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining(
"You are about to drop the `year_1` index from the `mflix.movies` namespace"
),
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
expect(await moviesCollection.listIndexes().toArray()).toHaveLength(1);
});

it("should not drop the index if the confirmation was not provided", async () => {
expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2);
mockElicitInput.confirmNo();
await integration.mcpClient().callTool({
name: "drop-index",
arguments: { database: "mflix", collection: "movies", indexName },
});
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
expect(mockElicitInput.mock).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining(
"You are about to drop the `year_1` index from the `mflix.movies` namespace"
),
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2);
});
});
2 changes: 1 addition & 1 deletion tests/integration/transports/stdio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describeWithMongoDB("StdioRunner", (integration) => {
const response = await client.listTools();
expect(response).toBeDefined();
expect(response.tools).toBeDefined();
expect(response.tools).toHaveLength(21);
expect(response.tools).toHaveLength(22);

const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name));
expect(sortedTools[0]?.name).toBe("aggregate");
Expand Down
Loading