Skip to content

Commit 87ea625

Browse files
committed
Add tests
1 parent 632680d commit 87ea625

File tree

10 files changed

+339
-63
lines changed

10 files changed

+339
-63
lines changed

src/tools/mongodb/create/createSearchIndex.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
5-
import { IndexDirection } from "mongodb";
65

76
export class CreateSearchIndexTool extends MongoDBToolBase {
87
protected name = "create-search-index";

src/tools/mongodb/delete/dropIndex.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
33
import { ToolArgs, OperationType } from "../../tool.js";
44
import { z } from "zod";
5+
import { MongoServerError } from "mongodb";
56

67
export class DropIndexTool extends MongoDBToolBase {
78
protected name = "drop-index";
@@ -15,7 +16,16 @@ export class DropIndexTool extends MongoDBToolBase {
1516
protected async execute({ database, collection, name }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
1617
const provider = await this.ensureConnected();
1718
await provider.mongoClient.db(database).collection(collection).dropIndex(name);
18-
await provider.dropSearchIndex(database, collection, name);
19+
try {
20+
await provider.dropSearchIndex(database, collection, name);
21+
} catch (error) {
22+
if (error instanceof MongoServerError && error.codeName === "SearchNotEnabled") {
23+
// If search is not enabled (e.g. due to connecting to a non-Atlas cluster), we can ignore the error
24+
// and return an empty array for search indexes.
25+
} else {
26+
throw error;
27+
}
28+
}
1929

2030
return {
2131
content: [
@@ -26,4 +36,22 @@ export class DropIndexTool extends MongoDBToolBase {
2636
],
2737
};
2838
}
39+
40+
protected handleError(
41+
error: unknown,
42+
args: ToolArgs<typeof this.argsShape>
43+
): Promise<CallToolResult> | CallToolResult {
44+
if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
45+
return {
46+
content: [
47+
{
48+
text: `Cannot drop index "${args.name}" because the namespace "${args.database}.${args.collection}" does not exist.`,
49+
type: "text",
50+
},
51+
],
52+
};
53+
}
54+
55+
return super.handleError(error, args);
56+
}
2957
}

src/tools/mongodb/read/collectionIndexes.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
33
import { ToolArgs, OperationType } from "../../tool.js";
4+
import { Document } from "bson";
5+
import { MongoServerError } from "mongodb";
46

57
export class CollectionIndexesTool extends MongoDBToolBase {
68
protected name = "collection-indexes";
@@ -11,12 +13,24 @@ export class CollectionIndexesTool extends MongoDBToolBase {
1113
protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
1214
const provider = await this.ensureConnected();
1315
const indexes = await provider.getIndexes(database, collection);
14-
const searchIndexes = await provider.getSearchIndexes(database, collection);
16+
17+
let searchIndexes: Document[];
18+
try {
19+
searchIndexes = await provider.getSearchIndexes(database, collection);
20+
} catch (error) {
21+
if (error instanceof MongoServerError && error.codeName === "SearchNotEnabled") {
22+
// If search is not enabled (e.g. due to connecting to a non-Atlas cluster), we can ignore the error
23+
// and return an empty array for search indexes.
24+
searchIndexes = [];
25+
} else {
26+
throw error;
27+
}
28+
}
1529

1630
return {
1731
content: [
1832
{
19-
text: `Found ${indexes.length} indexes in the collection "${collection}":`,
33+
text: `Found ${indexes.length + searchIndexes.length} indexes in the collection "${collection}":`,
2034
type: "text",
2135
},
2236
...indexes.map((indexDefinition) => {

tests/integration/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,7 @@ export function validateThrowsForInvalidArguments(
224224
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
225225
expect(arg).toBeDefined();
226226
}
227+
228+
export function sleep(ms: number) {
229+
return new Promise((resolve) => setTimeout(resolve, ms));
230+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ObjectId } from "bson";
2+
import { defaultTestConfig, IntegrationTest, setupIntegrationTest } from "../../helpers.js";
3+
import { deleteAndWaitCluster, waitClusterState, withProject } from "../atlas/atlasHelpers.js";
4+
5+
export function describeWithAtlasSearch(
6+
name: string,
7+
fn: (integration: IntegrationTest & { connectMcpClient: () => Promise<void> }) => void
8+
): void {
9+
const describeFn =
10+
process.env.MDB_MCP_API_CLIENT_ID?.length && process.env.MDB_MCP_API_CLIENT_SECRET?.length
11+
? describe
12+
: describe.skip;
13+
14+
describeFn("atlas-search", () => {
15+
const integration = setupIntegrationTest(() => ({
16+
...defaultTestConfig,
17+
apiClientId: process.env.MDB_MCP_API_CLIENT_ID,
18+
apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET,
19+
}));
20+
21+
describe(name, () => {
22+
withProject(integration, ({ getProjectId }) => {
23+
const clusterName = "ClusterTest-" + new ObjectId().toString();
24+
beforeAll(async () => {
25+
const projectId = getProjectId();
26+
27+
await integration.mcpClient().callTool({
28+
name: "atlas-create-free-cluster",
29+
arguments: {
30+
projectId,
31+
name: clusterName,
32+
region: "US_EAST_1",
33+
},
34+
});
35+
36+
await waitClusterState(integration.mcpServer().session, projectId, clusterName, "IDLE");
37+
await integration.mcpServer().session.apiClient.createProjectIpAccessList({
38+
params: {
39+
path: {
40+
groupId: projectId,
41+
},
42+
},
43+
body: [
44+
{
45+
comment: "MCP test",
46+
cidrBlock: "0.0.0.0/0",
47+
},
48+
],
49+
});
50+
});
51+
52+
afterAll(async () => {
53+
const projectId = getProjectId();
54+
55+
const session = integration.mcpServer().session;
56+
57+
await deleteAndWaitCluster(session, projectId, clusterName);
58+
});
59+
60+
fn({
61+
...integration,
62+
connectMcpClient: async () => {
63+
await integration.mcpClient().callTool({
64+
name: "atlas-connect-cluster",
65+
arguments: { projectId: getProjectId(), clusterName },
66+
});
67+
68+
expect(integration.mcpServer().session.connectedAtlasCluster).toBeDefined();
69+
expect(integration.mcpServer().session.serviceProvider).toBeDefined();
70+
},
71+
});
72+
});
73+
});
74+
});
75+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ObjectId } from "bson";
2+
import { expectDefined, getResponseElements } from "../../../helpers.js";
3+
import { describeWithAtlasSearch } from "../atlasSearchHelpers.js";
4+
5+
describeWithAtlasSearch("collectionIndexes tool", (integration) => {
6+
it("can inspect search indexes", async () => {
7+
await integration.connectMcpClient();
8+
9+
const provider = integration.mcpServer().session.serviceProvider;
10+
expectDefined(provider);
11+
12+
const database = new ObjectId().toString();
13+
14+
await provider.mongoClient
15+
.db(database)
16+
.collection("coll1")
17+
.insertMany([
18+
{ name: "Alice", age: 30 },
19+
{ name: "Bob", age: 25 },
20+
{ name: "Charlie", age: 35 },
21+
]);
22+
23+
const name = await provider.mongoClient
24+
.db(database)
25+
.collection("coll1")
26+
.createSearchIndex({
27+
name: "searchIndex1",
28+
definition: {
29+
mappings: {
30+
dynamic: true,
31+
},
32+
analyzer: "lucene.danish",
33+
},
34+
});
35+
36+
const response = await integration.mcpClient().callTool({
37+
name: "collection-indexes",
38+
arguments: { database, collection: "coll1" },
39+
});
40+
41+
const elements = getResponseElements(response.content);
42+
expect(elements).toHaveLength(3);
43+
expect(elements[0].text).toEqual(`Found 2 indexes in the collection "coll1":`);
44+
expect(elements[1].text).toEqual('Name "_id_", definition: {"_id":1}');
45+
expect(elements[2].text).toContain(`Search index name: "${name}"`);
46+
expect(elements[2].text).toContain('"analyzer":"lucene.danish"');
47+
});
48+
});

tests/integration/tools/atlas/atlasHelpers.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ObjectId } from "mongodb";
22
import { Group } from "../../../../src/common/atlas/openapi.js";
33
import { ApiClient } from "../../../../src/common/atlas/apiClient.js";
4-
import { setupIntegrationTest, IntegrationTest, defaultTestConfig } from "../../helpers.js";
4+
import { setupIntegrationTest, IntegrationTest, defaultTestConfig, sleep } from "../../helpers.js";
5+
import { Session } from "../../../../src/session.js";
56

67
export type IntegrationTestFunction = (integration: IntegrationTest) => void;
78

8-
export function describeWithAtlas(name: string, fn: IntegrationTestFunction) {
9+
export function describeWithAtlas(name: string, fn: IntegrationTestFunction): void {
910
const testDefinition = () => {
1011
const integration = setupIntegrationTest(() => ({
1112
...defaultTestConfig,
@@ -21,7 +22,8 @@ export function describeWithAtlas(name: string, fn: IntegrationTestFunction) {
2122
if (!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length) {
2223
return describe.skip("atlas", testDefinition);
2324
}
24-
return describe("atlas", testDefinition);
25+
26+
describe("atlas", testDefinition);
2527
}
2628

2729
interface ProjectTestArgs {
@@ -30,8 +32,8 @@ interface ProjectTestArgs {
3032

3133
type ProjectTestFunction = (args: ProjectTestArgs) => void;
3234

33-
export function withProject(integration: IntegrationTest, fn: ProjectTestFunction) {
34-
return describe("project", () => {
35+
export function withProject(integration: IntegrationTest, fn: ProjectTestFunction): void {
36+
describe("with project", () => {
3537
let projectId: string = "";
3638

3739
beforeAll(async () => {
@@ -57,9 +59,7 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio
5759
getProjectId: () => projectId,
5860
};
5961

60-
describe("with project", () => {
61-
fn(args);
62-
});
62+
fn(args);
6363
});
6464
}
6565

@@ -104,3 +104,46 @@ async function createProject(apiClient: ApiClient): Promise<Group> {
104104

105105
return group;
106106
}
107+
108+
export async function waitClusterState(session: Session, projectId: string, clusterName: string, state: string) {
109+
while (true) {
110+
const cluster = await session.apiClient.getCluster({
111+
params: {
112+
path: {
113+
groupId: projectId,
114+
clusterName,
115+
},
116+
},
117+
});
118+
if (cluster?.stateName === state) {
119+
return;
120+
}
121+
await sleep(1000);
122+
}
123+
}
124+
125+
export async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string) {
126+
await session.apiClient.deleteCluster({
127+
params: {
128+
path: {
129+
groupId: projectId,
130+
clusterName,
131+
},
132+
},
133+
});
134+
while (true) {
135+
try {
136+
await session.apiClient.getCluster({
137+
params: {
138+
path: {
139+
groupId: projectId,
140+
clusterName,
141+
},
142+
},
143+
});
144+
await sleep(1000);
145+
} catch {
146+
break;
147+
}
148+
}
149+
}

tests/integration/tools/atlas/clusters.test.ts

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,8 @@
11
import { Session } from "../../../../src/session.js";
22
import { expectDefined } from "../../helpers.js";
3-
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
3+
import { describeWithAtlas, withProject, randomId, waitClusterState, deleteAndWaitCluster } from "./atlasHelpers.js";
44
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
55

6-
function sleep(ms: number) {
7-
return new Promise((resolve) => setTimeout(resolve, ms));
8-
}
9-
10-
async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string) {
11-
await session.apiClient.deleteCluster({
12-
params: {
13-
path: {
14-
groupId: projectId,
15-
clusterName,
16-
},
17-
},
18-
});
19-
while (true) {
20-
try {
21-
await session.apiClient.getCluster({
22-
params: {
23-
path: {
24-
groupId: projectId,
25-
clusterName,
26-
},
27-
},
28-
});
29-
await sleep(1000);
30-
} catch {
31-
break;
32-
}
33-
}
34-
}
35-
36-
async function waitClusterState(session: Session, projectId: string, clusterName: string, state: string) {
37-
while (true) {
38-
const cluster = await session.apiClient.getCluster({
39-
params: {
40-
path: {
41-
groupId: projectId,
42-
clusterName,
43-
},
44-
},
45-
});
46-
if (cluster?.stateName === state) {
47-
return;
48-
}
49-
await sleep(1000);
50-
}
51-
}
52-
536
describeWithAtlas("clusters", (integration) => {
547
withProject(integration, ({ getProjectId }) => {
558
const clusterName = "ClusterTest-" + randomId;

0 commit comments

Comments
 (0)