Skip to content

Commit e627258

Browse files
committed
add vector search detection and a more graceful error message
1 parent 75092a8 commit e627258

File tree

6 files changed

+110
-37
lines changed

6 files changed

+110
-37
lines changed

src/common/connectionManager.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,33 @@ export interface ConnectionState {
3232
connectedAtlasCluster?: AtlasClusterConnectionInfo;
3333
}
3434

35-
export interface ConnectionStateConnected extends ConnectionState {
36-
tag: "connected";
37-
serviceProvider: NodeDriverServiceProvider;
35+
export class ConnectionStateConnected implements ConnectionState {
36+
public tag = "connected" as const;
37+
38+
constructor(
39+
public serviceProvider: NodeDriverServiceProvider,
40+
public connectionStringAuthType?: ConnectionStringAuthType,
41+
public connectedAtlasCluster?: AtlasClusterConnectionInfo
42+
) {}
43+
44+
private _isSearchSupported?: boolean;
45+
46+
public async isSearchSupported(): Promise<boolean> {
47+
if (this._isSearchSupported === undefined) {
48+
try {
49+
const dummyDatabase = `search-index-test-db-${Date.now()}`;
50+
const dummyCollection = `search-index-test-coll-${Date.now()}`;
51+
// If a cluster supports search indexes, the call below will succeed
52+
// with a cursor otherwise will throw an Error
53+
await this.serviceProvider.getSearchIndexes(dummyDatabase, dummyCollection);
54+
this._isSearchSupported = true;
55+
} catch {
56+
this._isSearchSupported = false;
57+
}
58+
}
59+
60+
return this._isSearchSupported;
61+
}
3862
}
3963

4064
export interface ConnectionStateConnecting extends ConnectionState {
@@ -199,12 +223,10 @@ export class MCPConnectionManager extends ConnectionManager {
199223
});
200224
}
201225

202-
return this.changeState("connection-success", {
203-
tag: "connected",
204-
connectedAtlasCluster: settings.atlas,
205-
serviceProvider: await serviceProvider,
206-
connectionStringAuthType,
207-
});
226+
return this.changeState(
227+
"connection-success",
228+
new ConnectionStateConnected(await serviceProvider, connectionStringAuthType, settings.atlas)
229+
);
208230
} catch (error: unknown) {
209231
const errorReason = error instanceof Error ? error.message : `${error as string}`;
210232
this.changeState("connection-error", {
@@ -270,11 +292,14 @@ export class MCPConnectionManager extends ConnectionManager {
270292
this.currentConnectionState.tag === "connecting" &&
271293
this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
272294
) {
273-
this.changeState("connection-success", {
274-
...this.currentConnectionState,
275-
tag: "connected",
276-
serviceProvider: await this.currentConnectionState.serviceProvider,
277-
});
295+
this.changeState(
296+
"connection-success",
297+
new ConnectionStateConnected(
298+
await this.currentConnectionState.serviceProvider,
299+
this.currentConnectionState.connectionStringAuthType,
300+
this.currentConnectionState.connectedAtlasCluster
301+
)
302+
);
278303
}
279304

280305
this.logger.info({

src/common/session.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ export class Session extends EventEmitter<SessionEvents> {
141141
return this.connectionManager.currentConnectionState.tag === "connected";
142142
}
143143

144+
get isConnectedToMongot(): Promise<boolean> {
145+
const state = this.connectionManager.currentConnectionState;
146+
if (state.tag === "connected") {
147+
return state.isSearchSupported();
148+
}
149+
150+
return Promise.resolve(false);
151+
}
152+
144153
get serviceProvider(): NodeDriverServiceProvider {
145154
if (this.isConnectedToMongoDB) {
146155
const state = this.connectionManager.currentConnectionState as ConnectionStateConnected;
@@ -153,17 +162,4 @@ export class Session extends EventEmitter<SessionEvents> {
153162
get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined {
154163
return this.connectionManager.currentConnectionState.connectedAtlasCluster;
155164
}
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-
}
169165
}

src/resources/common/debug.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class DebugResource extends ReactiveResource<
6161

6262
switch (this.current.tag) {
6363
case "connected": {
64-
const searchIndexesSupported = await this.session.isSearchIndexSupported();
64+
const searchIndexesSupported = await this.session.isConnectedToMongot;
6565
result += `The user is connected to the MongoDB cluster${searchIndexesSupported ? " with support for search indexes" : " without any support for search indexes"}.`;
6666
break;
6767
}

src/tools/mongodb/create/createIndex.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod";
22
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4+
import type { ToolCategory } from "../../tool.js";
45
import { type ToolArgs, type OperationType, FeatureFlags } from "../../tool.js";
56
import type { IndexDirection } from "mongodb";
67

@@ -108,15 +109,36 @@ export class CreateIndexTool extends MongoDBToolBase {
108109
]);
109110
break;
110111
case "vectorSearch":
111-
indexes = await provider.createSearchIndexes(database, collection, [
112-
{
113-
name,
114-
definition: {
115-
fields: definition.fields,
112+
{
113+
const isVectorSearchSupported = await this.session.isConnectedToMongot;
114+
if (!isVectorSearchSupported) {
115+
// TODO: remove hacky casts once we merge the local dev tools
116+
const isLocalAtlasAvailable =
117+
(this.server?.tools.filter((t) => t.category === ("atlas-local" as unknown as ToolCategory))
118+
.length ?? 0) > 0;
119+
120+
const CTA = isLocalAtlasAvailable ? "`atlas-local` tools" : "Atlas CLI";
121+
return {
122+
content: [
123+
{
124+
text: `The connected MongoDB deployment does not support vector search indexes. Either connect to a MongoDB Atlas cluster or use the ${CTA} to create and manage a local Atlas deployment.`,
125+
type: "text",
126+
},
127+
],
128+
isError: true,
129+
};
130+
}
131+
132+
indexes = await provider.createSearchIndexes(database, collection, [
133+
{
134+
name,
135+
definition: {
136+
fields: definition.fields,
137+
},
138+
type: "vectorSearch",
116139
},
117-
type: "vectorSearch",
118-
},
119-
]);
140+
]);
141+
}
120142

121143
break;
122144
}

src/tools/mongodb/mongodbTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const DbOperationArgs = {
1313
};
1414

1515
export abstract class MongoDBToolBase extends ToolBase {
16-
private server?: Server;
16+
protected server?: Server;
1717
public category: ToolCategory = "mongodb";
1818

1919
protected async ensureConnected(): Promise<NodeDriverServiceProvider> {

tests/integration/tools/mongodb/create/createIndex.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,36 @@ describeWithMongoDB(
311311
await validateIndex("coll1", [{ name: "prop1_1", key: { prop1: 1 } }]);
312312
});
313313

314+
it("fails to create a vector search index", async () => {
315+
await integration.connectMcpClient();
316+
const collection = new ObjectId().toString();
317+
await integration
318+
.mcpServer()
319+
.session.serviceProvider.createCollection(integration.randomDbName(), collection);
320+
321+
const response = await integration.mcpClient().callTool({
322+
name: "create-index",
323+
arguments: {
324+
database: integration.randomDbName(),
325+
collection,
326+
name: "vector_1_vector",
327+
definition: [
328+
{
329+
type: "vectorSearch",
330+
fields: [
331+
{ type: "vector", path: "vector_1", numDimensions: 4 },
332+
{ type: "filter", path: "category" },
333+
],
334+
},
335+
],
336+
},
337+
});
338+
339+
const content = getResponseContent(response.content);
340+
expect(content).toContain("The connected MongoDB deployment does not support vector search indexes.");
341+
expect(response.isError).toBe(true);
342+
});
343+
314344
const testCases: { name: string; direction: IndexDirection }[] = [
315345
{ name: "descending", direction: -1 },
316346
{ name: "ascending", direction: 1 },

0 commit comments

Comments
 (0)