From c0f3f67de8eb67e1bc4827d5c48ae99018fbe7c7 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Mon, 27 Oct 2025 16:31:02 +0100 Subject: [PATCH 1/3] chore: check that a vector search index exists with indexCheck --- .../search/vectorSearchEmbeddingsManager.ts | 20 +++++++ src/tools/mongodb/read/aggregate.ts | 57 +++++++++++++++++-- .../tools/mongodb/read/aggregate.test.ts | 41 +++++++++++++ 3 files changed, 113 insertions(+), 5 deletions(-) diff --git a/src/common/search/vectorSearchEmbeddingsManager.ts b/src/common/search/vectorSearchEmbeddingsManager.ts index a86d70269..28a7e4107 100644 --- a/src/common/search/vectorSearchEmbeddingsManager.ts +++ b/src/common/search/vectorSearchEmbeddingsManager.ts @@ -48,6 +48,26 @@ export class VectorSearchEmbeddingsManager { this.embeddings.delete(embeddingDefKey); } + async indexExists({ + database, + collection, + indexName, + }: { + database: string; + collection: string; + indexName: string; + }): Promise { + const provider = await this.atlasSearchEnabledProvider(); + if (!provider) { + return false; + } + + const searchIndexesWithName = await provider.getSearchIndexes(database, collection, indexName); + console.log(">>>>>", searchIndexesWithName); + + return searchIndexesWithName.length >= 1; + } + async embeddingsForNamespace({ database, collection, diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index a5ff12381..824441d9d 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -90,11 +90,23 @@ export class AggregateTool extends MongoDBToolBase { // Check if aggregate operation uses an index if enabled if (this.config.indexCheck) { - await checkIndexUsage(provider, database, collection, "aggregate", async () => { - return provider - .aggregate(database, collection, pipeline, {}, { writeConcern: undefined }) - .explain("queryPlanner"); - }); + const usesVectorSearchIndex = await this.isVectorSearchIndexUsed({ database, collection, pipeline }); + switch (usesVectorSearchIndex) { + case "no-vector-search-query": + await checkIndexUsage(provider, database, collection, "aggregate", async () => { + return provider + .aggregate(database, collection, pipeline, {}, { writeConcern: undefined }) + .explain("queryPlanner"); + }); + break; + case false: + throw new MongoDBError( + ErrorCodes.AtlasVectorSearchIndexNotFound, + "Could not find provided vector search index." + ); + case true: + // nothing to do, everything is correct so ready to run the query + } } pipeline = await this.replaceRawValuesWithEmbeddingsIfNecessary({ @@ -269,6 +281,41 @@ export class AggregateTool extends MongoDBToolBase { return pipeline; } + private async isVectorSearchIndexUsed({ + database, + collection, + pipeline, + }: { + database: string; + collection: string; + pipeline: Document[]; + }): Promise { + // check if the pipeline contains a $vectorSearch stage + let usesVectorSearch = false; + let indexName: string = "default"; + + for (const stage of pipeline) { + if ("$vectorSearch" in stage) { + const { $vectorSearch: vectorSearchStage } = stage as z.infer; + usesVectorSearch = true; + indexName = vectorSearchStage.index; + break; + } + } + + if (!usesVectorSearch) { + return "no-vector-search-query"; + } + + const indexExists = await this.session.vectorSearchEmbeddingsManager.indexExists({ + database, + collection, + indexName, + }); + + return indexExists; + } + private generateMessage({ aggResultsCount, documents, diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index d71ab4d91..2a65ceedf 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -394,6 +394,45 @@ describeWithMongoDB( await integration.mongoClient().db(integration.randomDbName()).collection("databases").drop(); }); + it("should throw an exception when using an index that does not exist", async () => { + await waitUntilSearchIsReady(integration.mongoClient()); + + const collection = integration.mongoClient().db(integration.randomDbName()).collection("databases"); + + await collection.insertOne({ name: "mongodb", description_embedding: [1, 2, 3, 4] }); + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "aggregate", + arguments: { + database: integration.randomDbName(), + collection: "databases", + pipeline: [ + { + $vectorSearch: { + index: "non_existing", + path: "description_embedding", + queryVector: "example", + numCandidates: 10, + limit: 10, + embeddingParameters: { + model: "voyage-3-large", + outputDimension: 256, + }, + }, + }, + { + $project: { + description_embedding: 0, + }, + }, + ], + }, + }); + + const responseContent = getResponseContent(response); + expect(responseContent).toContain("Error running aggregate: Could not find provided vector search index."); + }); + for (const [dataType, embedding] of Object.entries(DOCUMENT_EMBEDDINGS)) { for (const similarity of ["euclidean", "cosine", "dotProduct"]) { describe.skipIf(!process.env.TEST_MDB_MCP_VOYAGE_API_KEY)( @@ -406,6 +445,7 @@ describeWithMongoDB( .mongoClient() .db(integration.randomDbName()) .collection("databases"); + await collection.insertOne({ name: "mongodb", description_embedding: embedding }); await createVectorSearchIndexAndWait( @@ -674,6 +714,7 @@ describeWithMongoDB( voyageApiKey: process.env.TEST_MDB_MCP_VOYAGE_API_KEY ?? "", maxDocumentsPerQuery: -1, maxBytesPerQuery: -1, + indexCheck: true, }), downloadOptions: { search: true }, } From 90435b1100eff1c04a8f98f7da68e4e326c93985 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Mon, 27 Oct 2025 16:34:49 +0100 Subject: [PATCH 2/3] Update src/common/search/vectorSearchEmbeddingsManager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/common/search/vectorSearchEmbeddingsManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/search/vectorSearchEmbeddingsManager.ts b/src/common/search/vectorSearchEmbeddingsManager.ts index 28a7e4107..fc8c53beb 100644 --- a/src/common/search/vectorSearchEmbeddingsManager.ts +++ b/src/common/search/vectorSearchEmbeddingsManager.ts @@ -63,7 +63,6 @@ export class VectorSearchEmbeddingsManager { } const searchIndexesWithName = await provider.getSearchIndexes(database, collection, indexName); - console.log(">>>>>", searchIndexesWithName); return searchIndexesWithName.length >= 1; } From f0d4a3804c26f57ad0c69925a6c03cecf9eb327e Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 28 Oct 2025 11:02:24 +0100 Subject: [PATCH 3/3] chore: some improvements from the PR comments --- src/tools/mongodb/read/aggregate.ts | 20 +++++++++++-------- .../tools/mongodb/read/aggregate.test.ts | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index 824441d9d..85727556b 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -90,21 +90,25 @@ export class AggregateTool extends MongoDBToolBase { // Check if aggregate operation uses an index if enabled if (this.config.indexCheck) { - const usesVectorSearchIndex = await this.isVectorSearchIndexUsed({ database, collection, pipeline }); + const [usesVectorSearchIndex, indexName] = await this.isVectorSearchIndexUsed({ + database, + collection, + pipeline, + }); switch (usesVectorSearchIndex) { - case "no-vector-search-query": + case "not-vector-search-query": await checkIndexUsage(provider, database, collection, "aggregate", async () => { return provider .aggregate(database, collection, pipeline, {}, { writeConcern: undefined }) .explain("queryPlanner"); }); break; - case false: + case "non-existent-index": throw new MongoDBError( ErrorCodes.AtlasVectorSearchIndexNotFound, - "Could not find provided vector search index." + `Could not find an index with name "${indexName}" in namespace "${database}.${collection}".` ); - case true: + case "valid-index": // nothing to do, everything is correct so ready to run the query } } @@ -289,7 +293,7 @@ export class AggregateTool extends MongoDBToolBase { database: string; collection: string; pipeline: Document[]; - }): Promise { + }): Promise<["valid-index" | "non-existent-index" | "not-vector-search-query", string?]> { // check if the pipeline contains a $vectorSearch stage let usesVectorSearch = false; let indexName: string = "default"; @@ -304,7 +308,7 @@ export class AggregateTool extends MongoDBToolBase { } if (!usesVectorSearch) { - return "no-vector-search-query"; + return ["not-vector-search-query"]; } const indexExists = await this.session.vectorSearchEmbeddingsManager.indexExists({ @@ -313,7 +317,7 @@ export class AggregateTool extends MongoDBToolBase { indexName, }); - return indexExists; + return [indexExists ? "valid-index" : "non-existent-index", indexName]; } private generateMessage({ diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index 2a65ceedf..d3e89d4f2 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -430,7 +430,9 @@ describeWithMongoDB( }); const responseContent = getResponseContent(response); - expect(responseContent).toContain("Error running aggregate: Could not find provided vector search index."); + expect(responseContent).toContain( + `Error running aggregate: Could not find an index with name "non_existing" in namespace "${integration.randomDbName()}.databases".` + ); }); for (const [dataType, embedding] of Object.entries(DOCUMENT_EMBEDDINGS)) {