Skip to content

Commit faad36d

Browse files
chore: method to detect if a cluster support search indexes and expose the same on debug resource MCP-247 MCP-248 (#628)
1 parent 83f34b1 commit faad36d

File tree

5 files changed

+81
-28
lines changed

5 files changed

+81
-28
lines changed

src/common/session.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,17 @@ export class Session extends EventEmitter<SessionEvents> {
153153
get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined {
154154
return this.connectionManager.currentConnectionState.connectedAtlasCluster;
155155
}
156+
157+
async isSearchIndexSupported(): Promise<boolean> {
158+
try {
159+
const dummyDatabase = `search-index-test-db-${Date.now()}`;
160+
const dummyCollection = `search-index-test-coll-${Date.now()}`;
161+
// If a cluster supports search indexes, the call below will succeed
162+
// with a cursor otherwise will throw an Error
163+
await this.serviceProvider.getSearchIndexes(dummyDatabase, dummyCollection);
164+
return true;
165+
} catch {
166+
return false;
167+
}
168+
}
156169
}

src/resources/common/debug.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@ export class DebugResource extends ReactiveResource<
5656
}
5757
}
5858

59-
toOutput(): string {
59+
async toOutput(): Promise<string> {
6060
let result = "";
6161

6262
switch (this.current.tag) {
63-
case "connected":
64-
result += "The user is connected to the MongoDB cluster.";
63+
case "connected": {
64+
const searchIndexesSupported = await this.session.isSearchIndexSupported();
65+
result += `The user is connected to the MongoDB cluster${searchIndexesSupported ? " with support for search indexes" : " without any support for search indexes"}.`;
6566
break;
67+
}
6668
case "errored":
6769
result += `The user is not connected to a MongoDB cluster because of an error.\n`;
6870
if (this.current.connectedAtlasCluster) {

src/resources/resource.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ export abstract class ReactiveResource<Value, RelevantEvents extends readonly (k
7373
this.server.mcpServer.registerResource(this.name, this.uri, this.resourceConfig, this.resourceCallback);
7474
}
7575

76-
private resourceCallback: ReadResourceCallback = (uri) => ({
76+
private resourceCallback: ReadResourceCallback = async (uri) => ({
7777
contents: [
7878
{
79-
text: this.toOutput(),
79+
text: await this.toOutput(),
8080
mimeType: "application/json",
8181
uri: uri.href,
8282
},
@@ -101,5 +101,5 @@ export abstract class ReactiveResource<Value, RelevantEvents extends readonly (k
101101
}
102102

103103
protected abstract reduce(eventName: RelevantEvents[number], ...event: PayloadOf<RelevantEvents[number]>[]): Value;
104-
public abstract toOutput(): string;
104+
public abstract toOutput(): string | Promise<string>;
105105
}

tests/unit/common/session.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Mocked } from "vitest";
1+
import type { Mocked, MockedFunction } from "vitest";
22
import { beforeEach, describe, expect, it, vi } from "vitest";
33
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
44
import { Session } from "../../../src/common/session.js";
@@ -119,4 +119,30 @@ describe("Session", () => {
119119
expect(connectionString).toContain("--test-device-id--unknown");
120120
});
121121
});
122+
123+
describe("isSearchIndexSupported", () => {
124+
let getSearchIndexesMock: MockedFunction<() => unknown>;
125+
beforeEach(() => {
126+
getSearchIndexesMock = vi.fn();
127+
MockNodeDriverServiceProvider.connect = vi.fn().mockResolvedValue({
128+
getSearchIndexes: getSearchIndexesMock,
129+
} as unknown as NodeDriverServiceProvider);
130+
});
131+
132+
it("should return true if listing search indexes succeed", async () => {
133+
getSearchIndexesMock.mockResolvedValue([]);
134+
await session.connectToMongoDB({
135+
connectionString: "mongodb://localhost:27017",
136+
});
137+
expect(await session.isSearchIndexSupported()).toEqual(true);
138+
});
139+
140+
it("should return false if listing search indexes fail with search error", async () => {
141+
getSearchIndexesMock.mockRejectedValue(new Error("SearchNotEnabled"));
142+
await session.connectToMongoDB({
143+
connectionString: "mongodb://localhost:27017",
144+
});
145+
expect(await session.isSearchIndexSupported()).toEqual(false);
146+
});
147+
});
122148
});
Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it } from "vitest";
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import { DebugResource } from "../../../../src/resources/common/debug.js";
33
import { Session } from "../../../../src/common/session.js";
44
import { Telemetry } from "../../../../src/telemetry/telemetry.js";
@@ -13,13 +13,15 @@ import { Keychain } from "../../../../src/common/keychain.js";
1313
describe("debug resource", () => {
1414
const logger = new CompositeLogger();
1515
const deviceId = DeviceId.create(logger);
16-
const session = new Session({
17-
apiBaseUrl: "",
18-
logger,
19-
exportsManager: ExportsManager.init(config, logger),
20-
connectionManager: new MCPConnectionManager(config, driverOptions, logger, deviceId),
21-
keychain: new Keychain(),
22-
});
16+
const session = vi.mocked(
17+
new Session({
18+
apiBaseUrl: "",
19+
logger,
20+
exportsManager: ExportsManager.init(config, logger),
21+
connectionManager: new MCPConnectionManager(config, driverOptions, logger, deviceId),
22+
keychain: new Keychain(),
23+
})
24+
);
2325
const telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" }, deviceId);
2426

2527
let debugResource: DebugResource = new DebugResource(session, config, telemetry);
@@ -28,54 +30,56 @@ describe("debug resource", () => {
2830
debugResource = new DebugResource(session, config, telemetry);
2931
});
3032

31-
it("should be connected when a connected event happens", () => {
33+
it("should be connected when a connected event happens", async () => {
3234
debugResource.reduceApply("connect", undefined);
33-
const output = debugResource.toOutput();
35+
const output = await debugResource.toOutput();
3436

35-
expect(output).toContain(`The user is connected to the MongoDB cluster.`);
37+
expect(output).toContain(
38+
`The user is connected to the MongoDB cluster without any support for search indexes.`
39+
);
3640
});
3741

38-
it("should be disconnected when a disconnect event happens", () => {
42+
it("should be disconnected when a disconnect event happens", async () => {
3943
debugResource.reduceApply("disconnect", undefined);
40-
const output = debugResource.toOutput();
44+
const output = await debugResource.toOutput();
4145

4246
expect(output).toContain(`The user is not connected to a MongoDB cluster.`);
4347
});
4448

45-
it("should be disconnected when a close event happens", () => {
49+
it("should be disconnected when a close event happens", async () => {
4650
debugResource.reduceApply("close", undefined);
47-
const output = debugResource.toOutput();
51+
const output = await debugResource.toOutput();
4852

4953
expect(output).toContain(`The user is not connected to a MongoDB cluster.`);
5054
});
5155

52-
it("should be disconnected and contain an error when an error event occurred", () => {
56+
it("should be disconnected and contain an error when an error event occurred", async () => {
5357
debugResource.reduceApply("connection-error", {
5458
tag: "errored",
5559
errorReason: "Error message from the server",
5660
});
5761

58-
const output = debugResource.toOutput();
62+
const output = await debugResource.toOutput();
5963

6064
expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
6165
expect(output).toContain(`<error>Error message from the server</error>`);
6266
});
6367

64-
it("should show the inferred authentication type", () => {
68+
it("should show the inferred authentication type", async () => {
6569
debugResource.reduceApply("connection-error", {
6670
tag: "errored",
6771
connectionStringAuthType: "scram",
6872
errorReason: "Error message from the server",
6973
});
7074

71-
const output = debugResource.toOutput();
75+
const output = await debugResource.toOutput();
7276

7377
expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
7478
expect(output).toContain(`The inferred authentication mechanism is "scram".`);
7579
expect(output).toContain(`<error>Error message from the server</error>`);
7680
});
7781

78-
it("should show the atlas cluster information when provided", () => {
82+
it("should show the atlas cluster information when provided", async () => {
7983
debugResource.reduceApply("connection-error", {
8084
tag: "errored",
8185
connectionStringAuthType: "scram",
@@ -88,7 +92,7 @@ describe("debug resource", () => {
8892
},
8993
});
9094

91-
const output = debugResource.toOutput();
95+
const output = await debugResource.toOutput();
9296

9397
expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
9498
expect(output).toContain(
@@ -97,4 +101,12 @@ describe("debug resource", () => {
97101
expect(output).toContain(`The inferred authentication mechanism is "scram".`);
98102
expect(output).toContain(`<error>Error message from the server</error>`);
99103
});
104+
105+
it("should notify if a cluster supports search indexes", async () => {
106+
session.isSearchIndexSupported = vi.fn().mockResolvedValue(true);
107+
debugResource.reduceApply("connect", undefined);
108+
const output = await debugResource.toOutput();
109+
110+
expect(output).toContain(`The user is connected to the MongoDB cluster with support for search indexes.`);
111+
});
100112
});

0 commit comments

Comments
 (0)