Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
106 changes: 95 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,101 @@ 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)).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