diff --git a/README.md b/README.md index 152f34120..9c0675738 100644 --- a/README.md +++ b/README.md @@ -369,6 +369,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | 120000 | Time in milliseconds between export cleanup cycles that remove expired export files. | | `atlasTemporaryDatabaseUserLifetimeMs` | `MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS` | 14400000 | Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted. | | `voyageApiKey` | `MDB_VOYAGE_API_KEY` | | API key for communicating with Voyage AI. Used for generating embeddings for Vector search. | +| `previewFeatures` | `MDB_MCP_PREVIEW_FEATURES` | `[]` | An array of preview features to opt into. | #### Logger Options @@ -490,6 +491,19 @@ You can disable telemetry using: > **💡 Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. +#### Opting into Preview Features + +The MongoDB MCP Server may offer functionality that is still in development and may change in future releases. These features are considered "preview features" and are not enabled by default. Generally, these features are well tested, but may not offer the complete functionality we intend to provide in the final release or we'd like to gather feedback before making them generally available. To enable one or more preview features, use the `previewFeatures` configuration option. + +- For **environment variable** configuration, use a comma-separated string: `export MDB_MCP_PREVIEW_FEATURES="vectorSearch,feature1,feature2"`. +- For **command-line argument** configuration, use a space-separated string: `--previewFeatures vectorSearch feature1 feature2`. + +List of available preview features: + +- `vectorSearch` - Enables tools or functionality related to Vector Search in MongoDB Atlas: + - Index management, such as creating, listing, and dropping vector search indexes. + - Querying collections using vector search capabilities. This requires a configured embedding model that will be used to generate vector representations of the query data. + ### Atlas API Access To use the Atlas API tools, you'll need to create a service account in MongoDB Atlas: diff --git a/src/common/config.ts b/src/common/config.ts index 9565e1d07..03bcddf8c 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -10,6 +10,9 @@ import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js"; import { z } from "zod"; const levenshtein = levenshteinModule.default; +const previewFeatures = z.enum(["vectorSearch"]); +export type PreviewFeature = z.infer; + // From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts export const OPTIONS = { number: ["maxDocumentsPerQuery", "maxBytesPerQuery"], @@ -81,7 +84,7 @@ export const OPTIONS = { "tlsFIPSMode", "version", ], - array: ["disabledTools", "loggers", "confirmationRequiredTools"], + array: ["disabledTools", "loggers", "confirmationRequiredTools", "previewFeatures"], alias: { h: "help", p: "password", @@ -119,7 +122,7 @@ export const ALL_CONFIG_KEYS = new Set( .concat(Object.keys(OPTIONS.alias)) ); -export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } { +function validateConfigKey(key: string): { valid: boolean; suggestion?: string } { if (ALL_CONFIG_KEYS.has(key)) { return { valid: true }; } @@ -282,6 +285,7 @@ export const UserConfigSchema = z.object({ .optional() .default("euclidean") .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), + previewFeatures: z.array(previewFeatures).default([]).describe("An array of preview features that are enabled."), }); export type UserConfig = z.infer & CliOptions; @@ -318,6 +322,7 @@ export const defaultUserConfig: UserConfig = { disableEmbeddingsValidation: false, vectorSearchDimensions: 1024, vectorSearchSimilarityFunction: "euclidean", + previewFeatures: [], }; export const config = setupUserConfig({ @@ -554,13 +559,13 @@ export function setupUserConfig({ }: { cli: string[]; env: Record; - defaults: Partial; + defaults: UserConfig; }): UserConfig { - const userConfig: UserConfig = { + const userConfig = { ...defaults, ...parseEnvConfig(env), ...parseCliConfig(cli), - } as UserConfig; + } satisfies UserConfig; userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools); userConfig.loggers = commaSeparatedToArray(userConfig.loggers); diff --git a/src/common/search/embeddingsProvider.ts b/src/common/search/embeddingsProvider.ts index f81537312..24b6e2c34 100644 --- a/src/common/search/embeddingsProvider.ts +++ b/src/common/search/embeddingsProvider.ts @@ -72,8 +72,8 @@ class VoyageEmbeddingsProvider implements EmbeddingsProvider( diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index e535f4fe3..252d8c4c4 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; -import { type ToolArgs, type OperationType, FeatureFlags } from "../../tool.js"; +import { type ToolArgs, type OperationType } from "../../tool.js"; import type { IndexDirection } from "mongodb"; import { quantizationEnum, similarityEnum } from "../../../common/search/vectorSearchEmbeddingsManager.js"; @@ -74,7 +74,7 @@ export class CreateIndexTool extends MongoDBToolBase { type: z.literal("classic"), keys: z.object({}).catchall(z.custom()).describe("The index definition"), }), - ...(this.isFeatureFlagEnabled(FeatureFlags.VectorSearch) ? [this.vectorSearchIndexDefinition] : []), + ...(this.isFeatureEnabled("vectorSearch") ? [this.vectorSearchIndexDefinition] : []), ]) ) .describe( diff --git a/src/tools/mongodb/delete/dropIndex.ts b/src/tools/mongodb/delete/dropIndex.ts index dea72bf83..a6f28d0db 100644 --- a/src/tools/mongodb/delete/dropIndex.ts +++ b/src/tools/mongodb/delete/dropIndex.ts @@ -2,7 +2,7 @@ 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, FeatureFlags } from "../../tool.js"; +import { type ToolArgs, type OperationType, formatUntrustedData } from "../../tool.js"; export class DropIndexTool extends MongoDBToolBase { public name = "drop-index"; @@ -10,7 +10,7 @@ export class DropIndexTool extends MongoDBToolBase { protected argsShape = { ...DbOperationArgs, indexName: z.string().nonempty().describe("The name of the index to be dropped."), - type: this.isFeatureFlagEnabled(FeatureFlags.VectorSearch) + type: this.isFeatureEnabled("vectorSearch") ? z .enum(["classic", "search"]) .describe( diff --git a/src/tools/mongodb/metadata/collectionIndexes.ts b/src/tools/mongodb/metadata/collectionIndexes.ts index a04596b9b..19007c4f1 100644 --- a/src/tools/mongodb/metadata/collectionIndexes.ts +++ b/src/tools/mongodb/metadata/collectionIndexes.ts @@ -1,7 +1,7 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import type { ToolArgs, OperationType } from "../../tool.js"; -import { FeatureFlags, formatUntrustedData } from "../../tool.js"; +import { formatUntrustedData } from "../../tool.js"; type SearchIndexStatus = { name: string; @@ -31,7 +31,7 @@ export class CollectionIndexesTool extends MongoDBToolBase { })); const searchIndexDefinitions: SearchIndexStatus[] = []; - if (this.isFeatureFlagEnabled(FeatureFlags.VectorSearch) && (await this.session.isSearchSupported())) { + if (this.isFeatureEnabled("vectorSearch") && (await this.session.isSearchSupported())) { const searchIndexes = await provider.getSearchIndexes(database, collection); searchIndexDefinitions.push(...this.extractSearchIndexDetails(searchIndexes)); } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 8c8d2436f..ec9f01a61 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -6,7 +6,7 @@ import type { Session } from "../common/session.js"; import { LogId } from "../common/logger.js"; import type { Telemetry } from "../telemetry/telemetry.js"; import { type ToolEvent } from "../telemetry/types.js"; -import type { UserConfig } from "../common/config.js"; +import type { PreviewFeature, UserConfig } from "../common/config.js"; import type { Server } from "../server.js"; import type { Elicitation } from "../elicitation.js"; @@ -15,10 +15,6 @@ export type ToolCallbackArgs = Parameters = Parameters>[1]; -export const enum FeatureFlags { - VectorSearch = "vectorSearch", -} - /** * The type of operation the tool performs. This is used when evaluating if a tool is allowed to run based on * the config's `disabledTools` and `readOnly` settings. @@ -325,14 +321,8 @@ export abstract class ToolBase { this.telemetry.emitEvents([event]); } - // TODO: Move this to a separate file - protected isFeatureFlagEnabled(flag: FeatureFlags): boolean { - switch (flag) { - case FeatureFlags.VectorSearch: - return this.config.voyageApiKey !== ""; - default: - return false; - } + protected isFeatureEnabled(feature: PreviewFeature): boolean { + return this.config.previewFeatures.includes(feature); } } diff --git a/tests/accuracy/createIndex.test.ts b/tests/accuracy/createIndex.test.ts index 66f330148..9a3a4f90c 100644 --- a/tests/accuracy/createIndex.test.ts +++ b/tests/accuracy/createIndex.test.ts @@ -137,7 +137,7 @@ describeAccuracyTests( }, ], { - userConfig: { voyageApiKey: "valid-key" }, + userConfig: { previewFeatures: "vectorSearch" }, clusterConfig: { search: true, }, diff --git a/tests/accuracy/createIndex.vectorSearchDisabled.test.ts b/tests/accuracy/createIndex.vectorSearchDisabled.test.ts index eb5fd3ebe..320e2d1e1 100644 --- a/tests/accuracy/createIndex.vectorSearchDisabled.test.ts +++ b/tests/accuracy/createIndex.vectorSearchDisabled.test.ts @@ -52,6 +52,6 @@ describeAccuracyTests( }, ], { - userConfig: { voyageApiKey: "" }, + userConfig: { previewFeatures: "" }, } ); diff --git a/tests/accuracy/dropIndex.test.ts b/tests/accuracy/dropIndex.test.ts index d5df1182b..0c9b6803d 100644 --- a/tests/accuracy/dropIndex.test.ts +++ b/tests/accuracy/dropIndex.test.ts @@ -127,7 +127,7 @@ describeAccuracyTests( ], { userConfig: { - voyageApiKey: "voyage-api-key", + previewFeatures: "vectorSearch", }, clusterConfig: { search: true }, } diff --git a/tests/accuracy/dropIndex.vectorSearchDisabled.test.ts b/tests/accuracy/dropIndex.vectorSearchDisabled.test.ts index eca250907..6649b4fcc 100644 --- a/tests/accuracy/dropIndex.vectorSearchDisabled.test.ts +++ b/tests/accuracy/dropIndex.vectorSearchDisabled.test.ts @@ -90,7 +90,7 @@ describeAccuracyTests( ], { userConfig: { - voyageApiKey: "", + previewFeatures: "", }, } ); diff --git a/tests/integration/tools/mongodb/create/createIndex.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts index d273554c4..f76bb5ba1 100644 --- a/tests/integration/tools/mongodb/create/createIndex.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -14,7 +14,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; describeWithMongoDB("createIndex tool when search is not enabled", (integration) => { it("doesn't allow creating vector search indexes", async () => { - expect(integration.mcpServer().userConfig.voyageApiKey).toEqual(""); + expect(integration.mcpServer().userConfig.previewFeatures).to.not.include("vectorSearch"); const { tools } = await integration.mcpClient().listTools(); const createIndexTool = tools.find((tool) => tool.name === "create-index"); @@ -38,7 +38,7 @@ describeWithMongoDB( "createIndex tool when search is enabled", (integration) => { it("allows creating vector search indexes", async () => { - expect(integration.mcpServer().userConfig.voyageApiKey).not.toEqual(""); + expect(integration.mcpServer().userConfig.previewFeatures).includes("vectorSearch"); const { tools } = await integration.mcpClient().listTools(); const createIndexTool = tools.find((tool) => tool.name === "create-index"); @@ -84,7 +84,7 @@ describeWithMongoDB( getUserConfig: () => { return { ...defaultTestConfig, - voyageApiKey: "valid_key", + previewFeatures: ["vectorSearch"], }; }, } @@ -392,7 +392,7 @@ describeWithMongoDB( getUserConfig: () => { return { ...defaultTestConfig, - voyageApiKey: "valid_key", + previewFeatures: ["vectorSearch"], }; }, } @@ -613,7 +613,7 @@ describeWithMongoDB( { getUserConfig: () => ({ ...defaultTestConfig, - voyageApiKey: "valid_key", + previewFeatures: ["vectorSearch"], }), downloadOptions: { search: true, diff --git a/tests/integration/tools/mongodb/delete/dropIndex.test.ts b/tests/integration/tools/mongodb/delete/dropIndex.test.ts index e18f260cf..9bee2457e 100644 --- a/tests/integration/tools/mongodb/delete/dropIndex.test.ts +++ b/tests/integration/tools/mongodb/delete/dropIndex.test.ts @@ -157,7 +157,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])( { getUserConfig: () => ({ ...defaultTestConfig, - voyageApiKey: vectorSearchEnabled ? "test-api-key" : "", + previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [], }), } ); @@ -243,7 +243,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])( { getUserConfig: () => ({ ...defaultTestConfig, - voyageApiKey: vectorSearchEnabled ? "test-api-key" : "", + previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [], }), } ); @@ -310,7 +310,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])( { getUserConfig: () => ({ ...defaultTestConfig, - voyageApiKey: vectorSearchEnabled ? "test-api-key" : "", + previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [], }), getMockElicitationInput: () => mockElicitInput, } @@ -334,7 +334,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])( }); }, { - getUserConfig: () => ({ ...defaultTestConfig, voyageApiKey: "test-api-key" }), + getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }), } ); @@ -408,7 +408,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])( }); }, { - getUserConfig: () => ({ ...defaultTestConfig, voyageApiKey: "test-api-key" }), + getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }), downloadOptions: { search: true }, } ); @@ -484,7 +484,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])( }); }, { - getUserConfig: () => ({ ...defaultTestConfig, voyageApiKey: "test-api-key" }), + getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }), downloadOptions: { search: true }, getMockElicitationInput: () => mockElicitInput, } diff --git a/tests/integration/tools/mongodb/metadata/collectionIndexes.test.ts b/tests/integration/tools/mongodb/metadata/collectionIndexes.test.ts index 868d8d0a1..59a801055 100644 --- a/tests/integration/tools/mongodb/metadata/collectionIndexes.test.ts +++ b/tests/integration/tools/mongodb/metadata/collectionIndexes.test.ts @@ -315,7 +315,7 @@ describeWithMongoDB( { getUserConfig: () => ({ ...defaultTestConfig, - voyageApiKey: "valid_key", + previewFeatures: ["vectorSearch"], }), downloadOptions: { search: true }, } diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index 4c5963a63..5c923b571 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -683,6 +683,7 @@ describeWithMongoDB( getUserConfig: () => ({ ...defaultTestConfig, voyageApiKey: process.env.TEST_MDB_MCP_VOYAGE_API_KEY ?? "", + previewFeatures: ["vectorSearch"], maxDocumentsPerQuery: -1, maxBytesPerQuery: -1, }), diff --git a/tests/unit/toolBase.test.ts b/tests/unit/toolBase.test.ts index 0e7d958c8..984aa5bfb 100644 --- a/tests/unit/toolBase.test.ts +++ b/tests/unit/toolBase.test.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { ToolBase, type OperationType, type ToolCategory, type ToolConstructorParams } from "../../src/tools/tool.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { Session } from "../../src/common/session.js"; -import type { UserConfig } from "../../src/common/config.js"; +import type { PreviewFeature, UserConfig } from "../../src/common/config.js"; import type { Telemetry } from "../../src/telemetry/telemetry.js"; import type { Elicitation } from "../../src/elicitation.js"; import type { CompositeLogger } from "../../src/common/logger.js"; @@ -32,6 +32,7 @@ describe("ToolBase", () => { mockConfig = { confirmationRequiredTools: [], + previewFeatures: [], } as unknown as UserConfig; mockTelemetry = {} as Telemetry; @@ -100,6 +101,21 @@ describe("ToolBase", () => { expect(mockRequestConfirmation).toHaveBeenCalledTimes(1); }); }); + + describe("isFeatureEnabled", () => { + it("should return false for any feature by default", () => { + expect(testTool["isFeatureEnabled"]("vectorSearch")).to.equal(false); + expect(testTool["isFeatureEnabled"]("someOtherFeature" as PreviewFeature)).to.equal(false); + }); + + it("should return true for enabled features", () => { + mockConfig.previewFeatures = ["vectorSearch", "someOtherFeature" as PreviewFeature]; + expect(testTool["isFeatureEnabled"]("vectorSearch")).to.equal(true); + expect(testTool["isFeatureEnabled"]("someOtherFeature" as PreviewFeature)).to.equal(true); + + expect(testTool["isFeatureEnabled"]("anotherFeature" as PreviewFeature)).to.equal(false); + }); + }); }); class TestTool extends ToolBase {