Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8184f73
feat: introduces drop-search-index tool
himanshusinghs Oct 13, 2025
1065ee8
chore: fix isError property placement with tests
himanshusinghs Oct 13, 2025
f26ae1a
chore: fix isError property on listSearchIndexes tool with tests
himanshusinghs Oct 13, 2025
8960ffa
chore: remove redundant call
himanshusinghs Oct 13, 2025
eed4479
chore: remove unused import
himanshusinghs Oct 13, 2025
52dc7c5
chore: addresses a few common concerns
himanshusinghs Oct 14, 2025
746643b
chore: use Zod.string instead of CommonArgs.string
himanshusinghs Oct 14, 2025
00f2212
chore: adapt test for new interface
himanshusinghs Oct 15, 2025
716d9cb
chore: merge drop-search-index into drop-index tool
himanshusinghs Oct 15, 2025
7d795ca
chore: use common logic from createIndex
himanshusinghs Oct 15, 2025
7db2402
chore: replace with vi.waitFor
himanshusinghs Oct 15, 2025
45b17b8
Merge remote-tracking branch 'origin/main' into feat/MCP-239-delete-v…
himanshusinghs Oct 16, 2025
2add1f8
chore: remove search error handler class
himanshusinghs Oct 16, 2025
122f062
chore: move isToolCategoryAvailable to Server
himanshusinghs Oct 16, 2025
3e57858
chore: refactor helpers to use vi.waitFor
himanshusinghs Oct 16, 2025
d223098
chore: added a few more tests
himanshusinghs Oct 16, 2025
97810c6
chore: accuracy tests for dropping indexes
himanshusinghs Oct 16, 2025
ab3ccae
Merge branch 'main' into feat/MCP-239-delete-vector-search-indexes
himanshusinghs Oct 16, 2025
0817688
chore: replace another instance of provider with mongoclient
himanshusinghs Oct 16, 2025
ebe41fa
chore: make elicitation message more specific
himanshusinghs Oct 16, 2025
9889955
Merge remote-tracking branch 'origin/main' into feat/MCP-239-delete-v…
himanshusinghs Oct 16, 2025
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
6 changes: 5 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
UnsubscribeRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import assert from "assert";
import type { ToolBase, ToolConstructorParams } from "./tools/tool.js";
import type { ToolBase, ToolCategory, ToolConstructorParams } from "./tools/tool.js";
import { validateConnectionString } from "./helpers/connectionOptions.js";
import { packageInfo } from "./common/packageInfo.js";
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
Expand Down Expand Up @@ -174,6 +174,10 @@ export class Server {
this.mcpServer.sendResourceListChanged();
}

public isToolCategoryAvailable(name: ToolCategory): boolean {
return !!this.tools.filter((t) => t.category === name).length;
}

public sendResourceUpdated(uri: string): void {
this.session.logger.info({
id: LogId.resourceUpdateFailure,
Expand Down
70 changes: 62 additions & 8 deletions src/tools/mongodb/delete/dropIndex.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import z from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import { type ToolArgs, type OperationType, formatUntrustedData } from "../../tool.js";
import { type ToolArgs, type OperationType, formatUntrustedData, FeatureFlags } from "../../tool.js";
import { ListSearchIndexesTool } from "../search/listSearchIndexes.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."),
type: this.isFeatureFlagEnabled(FeatureFlags.VectorSearch)
? z
.enum(["classic", "search"])
.describe(
"The type of index to be deleted. Use 'classic' for standard indexes and 'search' for atlas search and vector search indexes."
)
: z
.literal("classic")
.default("classic")
.describe("The type of index to be deleted. Is always set to 'classic'."),
};
public operationType: OperationType = "delete";

protected async execute({
database,
collection,
indexName,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
protected async execute(toolArgs: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const provider = await this.ensureConnected();
switch (toolArgs.type) {
case "classic":
return this.dropClassicIndex(provider, toolArgs);
case "search":
return this.dropSearchIndex(provider, toolArgs);
}
}

private async dropClassicIndex(
provider: NodeDriverServiceProvider,
{ database, collection, indexName }: ToolArgs<typeof this.argsShape>
): Promise<CallToolResult> {
const result = await provider.runCommand(database, {
dropIndexes: collection,
index: indexName,
Expand All @@ -35,9 +55,43 @@ export class DropIndexTool extends MongoDBToolBase {
};
}

protected getConfirmationMessage({ database, collection, indexName }: ToolArgs<typeof this.argsShape>): string {
private async dropSearchIndex(
provider: NodeDriverServiceProvider,
{ database, collection, indexName }: ToolArgs<typeof this.argsShape>
): Promise<CallToolResult> {
await this.ensureSearchIsSupported();
const searchIndexes = await ListSearchIndexesTool.getSearchIndexes(provider, database, collection);
const indexDoesNotExist = !searchIndexes.find((index) => index.name === indexName);
if (indexDoesNotExist) {
return {
content: formatUntrustedData(
"Index does not exist in the provided namespace.",
JSON.stringify({ indexName, namespace: `${database}.${collection}` })
),
isError: true,
};
}

await provider.dropSearchIndex(database, collection, indexName);
return {
content: formatUntrustedData(
"Successfully dropped the index from the provided namespace.",
JSON.stringify({
indexName,
namespace: `${database}.${collection}`,
})
),
};
}

protected getConfirmationMessage({
database,
collection,
indexName,
type,
}: ToolArgs<typeof this.argsShape>): string {
return (
`You are about to drop the \`${indexName}\` index from the \`${database}.${collection}\` namespace:\n\n` +
`You are about to drop the ${type === "search" ? "search index" : "index"} named \`${indexName}\` 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?**"
);
Expand Down
6 changes: 1 addition & 5 deletions src/tools/mongodb/mongodbTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export abstract class MongoDBToolBase extends ToolBase {
isError: true,
};
case ErrorCodes.AtlasSearchNotSupported: {
const CTA = this.isToolCategoryAvailable("atlas-local" as unknown as ToolCategory)
const CTA = this.server?.isToolCategoryAvailable("atlas-local" as unknown as ToolCategory)
? "`atlas-local` tools"
: "Atlas CLI";
return {
Expand Down Expand Up @@ -123,8 +123,4 @@ export abstract class MongoDBToolBase extends ToolBase {

return metadata;
}

protected isToolCategoryAvailable(name: ToolCategory): boolean {
return (this.server?.tools.filter((t) => t.category === name).length ?? 0) > 0;
}
}
34 changes: 19 additions & 15 deletions src/tools/mongodb/search/listSearchIndexes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import type { ToolArgs, OperationType } from "../../tool.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import { formatUntrustedData } from "../../tool.js";
import { EJSON } from "bson";

export type SearchIndexStatus = {
export type SearchIndexWithStatus = {
name: string;
type: "search" | "vectorSearch";
status: string;
Expand All @@ -21,15 +22,13 @@ export class ListSearchIndexesTool extends MongoDBToolBase {
protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
const provider = await this.ensureConnected();
await this.ensureSearchIsSupported();
const searchIndexes = await ListSearchIndexesTool.getSearchIndexes(provider, database, collection);

const indexes = await provider.getSearchIndexes(database, collection);
const trimmedIndexDefinitions = this.pickRelevantInformation(indexes);

if (trimmedIndexDefinitions.length > 0) {
if (searchIndexes.length > 0) {
return {
content: formatUntrustedData(
`Found ${trimmedIndexDefinitions.length} search and vector search indexes in ${database}.${collection}`,
...trimmedIndexDefinitions.map((index) => EJSON.stringify(index))
`Found ${searchIndexes.length} search and vector search indexes in ${database}.${collection}`,
...searchIndexes.map((index) => EJSON.stringify(index))
),
};
} else {
Expand All @@ -47,14 +46,19 @@ export class ListSearchIndexesTool extends MongoDBToolBase {
return process.env.VITEST === "true";
}

/**
* Atlas Search index status contains a lot of information that is not relevant for the agent at this stage.
* Like for example, the status on each of the dedicated nodes. We only care about the main status, if it's
* queryable and the index name. We are also picking the index definition as it can be used by the agent to
* understand which fields are available for searching.
**/
protected pickRelevantInformation(indexes: Record<string, unknown>[]): SearchIndexStatus[] {
return indexes.map((index) => ({
static async getSearchIndexes(
provider: NodeDriverServiceProvider,
database: string,
collection: string
): Promise<SearchIndexWithStatus[]> {
const searchIndexes = await provider.getSearchIndexes(database, collection);
/**
* Atlas Search index status contains a lot of information that is not relevant for the agent at this stage.
* Like for example, the status on each of the dedicated nodes. We only care about the main status, if it's
* queryable and the index name. We are also picking the index definition as it can be used by the agent to
* understand which fields are available for searching.
**/
return searchIndexes.map<SearchIndexWithStatus>((index) => ({
name: (index["name"] ?? "default") as string,
type: (index["type"] ?? "UNKNOWN") as "search" | "vectorSearch",
status: (index["status"] ?? "UNKNOWN") as string,
Expand Down
177 changes: 116 additions & 61 deletions tests/accuracy/dropIndex.test.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,134 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js";
import { Matcher } from "./sdk/matcher.js";
import { formatUntrustedData } from "../../src/tools/tool.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",
},
],
content: formatUntrustedData(
"Successfully dropped the index from the provided namespace.",
JSON.stringify({
indexName,
namespace: `${database as string}.${collection as string}`,
})
),
};
},
} 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",
describeAccuracyTests(
[
{
prompt: "Delete the index called year_1 from mflix.movies namespace",
expectedToolCalls: [
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: "year_1",
type: "classic",
},
},
},
],
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()),
definition: [
{
keys: {
title: "text",
],
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()),
definition: [
{
keys: {
title: "text",
},
type: "classic",
},
type: "classic",
},
],
],
},
},
},
{
toolName: "collection-indexes",
parameters: {
database: "mflix",
collection: "movies",
{
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(),
type: "classic",
},
},
},
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: Matcher.string(),
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: Matcher.string(),
type: "classic",
},
},
},
],
mockedTools,
},
]);
],
mockedTools,
},
{
prompt: "Create a vector search index on 'mflix.movies' namespace on the 'plotSummary' field. The index should use 1024 dimensions. Confirm that its created and then drop the index.",
expectedToolCalls: [
{
toolName: "create-index",
parameters: {
database: "mflix",
collection: "movies",
name: Matcher.anyOf(Matcher.undefined, Matcher.string()),
definition: [
{
type: "vectorSearch",
fields: [
{
type: "vector",
path: "plotSummary",
numDimensions: 1024,
},
],
},
],
},
},
{
toolName: "collection-indexes",
parameters: {
database: "mflix",
collection: "movies",
},
},
{
toolName: "drop-index",
parameters: {
database: "mflix",
collection: "movies",
indexName: Matcher.string(),
type: "search",
},
},
],
mockedTools,
},
],
{
userConfig: {
voyageApiKey: "voyage-api-key",
},
clusterConfig: { search: true },
}
);
Loading
Loading