Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | <not set> | 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

Expand Down Expand Up @@ -490,6 +491,18 @@ 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,dedicatedClusters"`.
- For **command-line argument** configuration, use a space-separated string: `--previewFeatures vectorSearch dedicatedClusters`.

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:
Expand Down
11 changes: 8 additions & 3 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 PreviewFeatures = z.infer<typeof previewFeatures>;

// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
export const OPTIONS = {
number: ["maxDocumentsPerQuery", "maxBytesPerQuery"],
Expand Down Expand Up @@ -81,7 +84,7 @@ export const OPTIONS = {
"tlsFIPSMode",
"version",
],
array: ["disabledTools", "loggers", "confirmationRequiredTools"],
array: ["disabledTools", "loggers", "confirmationRequiredTools", "previewFeatures"],
alias: {
h: "help",
p: "password",
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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<typeof UserConfigSchema> & CliOptions;
Expand Down Expand Up @@ -318,6 +322,7 @@ export const defaultUserConfig: UserConfig = {
disableEmbeddingsValidation: false,
vectorSearchDimensions: 1024,
vectorSearchSimilarityFunction: "euclidean",
previewFeatures: [],
};

export const config = setupUserConfig({
Expand Down Expand Up @@ -556,7 +561,7 @@ export function setupUserConfig({
env: Record<string, unknown>;
defaults: Partial<UserConfig>;
}): UserConfig {
const userConfig: UserConfig = {
const userConfig = {
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation was removed from userConfig. This makes the code less maintainable as the return type is no longer explicitly documented at the declaration site. Consider restoring the type annotation: const userConfig: UserConfig = {

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] why are we removing this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already type asserted a few lines down, so I figured it's a bit redundant - const foo: Foo = { } as Foo is a bit repetitive. For context, we're not losing on type safety because we have the as UserConfig assertion anyway. That being said, since this is multiline, I can see how it might not be super obvious, so happy to bring it back if it's clearer.

...defaults,
...parseEnvConfig(env),
...parseCliConfig(cli),
Expand Down
4 changes: 2 additions & 2 deletions src/common/search/embeddingsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class VoyageEmbeddingsProvider implements EmbeddingsProvider<VoyageModels, Voyag
this.voyage = createVoyage({ apiKey: voyageApiKey, fetch: customFetch });
}

static isConfiguredIn({ voyageApiKey }: UserConfig): boolean {
return !!voyageApiKey;
static isConfiguredIn({ voyageApiKey, previewFeatures }: UserConfig): boolean {
return previewFeatures.includes("vectorSearch") && !!voyageApiKey;
}

async embed<Model extends VoyageModels>(
Expand Down
4 changes: 2 additions & 2 deletions src/tools/mongodb/create/createIndex.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -74,7 +74,7 @@ export class CreateIndexTool extends MongoDBToolBase {
type: z.literal("classic"),
keys: z.object({}).catchall(z.custom<IndexDirection>()).describe("The index definition"),
}),
...(this.isFeatureFlagEnabled(FeatureFlags.VectorSearch) ? [this.vectorSearchIndexDefinition] : []),
...(this.isFeatureEnabled("vectorSearch") ? [this.vectorSearchIndexDefinition] : []),
])
)
.describe(
Expand Down
4 changes: 2 additions & 2 deletions src/tools/mongodb/delete/dropIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ 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";
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)
type: this.isFeatureEnabled("vectorSearch")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ultimate nit: why not keep vectorSearch as a const against typos or even maintainisVectorSerachFeatureEnabled at the same level as isFeatureEnabled?

another nit: btw I usually prefer to keeping a tiny file with this logic as it might grow and we could also add some unit tests just while we're here to keep it safe from future changes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"vectorSearch" is a const in a union type. So when calling isFeatureEnabled you can only provide one of the preview features we've defined in the previewFeatures enum in config.ts.

Not sure I fully understood the second half of your suggestion - what part of the logic would you suggest we move to a separate file? For context, the enum values are currently defined in config.ts along with the PreviewFeatures union type, while isFeatureEnabled is defined on the base tool type. We could move it to config.ts if that makes more sense, but I wanted to keep the user config as close to a data bag as possible, which is why I opted to define the isFeatureEnabled helper in the base tool.

Copy link
Collaborator

@gagik gagik Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add to this: enums in JavaScript are pretty terrible and it's best to avoid them.
https://jsdevspace.substack.com/p/shrink-typescript-code-replace-enums
Especially because of "erasable syntax" that can allow us to run TypeScript in Node directly:
https://www.totaltypescript.com/erasable-syntax-only
Probably something to introduce as a lint rule soon.

? z
.enum(["classic", "search"])
.describe(
Expand Down
4 changes: 2 additions & 2 deletions src/tools/mongodb/metadata/collectionIndexes.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down
16 changes: 3 additions & 13 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { PreviewFeatures, UserConfig } from "../common/config.js";
import type { Server } from "../server.js";
import type { Elicitation } from "../elicitation.js";

Expand All @@ -15,10 +15,6 @@ export type ToolCallbackArgs<Args extends ZodRawShape> = Parameters<ToolCallback

export type ToolExecutionContext<Args extends ZodRawShape = ZodRawShape> = Parameters<ToolCallback<Args>>[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.
Expand Down Expand Up @@ -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(flag: PreviewFeatures): boolean {
return this.config.previewFeatures.includes(flag);
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/accuracy/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describeAccuracyTests([
},
},
{
prompt: "Run an approximate vectorSearch query on mflix.movies on path 'plot_embeddings' with the model voyage-3-large to find all 'sci-fy' movies.",
prompt: "Run an approximate vectorSearch query on mflix.movies on path 'plot_embeddings' with the model voyage-3-large to find all 'sci-fi' movies.",
expectedToolCalls: [
{
toolName: "collection-indexes",
Expand All @@ -173,7 +173,7 @@ describeAccuracyTests([
exact: Matcher.anyOf(Matcher.undefined, Matcher.boolean(false)),
index: "my-index",
path: "plot_embeddings",
queryVector: "sci-fy",
queryVector: "sci-fi",
embeddingParameters: {
model: "voyage-3-large",
outputDimension: Matcher.anyOf(
Expand Down
2 changes: 1 addition & 1 deletion tests/accuracy/createIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ describeAccuracyTests(
},
],
{
userConfig: { voyageApiKey: "valid-key" },
userConfig: { previewFeatures: "vectorSearch" },
clusterConfig: {
search: true,
},
Expand Down
2 changes: 1 addition & 1 deletion tests/accuracy/createIndex.vectorSearchDisabled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ describeAccuracyTests(
},
],
{
userConfig: { voyageApiKey: "" },
userConfig: { previewFeatures: "" },
}
);
2 changes: 1 addition & 1 deletion tests/accuracy/dropIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ describeAccuracyTests(
],
{
userConfig: {
voyageApiKey: "voyage-api-key",
previewFeatures: "vectorSearch",
},
clusterConfig: { search: true },
}
Expand Down
2 changes: 1 addition & 1 deletion tests/accuracy/dropIndex.vectorSearchDisabled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describeAccuracyTests(
],
{
userConfig: {
voyageApiKey: "",
previewFeatures: "",
},
}
);
10 changes: 5 additions & 5 deletions tests/integration/tools/mongodb/create/createIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -84,7 +84,7 @@ describeWithMongoDB(
getUserConfig: () => {
return {
...defaultTestConfig,
voyageApiKey: "valid_key",
previewFeatures: ["vectorSearch"],
};
},
}
Expand Down Expand Up @@ -392,7 +392,7 @@ describeWithMongoDB(
getUserConfig: () => {
return {
...defaultTestConfig,
voyageApiKey: "valid_key",
previewFeatures: ["vectorSearch"],
};
},
}
Expand Down Expand Up @@ -613,7 +613,7 @@ describeWithMongoDB(
{
getUserConfig: () => ({
...defaultTestConfig,
voyageApiKey: "valid_key",
previewFeatures: ["vectorSearch"],
}),
downloadOptions: {
search: true,
Expand Down
12 changes: 6 additions & 6 deletions tests/integration/tools/mongodb/delete/dropIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])(
{
getUserConfig: () => ({
...defaultTestConfig,
voyageApiKey: vectorSearchEnabled ? "test-api-key" : "",
previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [],
}),
}
);
Expand Down Expand Up @@ -243,7 +243,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])(
{
getUserConfig: () => ({
...defaultTestConfig,
voyageApiKey: vectorSearchEnabled ? "test-api-key" : "",
previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [],
}),
}
);
Expand Down Expand Up @@ -310,7 +310,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])(
{
getUserConfig: () => ({
...defaultTestConfig,
voyageApiKey: vectorSearchEnabled ? "test-api-key" : "",
previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [],
}),
getMockElicitationInput: () => mockElicitInput,
}
Expand All @@ -334,7 +334,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])(
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, voyageApiKey: "test-api-key" }),
getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }),
}
);

Expand Down Expand Up @@ -408,7 +408,7 @@ describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])(
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, voyageApiKey: "test-api-key" }),
getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }),
downloadOptions: { search: true },
}
);
Expand Down Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ describeWithMongoDB(
{
getUserConfig: () => ({
...defaultTestConfig,
voyageApiKey: "valid_key",
previewFeatures: ["vectorSearch"],
}),
downloadOptions: { search: true },
}
Expand Down
1 change: 1 addition & 0 deletions tests/integration/tools/mongodb/read/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ describeWithMongoDB(
getUserConfig: () => ({
...defaultTestConfig,
voyageApiKey: process.env.TEST_MDB_MCP_VOYAGE_API_KEY ?? "",
previewFeatures: ["vectorSearch"],
maxDocumentsPerQuery: -1,
maxBytesPerQuery: -1,
}),
Expand Down
Loading