diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index e1324917..cb071921 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "events"; -import type { MongoClientOptions } from "mongodb"; +import type { MongoClientOptions, MongoServerError } from "mongodb"; import { ConnectionString } from "mongodb-connection-string-url"; import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { generateConnectionInfoFromCliArgs, type ConnectionInfo } from "@mongosh/arg-parser"; @@ -33,6 +33,8 @@ export interface ConnectionState { } const MCP_TEST_DATABASE = "#mongodb-mcp"; +const MCP_TEST_VECTOR_SEARCH_INDEX_FIELD = "__mongodb-mcp-field"; +const MCP_TEST_VECTOR_SEARCH_INDEX_NAME = "mongodb-mcp-vector-search-index"; export const defaultDriverOptions: ConnectionInfo["driverOptions"] = { readConcern: { @@ -56,23 +58,72 @@ export class ConnectionStateConnected implements ConnectionState { public connectedAtlasCluster?: AtlasClusterConnectionInfo ) {} - private _isSearchSupported?: boolean; + private connectionMetadata?: { searchSupported: boolean; autoEmbeddingIndexSupported: boolean }; - public async isSearchSupported(): Promise { - if (this._isSearchSupported === undefined) { - try { - // If a cluster supports search indexes, the call below will succeed - // with a cursor otherwise will throw an Error. - // the Search Index Management Service might not be ready yet, but - // we assume that the agent can retry in that situation. - await this.serviceProvider.getSearchIndexes(MCP_TEST_DATABASE, "test"); - this._isSearchSupported = true; - } catch { - this._isSearchSupported = false; + private async populateConnectionMetadata(): Promise { + try { + await this.serviceProvider.createCollection(MCP_TEST_DATABASE, "test"); + // If a cluster supports search indexes, the call below will succeed + // with a cursor otherwise will throw an Error. The Search Index + // Management Service might not be ready yet, but we assume that the + // agent can retry in that situation. + await this.serviceProvider.createSearchIndexes(MCP_TEST_DATABASE, "test", [ + { + name: MCP_TEST_VECTOR_SEARCH_INDEX_NAME, + type: "vectorSearch", + definition: { + fields: [ + { + // TODO: Before public preview this needs to be + // changed to either "autoEmbedText" or "autoEmbed" + // depending on which syntax is finalized. + type: "text", + path: MCP_TEST_VECTOR_SEARCH_INDEX_FIELD, + model: "voyage-3-large", + }, + ], + }, + }, + ]); + this.connectionMetadata = { + searchSupported: true, + autoEmbeddingIndexSupported: true, + }; + } catch (error) { + if ((error as MongoServerError).codeName === "SearchNotEnabled") { + this.connectionMetadata = { + searchSupported: false, + autoEmbeddingIndexSupported: false, + }; } + // If the error if because we tried creating an index with autoEmbed + // field definition then we can safely assume that search is + // supported but auto-embeddings are not. + else if ((error as Error).message.includes('"userCommand.indexes[0].fields[0].type" must be one of')) { + this.connectionMetadata = { + searchSupported: true, + autoEmbeddingIndexSupported: false, + }; + } + } finally { + await this.serviceProvider.dropDatabase(MCP_TEST_DATABASE); + } + } + + public async isSearchSupported(): Promise { + if (this.connectionMetadata === undefined) { + await this.populateConnectionMetadata(); + } + + return this.connectionMetadata?.searchSupported ?? false; + } + + public async isAutoEmbeddingIndexSupported(): Promise { + if (this.connectionMetadata === undefined) { + await this.populateConnectionMetadata(); } - return this._isSearchSupported; + return this.connectionMetadata?.autoEmbeddingIndexSupported ?? false; } } diff --git a/src/common/errors.ts b/src/common/errors.ts index e44a7272..18306415 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -8,6 +8,7 @@ export enum ErrorCodes { AtlasVectorSearchIndexNotFound = 1_000_006, AtlasVectorSearchInvalidQuery = 1_000_007, Unexpected = 1_000_008, + AutoEmbeddingIndexNotSupported = 1_000_009, } export class MongoDBError extends Error { diff --git a/src/common/session.ts b/src/common/session.ts index 16920bc4..138de9e8 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -179,6 +179,25 @@ export class Session extends EventEmitter { } } + async isAutoEmbeddingIndexSupported(): Promise { + const state = this.connectionManager.currentConnectionState; + if (state.tag === "connected") { + return await state.isAutoEmbeddingIndexSupported(); + } + + return false; + } + + async assertAutoEmbeddingIndexSupported(): Promise { + const isSearchSupported = await this.isSearchSupported(); + if (!isSearchSupported) { + throw new MongoDBError( + ErrorCodes.AutoEmbeddingIndexNotSupported, + "The connected search management service does not support creating auto-embedding indexes." + ); + } + } + get serviceProvider(): NodeDriverServiceProvider { if (this.isConnectedToMongoDB) { const state = this.connectionManager.currentConnectionState as ConnectionStateConnected; diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index ecf06e48..87fa198e 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -7,62 +7,96 @@ import { quantizationEnum } from "../../../common/search/vectorSearchEmbeddingsM import { similarityValues } from "../../../common/schemas.js"; export class CreateIndexTool extends MongoDBToolBase { + private filterFieldSchema = z + .object({ + type: z.literal("filter"), + path: z + .string() + .describe( + "Name of the field to index. For nested fields, use dot notation to specify path to embedded fields" + ), + }) + .strict() + .describe("Definition for a field that will be used for pre-filtering results."); + + private vectorFieldSchema = z + .object({ + type: z.literal("vector"), + path: z + .string() + .describe( + "Name of the field to index. For nested fields, use dot notation to specify path to embedded fields" + ), + numDimensions: z + .number() + .min(1) + .max(8192) + .default(this.config.vectorSearchDimensions) + .describe( + "Number of vector dimensions that MongoDB Vector Search enforces at index-time and query-time" + ), + similarity: z + .enum(similarityValues) + .default(this.config.vectorSearchSimilarityFunction) + .describe( + "Vector similarity function to use to search for top K-nearest neighbors. You can set this field only for vector-type fields." + ), + quantization: quantizationEnum + .default("none") + .describe( + "Type of automatic vector quantization for your vectors. Use this setting only if your embeddings are float or double vectors." + ), + }) + .strict() + .describe("Definition for a field that contains vector embeddings."); + + private autoEmbedFieldSchema = z + .object({ + // TODO: Before public preview this needs to be changed to either + // "autoEmbedText" or "autoEmbed" depending on which syntax is + // finalized. + type: z.literal("text"), + path: z + .string() + .describe( + "Name of the field to index. For nested fields, use dot notation to specify path to embedded fields" + ), + model: z + .enum(["voyage-3-large", "voyage-3.5", "voyage-3.5-lite"]) + .describe("Voyage embedding model to be used for generating auto-embeddings"), + // TODO: As of public preview, this is not required and even if + // required, the supported modality is only "text". However we should + // keep an eye on whether the recent changes changes the requirements + // around this field. + // modality: z.enum(["text"]).describe("The type of data in the field mentioned in `path` property."), + + // We don't support specifying `hnswOptions` even in the current vector + // search implementation. Following the same idea, we won't support + // `hnswOptions` for `autoEmbedText` field definition as well. + // hnswOptions: z.object({}) + }) + .strict() + .describe("Definition for a field for which embeddings must be auto-generated."); + private vectorSearchIndexDefinition = z .object({ type: z.literal("vectorSearch"), fields: z .array( z.discriminatedUnion("type", [ - z - .object({ - type: z.literal("filter"), - path: z - .string() - .describe( - "Name of the field to index. For nested fields, use dot notation to specify path to embedded fields" - ), - }) - .strict() - .describe("Definition for a field that will be used for pre-filtering results."), - z - .object({ - type: z.literal("vector"), - path: z - .string() - .describe( - "Name of the field to index. For nested fields, use dot notation to specify path to embedded fields" - ), - numDimensions: z - .number() - .min(1) - .max(8192) - .default(this.config.vectorSearchDimensions) - .describe( - "Number of vector dimensions that MongoDB Vector Search enforces at index-time and query-time" - ), - similarity: z - .enum(similarityValues) - .default(this.config.vectorSearchSimilarityFunction) - .describe( - "Vector similarity function to use to search for top K-nearest neighbors. You can set this field only for vector-type fields." - ), - quantization: quantizationEnum - .default("none") - .describe( - "Type of automatic vector quantization for your vectors. Use this setting only if your embeddings are float or double vectors." - ), - }) - .strict() - .describe("Definition for a field that contains vector embeddings."), + this.filterFieldSchema, + this.vectorFieldSchema, + this.autoEmbedFieldSchema, ]) ) .nonempty() - .refine((fields) => fields.some((f) => f.type === "vector"), { - message: "At least one vector field must be defined", - }) - .describe( - "Definitions for the vector and filter fields to index, one definition per document. You must specify `vector` for fields that contain vector embeddings and `filter` for additional fields to filter on. At least one vector-type field definition is required." - ), + .refine((fields) => fields.some((f) => f.type === "vector" || f.type === "text"), { + message: "At least one field of type 'vector' or 'text' must be defined", + }).describe(`\ +Definitions for the vector, auto-embed and filter fields to index, one definition per document. \ +You must specify 'vector' for fields that contain vector embeddings, 'text' for fields for which embeddings must be auto-generated and, \ +'filter' for additional fields to filter on. At least one 'vector' or 'text' type field definition is required.\ +`), }) .describe("Definition for a Vector Search index."); @@ -183,7 +217,12 @@ export class CreateIndexTool extends MongoDBToolBase { break; case "vectorSearch": { - await this.ensureSearchIsSupported(); + const creatingAutoEmbedIndex = definition.fields.some((field) => field.type === "text"); + if (creatingAutoEmbedIndex) { + await this.session.assertAutoEmbeddingIndexSupported(); + } else { + await this.session.assertSearchSupported(); + } indexes = await provider.createSearchIndexes(database, collection, [ { name, @@ -204,7 +243,7 @@ export class CreateIndexTool extends MongoDBToolBase { break; case "search": { - await this.ensureSearchIsSupported(); + await this.session.assertSearchSupported(); indexes = await provider.createSearchIndexes(database, collection, [ { name, diff --git a/src/tools/mongodb/create/insertMany.ts b/src/tools/mongodb/create/insertMany.ts index f365c0fa..5fed89e7 100644 --- a/src/tools/mongodb/create/insertMany.ts +++ b/src/tools/mongodb/create/insertMany.ts @@ -12,7 +12,7 @@ const zSupportedEmbeddingParametersWithInput = zSupportedEmbeddingParameters.ext input: z .array(z.object({}).passthrough()) .describe( - "Array of objects with vector search index fields as keys (in dot notation) and the raw text values to generate embeddings for as values. The index of each object corresponds to the index of the document in the documents array." + "Array of objects with indexed field paths as keys (in dot notation) and the raw text values to generate embeddings for as values. Only provide fields that require embeddings to be generated; do not include fields that are covered by auto-embedding indexes. The index of each object corresponds to the index of the document in the documents array." ), }); @@ -34,7 +34,7 @@ export class InsertManyTool extends MongoDBToolBase { embeddingParameters: zSupportedEmbeddingParametersWithInput .optional() .describe( - "The embedding model and its parameters to use to generate embeddings for fields with vector search indexes. Note to LLM: If unsure which embedding model to use, ask the user before providing one." + "The embedding model and its parameters to use to generate embeddings. Only provide this for fields where you need to generate embeddings; do not include fields that have auto-embedding indexes configured, as MongoDB will automatically generate embeddings for those. Note to LLM: If unsure which embedding model to use, ask the user before providing one." ), } : commonArgs; @@ -93,7 +93,9 @@ export class InsertManyTool extends MongoDBToolBase { return documents; } - // Get vector search indexes for the collection + // Get vector search indexes for the collection. + // Note: embeddingsForNamespace() only returns fields that require manual embedding generation, + // excluding fields with auto-embedding indexes where MongoDB generates embeddings automatically. const vectorIndexes = await this.session.vectorSearchEmbeddingsManager.embeddingsForNamespace({ database, collection, @@ -105,7 +107,7 @@ export class InsertManyTool extends MongoDBToolBase { if (!vectorIndexes.some((index) => index.path === fieldPath)) { throw new MongoDBError( ErrorCodes.AtlasVectorSearchInvalidQuery, - `Field '${fieldPath}' does not have a vector search index in collection ${database}.${collection}. Only fields with vector search indexes can have embeddings generated.` + `Field '${fieldPath}' does not have a vector search index configured for manual embedding generation in collection ${database}.${collection}. This field either has no index or has an auto-embedding index where MongoDB generates embeddings automatically.` ); } } diff --git a/src/tools/mongodb/delete/dropIndex.ts b/src/tools/mongodb/delete/dropIndex.ts index a3af029d..ae6e5caa 100644 --- a/src/tools/mongodb/delete/dropIndex.ts +++ b/src/tools/mongodb/delete/dropIndex.ts @@ -58,7 +58,7 @@ export class DropIndexTool extends MongoDBToolBase { provider: NodeDriverServiceProvider, { database, collection, indexName }: ToolArgs ): Promise { - await this.ensureSearchIsSupported(); + await this.session.assertSearchSupported(); const indexes = await provider.getSearchIndexes(database, collection, indexName); if (indexes.length === 0) { return { diff --git a/src/tools/mongodb/mongodbSchemas.ts b/src/tools/mongodb/mongodbSchemas.ts index d9c16a09..5a3ed3a7 100644 --- a/src/tools/mongodb/mongodbSchemas.ts +++ b/src/tools/mongodb/mongodbSchemas.ts @@ -60,8 +60,17 @@ export const VectorSearchStage = z.object({ ), queryVector: z .union([z.string(), z.array(z.number())]) + .optional() + .describe( + "The content to search for when querying indexes that require manual embedding generation. Provide an array of numbers (embeddings) or a string with embeddingParameters. Do not use this for auto-embedding indexes; use 'query' instead." + ), + query: z + .object({ + text: z.string().describe("The text query to search for."), + }) + .optional() .describe( - "The content to search for. The embeddingParameters field is mandatory if the queryVector is a string, in that case, the tool generates the embedding automatically using the provided configuration." + "The query to search for when using auto-embedding indexes. MongoDB will automatically generate embeddings for the text. Use this for auto-embedding indexes, not 'queryVector'." ), numCandidates: z .number() @@ -78,8 +87,11 @@ export const VectorSearchStage = z.object({ embeddingParameters: zSupportedEmbeddingParameters .optional() .describe( - "The embedding model and its parameters to use to generate embeddings before searching. It is mandatory if queryVector is a string value. Note to LLM: If unsure, ask the user before providing one." + "The embedding model and its parameters to use to generate embeddings before searching. Only provide this when using 'queryVector' with a string value for indexes that require manual embedding generation. Do not provide this for auto-embedding indexes that use 'query'. Note to LLM: If unsure, ask the user before providing one." ), }) - .passthrough(), + .passthrough() + .refine((data) => (data.queryVector !== undefined) !== (data.query !== undefined), { + message: "Either 'queryVector' or 'query' must be provided, but not both.", + }), }); diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 888dffdc..11aa23ce 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -47,10 +47,6 @@ export abstract class MongoDBToolBase extends ToolBase { return this.session.serviceProvider; } - protected ensureSearchIsSupported(): Promise { - return this.session.assertSearchSupported(); - } - public register(server: Server): boolean { this.server = server; return super.register(server); diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index 1574ea09..28fc72a2 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -241,14 +241,29 @@ export class AggregateTool extends MongoDBToolBase { if ("$vectorSearch" in stage) { const { $vectorSearch: vectorSearchStage } = stage as z.infer; + // If using 'query' field (auto-embed indexes), MongoDB handles embeddings automatically + // so we don't need to do anything. + if (vectorSearchStage.query) { + continue; + } + + // If queryVector is already an array, no embedding generation needed if (Array.isArray(vectorSearchStage.queryVector)) { continue; } + // At this point, queryVector must be a string and we need to generate embeddings + if (!vectorSearchStage.queryVector) { + throw new MongoDBError( + ErrorCodes.AtlasVectorSearchInvalidQuery, + "Either 'queryVector' or 'query' must be provided in $vectorSearch." + ); + } + if (!vectorSearchStage.embeddingParameters) { throw new MongoDBError( ErrorCodes.AtlasVectorSearchInvalidQuery, - "embeddingModel is mandatory if queryVector is a raw string." + "embeddingParameters is mandatory when queryVector is a string. For auto-embedding indexes, use 'query' instead of 'queryVector'." ); } diff --git a/tests/integration/tools/mongodb/create/createIndex.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts index af1fe080..98b091c1 100644 --- a/tests/integration/tools/mongodb/create/createIndex.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -29,7 +29,7 @@ describeWithMongoDB("createIndex tool when search is not enabled", (integration) }, ]); - it("doesn't allow creating vector search indexes", async () => { + it("tool schema should not allow creating vector search indexes", async () => { expect(integration.mcpServer().userConfig.previewFeatures).to.not.include("search"); const { tools } = await integration.mcpClient().listTools(); @@ -52,83 +52,6 @@ describeWithMongoDB("createIndex tool when search is not enabled", (integration) describeWithMongoDB( "createIndex tool when search is enabled", - (integration) => { - it("allows creating vector search indexes", async () => { - expect(integration.mcpServer().userConfig.previewFeatures).includes("search"); - - const { tools } = await integration.mcpClient().listTools(); - const createIndexTool = tools.find((tool) => tool.name === "create-index"); - const definitionProperty = createIndexTool?.inputSchema.properties?.definition as { - type: string; - items: { anyOf: Array<{ properties: Record> }> }; - }; - expectDefined(definitionProperty); - - expect(definitionProperty.type).toEqual("array"); - - // Because search is now enabled, we should see both "classic", "search", and "vectorSearch" options in - // the anyOf array. - expect(definitionProperty.items.anyOf).toHaveLength(3); - - // Classic index definition - expect(definitionProperty.items.anyOf?.[0]?.properties?.type).toEqual({ type: "string", const: "classic" }); - expect(definitionProperty.items.anyOf?.[0]?.properties?.keys).toBeDefined(); - - // Vector search index definition - expect(definitionProperty.items.anyOf?.[1]?.properties?.type).toEqual({ - type: "string", - const: "vectorSearch", - }); - expect(definitionProperty.items.anyOf?.[1]?.properties?.fields).toBeDefined(); - - const fields = definitionProperty.items.anyOf?.[1]?.properties?.fields as { - type: string; - items: { anyOf: Array<{ type: string; properties: Record> }> }; - }; - - expect(fields.type).toEqual("array"); - expect(fields.items.anyOf).toHaveLength(2); - expect(fields.items.anyOf?.[0]?.type).toEqual("object"); - expect(fields.items.anyOf?.[0]?.properties?.type).toEqual({ type: "string", const: "filter" }); - expectDefined(fields.items.anyOf?.[0]?.properties?.path); - - expect(fields.items.anyOf?.[1]?.type).toEqual("object"); - expect(fields.items.anyOf?.[1]?.properties?.type).toEqual({ type: "string", const: "vector" }); - expectDefined(fields.items.anyOf?.[1]?.properties?.path); - expectDefined(fields.items.anyOf?.[1]?.properties?.quantization); - expectDefined(fields.items.anyOf?.[1]?.properties?.numDimensions); - expectDefined(fields.items.anyOf?.[1]?.properties?.similarity); - - // Atlas search index definition - expect(definitionProperty.items.anyOf?.[2]?.properties?.type).toEqual({ - type: "string", - const: "search", - }); - expectDefined(definitionProperty.items.anyOf?.[2]?.properties?.analyzer); - expectDefined(definitionProperty.items.anyOf?.[2]?.properties?.mappings); - - const mappings = definitionProperty.items.anyOf?.[2]?.properties?.mappings as { - type: string; - properties: Record>; - }; - - expect(mappings.type).toEqual("object"); - expectDefined(mappings.properties?.dynamic); - expectDefined(mappings.properties?.fields); - }); - }, - { - getUserConfig: () => { - return { - ...defaultTestConfig, - previewFeatures: ["search"], - }; - }, - } -); - -describeWithMongoDB( - "createIndex tool with classic indexes", (integration) => { validateToolMetadata(integration, "create-index", "Create an index for a collection", "create", [ ...databaseCollectionParameters, @@ -213,6 +136,94 @@ describeWithMongoDB( }, ]); + it("tool schema should allow creating vector search indexes", async () => { + expect(integration.mcpServer().userConfig.previewFeatures).includes("search"); + + const { tools } = await integration.mcpClient().listTools(); + const createIndexTool = tools.find((tool) => tool.name === "create-index"); + const definitionProperty = createIndexTool?.inputSchema.properties?.definition as { + type: string; + items: { anyOf: Array<{ properties: Record> }> }; + }; + expectDefined(definitionProperty); + + expect(definitionProperty.type).toEqual("array"); + + // Because search is now enabled, we should see both "classic", "search", and "vectorSearch" options in + // the anyOf array. + expect(definitionProperty.items.anyOf).toHaveLength(3); + + // Classic index definition + expect(definitionProperty.items.anyOf?.[0]?.properties?.type).toEqual({ + type: "string", + const: "classic", + }); + expect(definitionProperty.items.anyOf?.[0]?.properties?.keys).toBeDefined(); + + // Vector search index definition + expect(definitionProperty.items.anyOf?.[1]?.properties?.type).toEqual({ + type: "string", + const: "vectorSearch", + }); + expect(definitionProperty.items.anyOf?.[1]?.properties?.fields).toBeDefined(); + + const fields = definitionProperty.items.anyOf?.[1]?.properties?.fields as { + type: string; + items: { anyOf: Array<{ type: string; properties: Record> }> }; + }; + + expect(fields.type).toEqual("array"); + expect(fields.items.anyOf).toHaveLength(3); + expect(fields.items.anyOf?.[0]?.type).toEqual("object"); + expect(fields.items.anyOf?.[0]?.properties?.type).toEqual({ type: "string", const: "filter" }); + expectDefined(fields.items.anyOf?.[0]?.properties?.path); + + expect(fields.items.anyOf?.[1]?.type).toEqual("object"); + expect(fields.items.anyOf?.[1]?.properties?.type).toEqual({ type: "string", const: "vector" }); + expectDefined(fields.items.anyOf?.[1]?.properties?.path); + expectDefined(fields.items.anyOf?.[1]?.properties?.quantization); + expectDefined(fields.items.anyOf?.[1]?.properties?.numDimensions); + expectDefined(fields.items.anyOf?.[1]?.properties?.similarity); + + expect(fields.items.anyOf?.[2]?.type).toEqual("object"); + expect(fields.items.anyOf?.[2]?.properties?.type).toEqual({ type: "string", const: "text" }); + expectDefined(fields.items.anyOf?.[2]?.properties?.path); + expectDefined(fields.items.anyOf?.[2]?.properties?.model); + // TODO: enable this check when modality is supported, likely after + // public preview. + // expectDefined(fields.items.anyOf?.[2]?.properties?.modality); + + // Atlas search index definition + expect(definitionProperty.items.anyOf?.[2]?.properties?.type).toEqual({ + type: "string", + const: "search", + }); + expectDefined(definitionProperty.items.anyOf?.[2]?.properties?.analyzer); + expectDefined(definitionProperty.items.anyOf?.[2]?.properties?.mappings); + + const mappings = definitionProperty.items.anyOf?.[2]?.properties?.mappings as { + type: string; + properties: Record>; + }; + + expect(mappings.type).toEqual("object"); + expectDefined(mappings.properties?.dynamic); + expectDefined(mappings.properties?.fields); + }); + }, + { + getUserConfig: () => { + return { + ...defaultTestConfig, + previewFeatures: ["search"], + }; + }, + } +); + +describeWithMongoDB( + "createIndex tool with classic indexes when search is enabled", + (integration) => { const validateIndex = async (collection: string, expected: { name: string; key: object }[]): Promise => { const mongoClient = integration.mongoClient(); const collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray(); @@ -456,7 +467,7 @@ describeWithMongoDB( ); describeWithMongoDB( - "createIndex tool with vector search indexes", + "createIndex tool with vector search indexes when search is enabled", (integration) => { beforeEach(async () => { await integration.connectMcpClient(); @@ -679,7 +690,7 @@ describeWithMongoDB( ); describeWithMongoDB( - "createIndex tool with Atlas search indexes", + "createIndex tool with Atlas search indexes when search is enabled", (integration) => { beforeEach(async () => { await integration.connectMcpClient(); @@ -955,3 +966,83 @@ describeWithMongoDB( }, } ); + +describeWithMongoDB( + "createIndex tool with auto-embed Vector search indexes when search is enabled", + (integration) => { + beforeEach(async () => { + await integration.connectMcpClient(); + await waitUntilSearchIsReady(integration.mongoClient()); + }); + + describe("when the collection for auto-embed index does not exist", () => { + it("throws an error", async () => { + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { + database: integration.randomDbName(), + collection: "foo", + definition: [ + { + type: "vectorSearch", + fields: [{ type: "text", path: "plot", model: "voyage-3-large" }], + }, + ], + }, + }); + + const content = getResponseContent(response.content); + expect(content).toContain(`Collection '${integration.randomDbName()}.foo' does not exist`); + }); + }); + + describe("when the collection for auto-embed index exists", () => { + let collectionName: string; + let collection: Collection; + beforeEach(async () => { + collectionName = new ObjectId().toString(); + collection = await integration + .mongoClient() + .db(integration.randomDbName()) + .createCollection(collectionName); + }); + + afterEach(async () => { + await collection.drop(); + }); + + it.only("creates the index successfully", async () => { + const response = await integration.mcpClient().callTool({ + name: "create-index", + arguments: { + database: integration.randomDbName(), + collection: collectionName, + name: "auto_embed_index", + definition: [ + { + type: "vectorSearch", + fields: [{ type: "text", path: "plot", model: "voyage-3-large" }], + }, + ], + }, + }); + + const content = getResponseContent(response.content); + expect(content).toContain( + `Created the index "auto_embed_index" on collection "${collectionName}" in database "${integration.randomDbName()}"` + ); + }); + }); + }, + { + getUserConfig() { + return { + ...defaultTestConfig, + previewFeatures: ["search"], + }; + }, + downloadOptions: { + search: true, + }, + } +);