Skip to content

Commit 2bb10e8

Browse files
committed
Merge branch 'main' of github.com:mongodb-js/mongodb-mcp-server into gagik/accuracy-embeddings
2 parents 746e6da + 01f799c commit 2bb10e8

20 files changed

+215
-73
lines changed

README.md

Lines changed: 38 additions & 24 deletions
Large diffs are not rendered by default.

src/common/config.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js";
1010
import { z } from "zod";
1111
const levenshtein = levenshteinModule.default;
1212

13+
const previewFeatures = z.enum(["vectorSearch"]);
14+
export type PreviewFeature = z.infer<typeof previewFeatures>;
15+
1316
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
1417
export const OPTIONS = {
1518
number: ["maxDocumentsPerQuery", "maxBytesPerQuery"],
@@ -81,7 +84,7 @@ export const OPTIONS = {
8184
"tlsFIPSMode",
8285
"version",
8386
],
84-
array: ["disabledTools", "loggers", "confirmationRequiredTools"],
87+
array: ["disabledTools", "loggers", "confirmationRequiredTools", "previewFeatures"],
8588
alias: {
8689
h: "help",
8790
p: "password",
@@ -119,7 +122,7 @@ export const ALL_CONFIG_KEYS = new Set(
119122
.concat(Object.keys(OPTIONS.alias))
120123
);
121124

122-
export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
125+
function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
123126
if (ALL_CONFIG_KEYS.has(key)) {
124127
return { valid: true };
125128
}
@@ -282,6 +285,7 @@ export const UserConfigSchema = z.object({
282285
.optional()
283286
.default("euclidean")
284287
.describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."),
288+
previewFeatures: z.array(previewFeatures).default([]).describe("An array of preview features that are enabled."),
285289
});
286290

287291
export type UserConfig = z.infer<typeof UserConfigSchema> & CliOptions;
@@ -318,6 +322,7 @@ export const defaultUserConfig: UserConfig = {
318322
disableEmbeddingsValidation: false,
319323
vectorSearchDimensions: 1024,
320324
vectorSearchSimilarityFunction: "euclidean",
325+
previewFeatures: [],
321326
};
322327

323328
export const config = setupUserConfig({
@@ -554,13 +559,13 @@ export function setupUserConfig({
554559
}: {
555560
cli: string[];
556561
env: Record<string, unknown>;
557-
defaults: Partial<UserConfig>;
562+
defaults: UserConfig;
558563
}): UserConfig {
559-
const userConfig: UserConfig = {
564+
const userConfig = {
560565
...defaults,
561566
...parseEnvConfig(env),
562567
...parseCliConfig(cli),
563-
} as UserConfig;
568+
} satisfies UserConfig;
564569

565570
userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
566571
userConfig.loggers = commaSeparatedToArray(userConfig.loggers);

src/common/search/embeddingsProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ class VoyageEmbeddingsProvider implements EmbeddingsProvider<VoyageModels, Voyag
7272
this.voyage = createVoyage({ apiKey: voyageApiKey, fetch: customFetch });
7373
}
7474

75-
static isConfiguredIn({ voyageApiKey }: UserConfig): boolean {
76-
return !!voyageApiKey;
75+
static isConfiguredIn({ voyageApiKey, previewFeatures }: UserConfig): boolean {
76+
return previewFeatures.includes("vectorSearch") && !!voyageApiKey;
7777
}
7878

7979
async embed<Model extends VoyageModels>(

src/common/search/vectorSearchEmbeddingsManager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ export class VectorSearchEmbeddingsManager {
4949
this.embeddings.delete(embeddingDefKey);
5050
}
5151

52+
async indexExists({
53+
database,
54+
collection,
55+
indexName,
56+
}: {
57+
database: string;
58+
collection: string;
59+
indexName: string;
60+
}): Promise<boolean> {
61+
const provider = await this.atlasSearchEnabledProvider();
62+
if (!provider) {
63+
return false;
64+
}
65+
66+
const searchIndexesWithName = await provider.getSearchIndexes(database, collection, indexName);
67+
68+
return searchIndexesWithName.length >= 1;
69+
}
70+
5271
async embeddingsForNamespace({
5372
database,
5473
collection,

src/tools/mongodb/create/createIndex.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4-
import { type ToolArgs, type OperationType, FeatureFlags } from "../../tool.js";
4+
import { type ToolArgs, type OperationType } from "../../tool.js";
55
import type { IndexDirection } from "mongodb";
66
import { quantizationEnum, similarityEnum } from "../../../common/search/vectorSearchEmbeddingsManager.js";
77

@@ -74,7 +74,7 @@ export class CreateIndexTool extends MongoDBToolBase {
7474
type: z.literal("classic"),
7575
keys: z.object({}).catchall(z.custom<IndexDirection>()).describe("The index definition"),
7676
}),
77-
...(this.isFeatureFlagEnabled(FeatureFlags.VectorSearch) ? [this.vectorSearchIndexDefinition] : []),
77+
...(this.isFeatureEnabled("vectorSearch") ? [this.vectorSearchIndexDefinition] : []),
7878
])
7979
)
8080
.describe(

src/tools/mongodb/delete/dropIndex.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import z from "zod";
22
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
44
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
5-
import { type ToolArgs, type OperationType, formatUntrustedData, FeatureFlags } from "../../tool.js";
5+
import { type ToolArgs, type OperationType, formatUntrustedData } from "../../tool.js";
66

77
export class DropIndexTool extends MongoDBToolBase {
88
public name = "drop-index";
99
protected description = "Drop an index for the provided database and collection.";
1010
protected argsShape = {
1111
...DbOperationArgs,
1212
indexName: z.string().nonempty().describe("The name of the index to be dropped."),
13-
type: this.isFeatureFlagEnabled(FeatureFlags.VectorSearch)
13+
type: this.isFeatureEnabled("vectorSearch")
1414
? z
1515
.enum(["classic", "search"])
1616
.describe(

src/tools/mongodb/metadata/collectionIndexes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
33
import type { ToolArgs, OperationType } from "../../tool.js";
4-
import { FeatureFlags, formatUntrustedData } from "../../tool.js";
4+
import { formatUntrustedData } from "../../tool.js";
55

66
type SearchIndexStatus = {
77
name: string;
@@ -31,7 +31,7 @@ export class CollectionIndexesTool extends MongoDBToolBase {
3131
}));
3232

3333
const searchIndexDefinitions: SearchIndexStatus[] = [];
34-
if (this.isFeatureFlagEnabled(FeatureFlags.VectorSearch) && (await this.session.isSearchSupported())) {
34+
if (this.isFeatureEnabled("vectorSearch") && (await this.session.isSearchSupported())) {
3535
const searchIndexes = await provider.getSearchIndexes(database, collection);
3636
searchIndexDefinitions.push(...this.extractSearchIndexDetails(searchIndexes));
3737
}

src/tools/mongodb/read/aggregate.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,27 @@ export class AggregateTool extends MongoDBToolBase {
100100

101101
// Check if aggregate operation uses an index if enabled
102102
if (this.config.indexCheck) {
103-
await checkIndexUsage(provider, database, collection, "aggregate", async () => {
104-
return provider
105-
.aggregate(database, collection, pipeline, {}, { writeConcern: undefined })
106-
.explain("queryPlanner");
103+
const [usesVectorSearchIndex, indexName] = await this.isVectorSearchIndexUsed({
104+
database,
105+
collection,
106+
pipeline,
107107
});
108+
switch (usesVectorSearchIndex) {
109+
case "not-vector-search-query":
110+
await checkIndexUsage(provider, database, collection, "aggregate", async () => {
111+
return provider
112+
.aggregate(database, collection, pipeline, {}, { writeConcern: undefined })
113+
.explain("queryPlanner");
114+
});
115+
break;
116+
case "non-existent-index":
117+
throw new MongoDBError(
118+
ErrorCodes.AtlasVectorSearchIndexNotFound,
119+
`Could not find an index with name "${indexName}" in namespace "${database}.${collection}".`
120+
);
121+
case "valid-index":
122+
// nothing to do, everything is correct so ready to run the query
123+
}
108124
}
109125

110126
pipeline = await this.replaceRawValuesWithEmbeddingsIfNecessary({
@@ -294,6 +310,41 @@ export class AggregateTool extends MongoDBToolBase {
294310
return pipeline;
295311
}
296312

313+
private async isVectorSearchIndexUsed({
314+
database,
315+
collection,
316+
pipeline,
317+
}: {
318+
database: string;
319+
collection: string;
320+
pipeline: Document[];
321+
}): Promise<["valid-index" | "non-existent-index" | "not-vector-search-query", string?]> {
322+
// check if the pipeline contains a $vectorSearch stage
323+
let usesVectorSearch = false;
324+
let indexName: string = "default";
325+
326+
for (const stage of pipeline) {
327+
if ("$vectorSearch" in stage) {
328+
const { $vectorSearch: vectorSearchStage } = stage as z.infer<typeof VectorSearchStage>;
329+
usesVectorSearch = true;
330+
indexName = vectorSearchStage.index;
331+
break;
332+
}
333+
}
334+
335+
if (!usesVectorSearch) {
336+
return ["not-vector-search-query"];
337+
}
338+
339+
const indexExists = await this.session.vectorSearchEmbeddingsManager.indexExists({
340+
database,
341+
collection,
342+
indexName,
343+
});
344+
345+
return [indexExists ? "valid-index" : "non-existent-index", indexName];
346+
}
347+
297348
private generateMessage({
298349
aggResultsCount,
299350
documents,

src/tools/tool.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Session } from "../common/session.js";
66
import { LogId } from "../common/logger.js";
77
import type { Telemetry } from "../telemetry/telemetry.js";
88
import { type ToolEvent } from "../telemetry/types.js";
9-
import type { UserConfig } from "../common/config.js";
9+
import type { PreviewFeature, UserConfig } from "../common/config.js";
1010
import type { Server } from "../server.js";
1111
import type { Elicitation } from "../elicitation.js";
1212

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

1616
export type ToolExecutionContext<Args extends ZodRawShape = ZodRawShape> = Parameters<ToolCallback<Args>>[1];
1717

18-
export const enum FeatureFlags {
19-
VectorSearch = "vectorSearch",
20-
}
21-
2218
/**
2319
* The type of operation the tool performs. This is used when evaluating if a tool is allowed to run based on
2420
* the config's `disabledTools` and `readOnly` settings.
@@ -325,14 +321,8 @@ export abstract class ToolBase {
325321
this.telemetry.emitEvents([event]);
326322
}
327323

328-
// TODO: Move this to a separate file
329-
protected isFeatureFlagEnabled(flag: FeatureFlags): boolean {
330-
switch (flag) {
331-
case FeatureFlags.VectorSearch:
332-
return this.config.voyageApiKey !== "";
333-
default:
334-
return false;
335-
}
324+
protected isFeatureEnabled(feature: PreviewFeature): boolean {
325+
return this.config.previewFeatures.includes(feature);
336326
}
337327
}
338328

tests/accuracy/createIndex.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describeAccuracyTests(
137137
},
138138
],
139139
{
140-
userConfig: { voyageApiKey: "valid-key" },
140+
userConfig: { previewFeatures: "vectorSearch" },
141141
clusterConfig: {
142142
search: true,
143143
},

0 commit comments

Comments
 (0)