Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
27 changes: 23 additions & 4 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ export function warnAboutDeprecatedOrUnknownCliArgs(
if (knownArgs.connectionString) {
usedDeprecatedArgument = true;
warn(
"The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
"Warning: The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
);
}

Expand All @@ -333,15 +333,15 @@ export function warnAboutDeprecatedOrUnknownCliArgs(
if (!valid) {
usedInvalidArgument = true;
if (suggestion) {
warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
warn(`Warning: Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
} else {
warn(`Invalid command line argument '${providedKey}'.`);
warn(`Warning: Invalid command line argument '${providedKey}'.`);
}
}
}

if (usedInvalidArgument || usedDeprecatedArgument) {
warn("Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
warn("- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
}

if (usedInvalidArgument) {
Expand Down Expand Up @@ -372,6 +372,24 @@ export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfi
maybeRegister(userConfig.username, "user");
}

function warnIfVectorSearchNotEnabledCorrectly(config: UserConfig): void {
const vectorSearchEnabled = config.previewFeatures.includes("vectorSearch");
const embeddingsProviderConfigured = !!config.voyageApiKey;
if (vectorSearchEnabled && !embeddingsProviderConfigured) {
console.warn(`\
Warning: Vector search is enabled but no embeddings provider is configured.
- Set an embeddings provider configuration option to enable auto-embeddings during document insertion and text-based queries with $vectorSearch.\
`);
}

if (!vectorSearchEnabled && embeddingsProviderConfigured) {
console.warn(`\
Warning: An embeddings provider is configured but the 'vectorSearch' preview feature is not enabled.
- Enable vector search by adding 'vectorSearch' to the 'previewFeatures' configuration option, or remove the embeddings provider configuration if not needed.\
`);
}
}

export function setupUserConfig({ cli, env }: { cli: string[]; env: Record<string, unknown> }): UserConfig {
const rawConfig = {
...parseEnvConfig(env),
Expand All @@ -392,6 +410,7 @@ export function setupUserConfig({ cli, env }: { cli: string[]; env: Record<strin
// We don't have as schema defined for all args-parser arguments so we need to merge the raw config with the parsed config.
const userConfig = { ...rawConfig, ...parseResult.data } as UserConfig;

warnIfVectorSearchNotEnabledCorrectly(userConfig);
registerKnownSecretsInRootKeychain(userConfig);
return userConfig;
}
Expand Down
30 changes: 27 additions & 3 deletions src/common/search/vectorSearchEmbeddingsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,33 @@ export class VectorSearchEmbeddingsManager {
}

switch (definition.quantization) {
// Because quantization is not defined by the user
// we have to trust them in the format they use.
// Quantization "none" means no quantization is performed, so
// full-fidelity vectors are stored therefore the underlying vector
// must be stored as an array of numbers having the same dimension
// as that of the index.
case "none":
if (!Array.isArray(fieldRef)) {
return constructError({
error: "not-a-vector",
});
}

if (fieldRef.length !== definition.numDimensions) {
return constructError({
actualNumDimensions: fieldRef.length,
actualQuantization: "none",
error: "dimension-mismatch",
});
}

if (fieldRef.some((e) => !this.isANumber(e))) {
return constructError({
actualNumDimensions: fieldRef.length,
actualQuantization: "none",
error: "not-numeric",
});
}

return undefined;
case "scalar":
case "binary":
Expand Down Expand Up @@ -251,7 +275,7 @@ export class VectorSearchEmbeddingsManager {
});
}

if (!fieldRef.every((e) => this.isANumber(e))) {
if (fieldRef.some((e) => !this.isANumber(e))) {
return constructError({
actualNumDimensions: fieldRef.length,
actualQuantization: "scalar",
Expand Down
2 changes: 1 addition & 1 deletion src/tools/mongodb/create/createIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class CreateIndexTool extends MongoDBToolBase {
])
)
.describe(
"The index definition. Use 'classic' for standard indexes and 'vectorSearch' for vector search indexes"
`The index definition. Use 'classic' for standard indexes${this.isFeatureEnabled("vectorSearch") ? " and 'vectorSearch' for vector search indexes" : ""}.`
),
};

Expand Down
18 changes: 17 additions & 1 deletion tests/integration/tools/mongodb/create/createIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ import { ObjectId, type Collection, type Document, type IndexDirection } from "m
import { afterEach, beforeEach, describe, expect, it } from "vitest";

describeWithMongoDB("createIndex tool when search is not enabled", (integration) => {
validateToolMetadata(integration, "create-index", "Create an index for a collection", [
...databaseCollectionParameters,
{
name: "definition",
type: "array",
description: "The index definition. Use 'classic' for standard indexes.",
required: true,
},
{
name: "name",
type: "string",
description: "The name of the index",
required: false,
},
]);

it("doesn't allow creating vector search indexes", async () => {
expect(integration.mcpServer().userConfig.previewFeatures).to.not.include("vectorSearch");

Expand Down Expand Up @@ -99,7 +115,7 @@ describeWithMongoDB(
name: "definition",
type: "array",
description:
"The index definition. Use 'classic' for standard indexes and 'vectorSearch' for vector search indexes",
"The index definition. Use 'classic' for standard indexes and 'vectorSearch' for vector search indexes.",
required: true,
},
{
Expand Down
18 changes: 18 additions & 0 deletions tests/integration/tools/mongodb/create/insertMany.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ describeWithMongoDB(
await collection.drop();
});

validateToolMetadata(integration, "insert-many", "Insert an array of documents into a MongoDB collection", [
...databaseCollectionParameters,
{
name: "documents",
type: "array",
description:
"The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany().",
required: true,
},
{
name: "embeddingParameters",
type: "object",
description:
"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.",
required: false,
},
]);

it("inserts a document when the embedding is correct", async () => {
await createVectorSearchIndexAndWait(integration.mongoClient(), database, "test", [
{
Expand Down
20 changes: 12 additions & 8 deletions tests/unit/common/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,14 +686,14 @@ describe("config", () => {

describe("CLI arguments", () => {
const referDocMessage =
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.";
"- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.";

type TestCase = { readonly cliArg: keyof (CliOptions & UserConfig); readonly warning: string };
const testCases = [
{
cliArg: "connectionString",
warning:
"The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string.",
"Warning: The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string.",
},
] as TestCase[];

Expand Down Expand Up @@ -742,9 +742,9 @@ describe("CLI arguments", () => {
{ warn, exit }
);

expect(warn).toHaveBeenCalledWith("Invalid command line argument 'wakanda'.");
expect(warn).toHaveBeenCalledWith("Warning: Invalid command line argument 'wakanda'.");
expect(warn).toHaveBeenCalledWith(
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
"- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
);
});

Expand All @@ -767,9 +767,11 @@ describe("CLI arguments", () => {
{ warn, exit }
);

expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonli'. Did you mean 'readOnly'?");
expect(warn).toHaveBeenCalledWith(
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
"Warning: Invalid command line argument 'readonli'. Did you mean 'readOnly'?"
);
expect(warn).toHaveBeenCalledWith(
"- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
);
});

Expand All @@ -781,9 +783,11 @@ describe("CLI arguments", () => {
{ warn, exit }
);

expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonly'. Did you mean 'readOnly'?");
expect(warn).toHaveBeenCalledWith(
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
"Warning: Invalid command line argument 'readonly'. Did you mean 'readOnly'?"
);
expect(warn).toHaveBeenCalledWith(
"- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
);
});
});
Expand Down
114 changes: 72 additions & 42 deletions tests/unit/common/search/vectorSearchEmbeddingsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ const embeddingConfig: Map<EmbeddingNamespace, VectorFieldIndexDefinition[]> = n
[
mapKey,
[
{
type: "vector",
path: "embedding_field_wo_quantization",
numDimensions: 8,
quantization: "none",
similarity: "euclidean",
},
{
type: "vector",
path: "embedding_field",
Expand Down Expand Up @@ -278,51 +285,74 @@ describe("VectorSearchEmbeddingsManager", () => {
expect(result).toHaveLength(0);
});

it("documents inserting the field with wrong type are invalid", async () => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
{ database, collection },
{ embedding_field: "some text" }
);

expect(result).toHaveLength(1);
});
it.each(["embedding_field", "embedding_field_wo_quantization"] as const)(
"documents inserting the field with wrong type are invalid - $0",
async (field) => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
{ database, collection },
{ [field]: "some text" }
);

it("documents inserting the field with wrong dimensions are invalid", async () => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
{ database, collection },
{ embedding_field: [1, 2, 3] }
);

expect(result).toHaveLength(1);
const expectedError: VectorFieldValidationError = {
actualNumDimensions: 3,
actualQuantization: "scalar",
error: "dimension-mismatch",
expectedNumDimensions: 8,
expectedQuantization: "scalar",
path: "embedding_field",
};
expect(result[0]).toEqual(expectedError);
});
expect(result).toHaveLength(1);
}
);

it("documents inserting the field with correct dimensions, but wrong type are invalid", async () => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
{ database, collection },
{ embedding_field: ["1", "2", "3", "4", "5", "6", "7", "8"] }
);
it.each([
{ path: "embedding_field", expectedQuantization: "scalar", actualQuantization: "scalar" },
{
path: "embedding_field_wo_quantization",
expectedQuantization: "none",
actualQuantization: "none",
},
] as const)(
"documents inserting the field with wrong dimensions are invalid - path = $path",
async ({ path, expectedQuantization, actualQuantization }) => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
{ database, collection },
{ [path]: [1, 2, 3] }
);

expect(result).toHaveLength(1);
const expectedError: VectorFieldValidationError = {
actualNumDimensions: 3,
actualQuantization,
error: "dimension-mismatch",
expectedNumDimensions: 8,
expectedQuantization,
path,
};
expect(result[0]).toEqual(expectedError);
}
);

expect(result).toHaveLength(1);
const expectedError: VectorFieldValidationError = {
actualNumDimensions: 8,
actualQuantization: "scalar",
error: "not-numeric",
expectedNumDimensions: 8,
expectedQuantization: "scalar",
path: "embedding_field",
};

expect(result[0]).toEqual(expectedError);
});
it.each([
{ path: "embedding_field", expectedQuantization: "scalar", actualQuantization: "scalar" },
{
path: "embedding_field_wo_quantization",
expectedQuantization: "none",
actualQuantization: "none",
},
] as const)(
"documents inserting the field with correct dimensions, but wrong type are invalid - $path",
async ({ path, expectedQuantization, actualQuantization }) => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
{ database, collection },
{ [path]: ["1", "2", "3", "4", "5", "6", "7", "8"] }
);

expect(result).toHaveLength(1);
const expectedError: VectorFieldValidationError = {
actualNumDimensions: 8,
actualQuantization,
error: "not-numeric",
expectedNumDimensions: 8,
expectedQuantization,
path,
};

expect(result[0]).toEqual(expectedError);
}
);

it("documents inserting the field with correct dimensions and quantization in binary are valid", async () => {
const result = await embeddings.findFieldsWithWrongEmbeddings(
Expand Down
Loading