Skip to content
13 changes: 13 additions & 0 deletions src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,17 @@ export class Session extends EventEmitter<SessionEvents> {
get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined {
return this.connectionManager.currentConnectionState.connectedAtlasCluster;
}

async isSearchIndexSupported(): Promise<boolean> {
try {
const dummyDatabase = `search-index-test-db-${Date.now()}`;
const dummyCollection = `search-index-test-coll-${Date.now()}`;
// If a cluster supports search indexes, the call below will succeed
// with a cursor otherwise will throw an Error
await this.serviceProvider.getSearchIndexes(dummyDatabase, dummyCollection);
return true;
} catch {
return false;
}
}
}
8 changes: 5 additions & 3 deletions src/resources/common/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ export class DebugResource extends ReactiveResource<
}
}

toOutput(): string {
async toOutput(): Promise<string> {
let result = "";

switch (this.current.tag) {
case "connected":
result += "The user is connected to the MongoDB cluster.";
case "connected": {
const searchIndexesSupported = await this.session.isSearchIndexSupported();
Copy link
Collaborator

@gagik gagik Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind these additions but this seems like a bit of random information to include. What will be the deciding factor in what output we'll show here? I imagine there's a ton of info which could be relevant in that sense.

Copy link
Collaborator Author

@himanshusinghs himanshusinghs Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far the idea is to have anything connection related that might help Agent debug a problem with tool calls / connectivity problems.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case - we're expecting Agent to use debug resource at some point when its attempting to do search related work like creating search index. Although the tool call would respond with the appropriate error but there is a good chance of agent pulling debug resource information so we should have search related metadata here as well.

result += `The user is connected to the MongoDB cluster${searchIndexesSupported ? " with support for search indexes" : " without any support for search indexes"}.`;
break;
}
case "errored":
result += `The user is not connected to a MongoDB cluster because of an error.\n`;
if (this.current.connectedAtlasCluster) {
Expand Down
6 changes: 3 additions & 3 deletions src/resources/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ export abstract class ReactiveResource<Value, RelevantEvents extends readonly (k
this.server.mcpServer.registerResource(this.name, this.uri, this.resourceConfig, this.resourceCallback);
}

private resourceCallback: ReadResourceCallback = (uri) => ({
private resourceCallback: ReadResourceCallback = async (uri) => ({
contents: [
{
text: this.toOutput(),
text: await this.toOutput(),
mimeType: "application/json",
uri: uri.href,
},
Expand All @@ -101,5 +101,5 @@ export abstract class ReactiveResource<Value, RelevantEvents extends readonly (k
}

protected abstract reduce(eventName: RelevantEvents[number], ...event: PayloadOf<RelevantEvents[number]>[]): Value;
public abstract toOutput(): string;
public abstract toOutput(): string | Promise<string>;
}
28 changes: 27 additions & 1 deletion tests/unit/common/session.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Mocked } from "vitest";
import type { Mocked, MockedFunction } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { Session } from "../../../src/common/session.js";
Expand Down Expand Up @@ -119,4 +119,30 @@ describe("Session", () => {
expect(connectionString).toContain("--test-device-id--unknown");
});
});

describe("isSearchIndexSupported", () => {
let getSearchIndexesMock: MockedFunction<() => unknown>;
beforeEach(() => {
getSearchIndexesMock = vi.fn();
MockNodeDriverServiceProvider.connect = vi.fn().mockResolvedValue({
getSearchIndexes: getSearchIndexesMock,
} as unknown as NodeDriverServiceProvider);
});

it("should return true if listing search indexes succeed", async () => {
getSearchIndexesMock.mockResolvedValue([]);
await session.connectToMongoDB({
connectionString: "mongodb://localhost:27017",
});
expect(await session.isSearchIndexSupported()).toEqual(true);
});

it("should return false if listing search indexes fail with search error", async () => {
getSearchIndexesMock.mockRejectedValue(new Error("SearchNotEnabled"));
await session.connectToMongoDB({
connectionString: "mongodb://localhost:27017",
});
expect(await session.isSearchIndexSupported()).toEqual(false);
});
});
});
54 changes: 33 additions & 21 deletions tests/unit/resources/common/debug.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DebugResource } from "../../../../src/resources/common/debug.js";
import { Session } from "../../../../src/common/session.js";
import { Telemetry } from "../../../../src/telemetry/telemetry.js";
Expand All @@ -13,13 +13,15 @@ import { Keychain } from "../../../../src/common/keychain.js";
describe("debug resource", () => {
const logger = new CompositeLogger();
const deviceId = DeviceId.create(logger);
const session = new Session({
apiBaseUrl: "",
logger,
exportsManager: ExportsManager.init(config, logger),
connectionManager: new MCPConnectionManager(config, driverOptions, logger, deviceId),
keychain: new Keychain(),
});
const session = vi.mocked(
new Session({
apiBaseUrl: "",
logger,
exportsManager: ExportsManager.init(config, logger),
connectionManager: new MCPConnectionManager(config, driverOptions, logger, deviceId),
keychain: new Keychain(),
})
);
const telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" }, deviceId);

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

it("should be connected when a connected event happens", () => {
it("should be connected when a connected event happens", async () => {
debugResource.reduceApply("connect", undefined);
const output = debugResource.toOutput();
const output = await debugResource.toOutput();

expect(output).toContain(`The user is connected to the MongoDB cluster.`);
expect(output).toContain(
`The user is connected to the MongoDB cluster without any support for search indexes.`
);
});

it("should be disconnected when a disconnect event happens", () => {
it("should be disconnected when a disconnect event happens", async () => {
debugResource.reduceApply("disconnect", undefined);
const output = debugResource.toOutput();
const output = await debugResource.toOutput();

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

it("should be disconnected when a close event happens", () => {
it("should be disconnected when a close event happens", async () => {
debugResource.reduceApply("close", undefined);
const output = debugResource.toOutput();
const output = await debugResource.toOutput();

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

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

const output = debugResource.toOutput();
const output = await debugResource.toOutput();

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

it("should show the inferred authentication type", () => {
it("should show the inferred authentication type", async () => {
debugResource.reduceApply("connection-error", {
tag: "errored",
connectionStringAuthType: "scram",
errorReason: "Error message from the server",
});

const output = debugResource.toOutput();
const output = await debugResource.toOutput();

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

it("should show the atlas cluster information when provided", () => {
it("should show the atlas cluster information when provided", async () => {
debugResource.reduceApply("connection-error", {
tag: "errored",
connectionStringAuthType: "scram",
Expand All @@ -88,7 +92,7 @@ describe("debug resource", () => {
},
});

const output = debugResource.toOutput();
const output = await debugResource.toOutput();

expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
expect(output).toContain(
Expand All @@ -97,4 +101,12 @@ describe("debug resource", () => {
expect(output).toContain(`The inferred authentication mechanism is "scram".`);
expect(output).toContain(`<error>Error message from the server</error>`);
});

it("should notify if a cluster supports search indexes", async () => {
session.isSearchIndexSupported = vi.fn().mockResolvedValue(true);
debugResource.reduceApply("connect", undefined);
const output = await debugResource.toOutput();

expect(output).toContain(`The user is connected to the MongoDB cluster with support for search indexes.`);
});
});
Loading