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
2 changes: 1 addition & 1 deletion api-extractor/reports/mongodb-mcp-server.public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ export class ConnectionStateConnected implements ConnectionState {
// (undocumented)
connectionStringInfo?: ConnectionStringInfo | undefined;
// (undocumented)
isSearchSupported(): Promise<boolean>;
isSearchSupported(logger: LoggerBase): Promise<boolean>;
// (undocumented)
serviceProvider: NodeDriverServiceProvider;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion api-extractor/reports/web.public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ export class ConnectionStateConnected implements ConnectionState {
// (undocumented)
connectionStringInfo?: ConnectionStringInfo | undefined;
// (undocumented)
isSearchSupported(): Promise<boolean>;
isSearchSupported(logger: LoggerBase): Promise<boolean>;
// (undocumented)
serviceProvider: NodeDriverServiceProvider;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@
"husky": "^9.1.7",
"jsdom": "^29.0.2",
"knip": "^6.4.1",
"mongodb": "^7.1.1",
"mongodb-runner": "^6.7.6",
"openapi-types": "^12.1.3",
"openapi-typescript": "^7.13.0",
Expand Down Expand Up @@ -173,6 +172,7 @@
"express": "^5.2.1",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.2.6",
"mongodb": "^7.1.1",
"mongodb-build-info": "^1.9.7",
"mongodb-connection-string-url": "^7.0.1",
"mongodb-log-writer": "^2.5.7",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 98 additions & 11 deletions src/common/connectionManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from "events";
import { MongoServerError } from "mongodb";
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { generateConnectionInfoFromCliArgs, type ConnectionInfo } from "@mongosh/arg-parser";
import type { DeviceId } from "../helpers/deviceId.js";
Expand Down Expand Up @@ -29,7 +30,10 @@ export interface ConnectionState {
connectedAtlasCluster?: AtlasClusterConnectionInfo;
}

const MCP_TEST_DATABASE = "#mongodb-mcp";
const SEARCH_PROBE_COLLECTION_NAME = "test";

/** See https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.yml (SearchNotEnabled). */
const MONGODB_SEARCH_NOT_ENABLED_ERROR_CODE = 31082;

export const defaultDriverOptions: ConnectionInfo["driverOptions"] = {
readConcern: {
Expand All @@ -55,21 +59,104 @@ export class ConnectionStateConnected implements ConnectionState {

private _isSearchSupported?: boolean;

public async isSearchSupported(): Promise<boolean> {
public async isSearchSupported(logger: LoggerBase): Promise<boolean> {
if (this._isSearchSupported === undefined) {
this._isSearchSupported = await this.probeSearchCapability(logger);
}

Comment thread
nirinchev marked this conversation as resolved.
return this._isSearchSupported;
}

private async probeSearchCapability(logger: LoggerBase): Promise<boolean> {
const databases = await this.buildSearchProbeDatabaseCandidates(logger);

for (const databaseName of databases) {
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;
await this.serviceProvider.getSearchIndexes(databaseName, SEARCH_PROBE_COLLECTION_NAME);
logger.debug({
Comment thread
nirinchev marked this conversation as resolved.
id: LogId.searchCapabilityProbe,
context: "ConnectionStateConnected",
message: "Atlas Search capability probe succeeded",
});
return true;
} catch (probeError: unknown) {
if (
probeError instanceof MongoServerError &&
(probeError.code === MONGODB_SEARCH_NOT_ENABLED_ERROR_CODE ||
probeError.codeName === "SearchNotEnabled")
) {
logger.debug({
id: LogId.searchCapabilityProbe,
context: "ConnectionStateConnected",
message: "Atlas Search capability probe: search not enabled on cluster",
});

return false;
}

logger.debug({
id: LogId.searchCapabilityProbe,
context: "ConnectionStateConnected",
message: "Atlas Search capability probe: inconclusive error for database candidate, trying next",
});
}
}

return this._isSearchSupported;
logger.debug({
id: LogId.searchCapabilityProbe,
context: "ConnectionStateConnected",
message: "Atlas Search capability probe: no success and no SearchNotEnabled; assuming search is supported",
});

return true;
}

/**
* Build an ordered list of database names to try for the search index probe.
* Prefers the driver's initial database from the connection string (when not
* a system DB), then other non-system databases from listDatabases, then the
* fallback #mongodb-mcp database.
*/
private async buildSearchProbeDatabaseCandidates(logger: LoggerBase): Promise<string[]> {
type ListDatabasesDocument = { databases?: { name?: string }[] };
let listedNames: string[] = [];
try {
const raw = (await this.serviceProvider.listDatabases("")) as ListDatabasesDocument;
const rows = raw.databases;
if (Array.isArray(rows)) {
listedNames = rows
.map((row) => row.name)
.filter((name): name is string => typeof name === "string" && name.length > 0);
}
} catch {
logger.debug({
id: LogId.searchCapabilityProbe,
context: "ConnectionStateConnected",
message: "listDatabases failed while building Atlas Search probe candidates",
});
}

// System databases that should be skipped when searching for accessible databases
const SYSTEM_DATABASES = new Set(["admin", "local", "config"]);

const nonSystem = listedNames
.filter((name) => !SYSTEM_DATABASES.has(name))
.slice(0, 10)
.sort((a, b) => a.localeCompare(b));

const result = new Set<string>();
const initialDb = this.serviceProvider.initialDb;
if (initialDb.length > 0 && !SYSTEM_DATABASES.has(initialDb)) {
result.add(initialDb);
}

for (const name of nonSystem) {
result.add(name);
}

result.add("#mongodb-mcp");

return [...result];
}
}

Expand Down
1 change: 1 addition & 0 deletions src/common/logging/loggingDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const LogId = {
mongodbConnectTry: mongoLogId(1_004_003),
mongodbCursorCloseError: mongoLogId(1_004_004),
mongodbIndexCheckFailure: mongoLogId(1_004_005),
searchCapabilityProbe: mongoLogId(1_004_006),

toolUpdateFailure: mongoLogId(1_005_001),
resourceUpdateFailure: mongoLogId(1_005_002),
Expand Down
2 changes: 1 addition & 1 deletion src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class Session extends EventEmitter<SessionEvents> {
async isSearchSupported(): Promise<boolean> {
const state = this.connectionManager.currentConnectionState;
if (state.tag === "connected") {
return await state.isSearchSupported();
return await state.isSearchSupported(this.logger);
}

return false;
Expand Down
Loading
Loading