Skip to content

Commit a6dd1c0

Browse files
nirinchevWill-hxw
andauthored
fix: use dynamic database lookup for search index check (#1095)
Co-authored-by: Will-hxw <[email protected]>
1 parent 48e5823 commit a6dd1c0

11 files changed

Lines changed: 498 additions & 28 deletions

File tree

api-extractor/reports/mongodb-mcp-server.public.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ export class ConnectionStateConnected implements ConnectionState {
358358
// (undocumented)
359359
connectionStringInfo?: ConnectionStringInfo | undefined;
360360
// (undocumented)
361-
isSearchSupported(): Promise<boolean>;
361+
isSearchSupported(logger: LoggerBase): Promise<boolean>;
362362
// (undocumented)
363363
serviceProvider: NodeDriverServiceProvider;
364364
// (undocumented)

api-extractor/reports/web.public.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ export class ConnectionStateConnected implements ConnectionState {
382382
// (undocumented)
383383
connectionStringInfo?: ConnectionStringInfo | undefined;
384384
// (undocumented)
385-
isSearchSupported(): Promise<boolean>;
385+
isSearchSupported(logger: LoggerBase): Promise<boolean>;
386386
// (undocumented)
387387
serviceProvider: NodeDriverServiceProvider;
388388
// (undocumented)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@
136136
"husky": "^9.1.7",
137137
"jsdom": "^29.0.2",
138138
"knip": "^6.4.1",
139-
"mongodb": "^7.1.1",
140139
"mongodb-runner": "^6.7.6",
141140
"openapi-types": "^12.1.3",
142141
"openapi-typescript": "^7.13.0",
@@ -172,6 +171,7 @@
172171
"express": "^5.2.1",
173172
"jsonc-parser": "^3.3.1",
174173
"lru-cache": "^11.2.6",
174+
"mongodb": "^7.1.1",
175175
"mongodb-build-info": "^1.9.7",
176176
"mongodb-connection-string-url": "^7.0.1",
177177
"mongodb-log-writer": "^2.5.7",

pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common/connectionManager.ts

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventEmitter } from "events";
2+
import { MongoServerError } from "mongodb";
23
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
34
import { generateConnectionInfoFromCliArgs, type ConnectionInfo } from "@mongosh/arg-parser";
45
import type { DeviceId } from "../helpers/deviceId.js";
@@ -29,7 +30,10 @@ export interface ConnectionState {
2930
connectedAtlasCluster?: AtlasClusterConnectionInfo;
3031
}
3132

32-
const MCP_TEST_DATABASE = "#mongodb-mcp";
33+
const SEARCH_PROBE_COLLECTION_NAME = "test";
34+
35+
/** See https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.yml (SearchNotEnabled). */
36+
const MONGODB_SEARCH_NOT_ENABLED_ERROR_CODE = 31082;
3337

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

5660
private _isSearchSupported?: boolean;
5761

58-
public async isSearchSupported(): Promise<boolean> {
62+
public async isSearchSupported(logger: LoggerBase): Promise<boolean> {
5963
if (this._isSearchSupported === undefined) {
64+
this._isSearchSupported = await this.probeSearchCapability(logger);
65+
}
66+
67+
return this._isSearchSupported;
68+
}
69+
70+
private async probeSearchCapability(logger: LoggerBase): Promise<boolean> {
71+
const databases = await this.buildSearchProbeDatabaseCandidates(logger);
72+
73+
for (const databaseName of databases) {
6074
try {
61-
// If a cluster supports search indexes, the call below will succeed
62-
// with a cursor otherwise will throw an Error.
63-
// the Search Index Management Service might not be ready yet, but
64-
// we assume that the agent can retry in that situation.
65-
await this.serviceProvider.getSearchIndexes(MCP_TEST_DATABASE, "test");
66-
this._isSearchSupported = true;
67-
} catch {
68-
this._isSearchSupported = false;
75+
await this.serviceProvider.getSearchIndexes(databaseName, SEARCH_PROBE_COLLECTION_NAME);
76+
logger.debug({
77+
id: LogId.searchCapabilityProbe,
78+
context: "ConnectionStateConnected",
79+
message: "Atlas Search capability probe succeeded",
80+
});
81+
return true;
82+
} catch (probeError: unknown) {
83+
if (
84+
probeError instanceof MongoServerError &&
85+
(probeError.code === MONGODB_SEARCH_NOT_ENABLED_ERROR_CODE ||
86+
probeError.codeName === "SearchNotEnabled")
87+
) {
88+
logger.debug({
89+
id: LogId.searchCapabilityProbe,
90+
context: "ConnectionStateConnected",
91+
message: "Atlas Search capability probe: search not enabled on cluster",
92+
});
93+
94+
return false;
95+
}
96+
97+
logger.debug({
98+
id: LogId.searchCapabilityProbe,
99+
context: "ConnectionStateConnected",
100+
message: "Atlas Search capability probe: inconclusive error for database candidate, trying next",
101+
});
69102
}
70103
}
71104

72-
return this._isSearchSupported;
105+
logger.debug({
106+
id: LogId.searchCapabilityProbe,
107+
context: "ConnectionStateConnected",
108+
message: "Atlas Search capability probe: no success and no SearchNotEnabled; assuming search is supported",
109+
});
110+
111+
return true;
112+
}
113+
114+
/**
115+
* Build an ordered list of database names to try for the search index probe.
116+
* Prefers the driver's initial database from the connection string (when not
117+
* a system DB), then other non-system databases from listDatabases, then the
118+
* fallback #mongodb-mcp database.
119+
*/
120+
private async buildSearchProbeDatabaseCandidates(logger: LoggerBase): Promise<string[]> {
121+
type ListDatabasesDocument = { databases?: { name?: string }[] };
122+
let listedNames: string[] = [];
123+
try {
124+
const raw = (await this.serviceProvider.listDatabases("")) as ListDatabasesDocument;
125+
const rows = raw.databases;
126+
if (Array.isArray(rows)) {
127+
listedNames = rows
128+
.map((row) => row.name)
129+
.filter((name): name is string => typeof name === "string" && name.length > 0);
130+
}
131+
} catch {
132+
logger.debug({
133+
id: LogId.searchCapabilityProbe,
134+
context: "ConnectionStateConnected",
135+
message: "listDatabases failed while building Atlas Search probe candidates",
136+
});
137+
}
138+
139+
// System databases that should be skipped when searching for accessible databases
140+
const SYSTEM_DATABASES = new Set(["admin", "local", "config"]);
141+
142+
const nonSystem = listedNames
143+
.filter((name) => !SYSTEM_DATABASES.has(name))
144+
.slice(0, 10)
145+
.sort((a, b) => a.localeCompare(b));
146+
147+
const result = new Set<string>();
148+
const initialDb = this.serviceProvider.initialDb;
149+
if (initialDb.length > 0 && !SYSTEM_DATABASES.has(initialDb)) {
150+
result.add(initialDb);
151+
}
152+
153+
for (const name of nonSystem) {
154+
result.add(name);
155+
}
156+
157+
result.add("#mongodb-mcp");
158+
159+
return [...result];
73160
}
74161
}
75162

src/common/logging/loggingDefinitions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const LogId = {
4040
mongodbConnectTry: mongoLogId(1_004_003),
4141
mongodbCursorCloseError: mongoLogId(1_004_004),
4242
mongodbIndexCheckFailure: mongoLogId(1_004_005),
43+
searchCapabilityProbe: mongoLogId(1_004_006),
4344

4445
toolUpdateFailure: mongoLogId(1_005_001),
4546
resourceUpdateFailure: mongoLogId(1_005_002),

src/common/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class Session extends EventEmitter<SessionEvents> {
156156
async isSearchSupported(): Promise<boolean> {
157157
const state = this.connectionManager.currentConnectionState;
158158
if (state.tag === "connected") {
159-
return await state.isSearchSupported();
159+
return await state.isSearchSupported(this.logger);
160160
}
161161

162162
return false;

0 commit comments

Comments
 (0)