mongodb-mcp-server 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +11 -0
- package/.github/CODEOWNERS +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
- package/.github/workflows/check-pr-title.yml +29 -0
- package/.github/workflows/{lint.yml → check.yml} +22 -1
- package/.github/workflows/code_health.yaml +0 -22
- package/.github/workflows/code_health_fork.yaml +7 -63
- package/.github/workflows/docker.yaml +57 -0
- package/.github/workflows/stale.yml +32 -0
- package/.smithery/Dockerfile +30 -0
- package/.smithery/smithery.yaml +63 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +11 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +10 -0
- package/README.md +173 -35
- package/dist/common/atlas/apiClient.js +151 -35
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/common/atlas/apiClientError.js +38 -5
- package/dist/common/atlas/apiClientError.js.map +1 -1
- package/dist/common/atlas/cluster.js +66 -0
- package/dist/common/atlas/cluster.js.map +1 -0
- package/dist/common/atlas/generatePassword.js +9 -0
- package/dist/common/atlas/generatePassword.js.map +1 -0
- package/dist/helpers/EJsonTransport.js +38 -0
- package/dist/helpers/EJsonTransport.js.map +1 -0
- package/dist/helpers/connectionOptions.js +10 -0
- package/dist/helpers/connectionOptions.js.map +1 -0
- package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
- package/dist/helpers/packageInfo.js.map +1 -0
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.js +7 -0
- package/dist/logger.js.map +1 -1
- package/dist/server.js +16 -12
- package/dist/server.js.map +1 -1
- package/dist/session.js +8 -3
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +1 -3
- package/dist/telemetry/constants.js.map +1 -1
- package/dist/telemetry/eventCache.js.map +1 -1
- package/dist/telemetry/telemetry.js +126 -47
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/atlas/atlasTool.js +38 -0
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/create/createDBUser.js +19 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -1
- package/dist/tools/atlas/create/createProject.js +5 -1
- package/dist/tools/atlas/create/createProject.js.map +1 -1
- package/dist/tools/atlas/metadata/connectCluster.js +5 -22
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
- package/dist/tools/atlas/read/inspectCluster.js +4 -24
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
- package/dist/tools/atlas/read/listAlerts.js +41 -0
- package/dist/tools/atlas/read/listAlerts.js.map +1 -0
- package/dist/tools/atlas/read/listClusters.js +9 -18
- package/dist/tools/atlas/read/listClusters.js.map +1 -1
- package/dist/tools/atlas/read/listProjects.js +3 -1
- package/dist/tools/atlas/read/listProjects.js.map +1 -1
- package/dist/tools/atlas/tools.js +2 -0
- package/dist/tools/atlas/tools.js.map +1 -1
- package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +2 -2
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/mongodb/tools.js +2 -4
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/dist/tools/tool.js +38 -6
- package/dist/tools/tool.js.map +1 -1
- package/eslint.config.js +2 -1
- package/{jest.config.ts → jest.config.cjs} +1 -1
- package/package.json +11 -9
- package/scripts/apply.ts +8 -5
- package/scripts/filter.ts +5 -0
- package/src/common/atlas/apiClient.ts +190 -38
- package/src/common/atlas/apiClientError.ts +58 -7
- package/src/common/atlas/cluster.ts +94 -0
- package/src/common/atlas/generatePassword.ts +10 -0
- package/src/common/atlas/openapi.d.ts +1876 -239
- package/src/helpers/EJsonTransport.ts +47 -0
- package/src/helpers/connectionOptions.ts +20 -0
- package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
- package/src/index.ts +27 -3
- package/src/logger.ts +8 -0
- package/src/server.ts +23 -15
- package/src/session.ts +8 -4
- package/src/telemetry/constants.ts +2 -3
- package/src/telemetry/eventCache.ts +1 -1
- package/src/telemetry/telemetry.ts +182 -64
- package/src/telemetry/types.ts +1 -1
- package/src/tools/atlas/atlasTool.ts +47 -1
- package/src/tools/atlas/create/createDBUser.ts +22 -2
- package/src/tools/atlas/create/createProject.ts +7 -1
- package/src/tools/atlas/metadata/connectCluster.ts +5 -27
- package/src/tools/atlas/read/inspectCluster.ts +4 -40
- package/src/tools/atlas/read/listAlerts.ts +45 -0
- package/src/tools/atlas/read/listClusters.ts +19 -36
- package/src/tools/atlas/read/listProjects.ts +4 -2
- package/src/tools/atlas/tools.ts +2 -0
- package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
- package/src/tools/mongodb/read/count.ts +3 -2
- package/src/tools/mongodb/tools.ts +2 -4
- package/src/tools/tool.ts +45 -8
- package/src/types/mongodb-connection-string-url.d.ts +69 -0
- package/tests/integration/helpers.ts +41 -2
- package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
- package/tests/integration/tools/atlas/alerts.test.ts +42 -0
- package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
- package/tests/integration/tools/atlas/clusters.test.ts +4 -4
- package/tests/integration/tools/atlas/dbUsers.test.ts +58 -33
- package/tests/integration/tools/atlas/orgs.test.ts +2 -2
- package/tests/integration/tools/atlas/projects.test.ts +3 -3
- package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
- package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
- package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
- package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
- package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
- package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
- package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
- package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
- package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
- package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
- package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
- package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
- package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
- package/tests/integration/tools/mongodb/read/find.test.ts +32 -4
- package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
- package/tests/unit/EJsonTransport.test.ts +71 -0
- package/tests/unit/apiClient.test.ts +193 -0
- package/tests/unit/session.test.ts +65 -0
- package/tests/unit/telemetry.test.ts +222 -80
- package/tsconfig.build.json +2 -1
- package/dist/packageInfo.js.map +0 -1
- package/dist/telemetry/device-id.js +0 -20
- package/dist/telemetry/device-id.js.map +0 -1
- package/src/telemetry/device-id.ts +0 -21
|
@@ -67,7 +67,7 @@ describeWithAtlas("ip access lists", (integration) => {
|
|
|
67
67
|
})) as CallToolResult;
|
|
68
68
|
expect(response.content).toBeArray();
|
|
69
69
|
expect(response.content).toHaveLength(1);
|
|
70
|
-
expect(response.content[0]
|
|
70
|
+
expect(response.content[0]?.text).toContain("IP/CIDR ranges added to access list");
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
@@ -90,7 +90,7 @@ describeWithAtlas("ip access lists", (integration) => {
|
|
|
90
90
|
expect(response.content).toBeArray();
|
|
91
91
|
expect(response.content).toHaveLength(1);
|
|
92
92
|
for (const value of values) {
|
|
93
|
-
expect(response.content[0]
|
|
93
|
+
expect(response.content[0]?.text).toContain(value);
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { expectDefined } from "../../helpers.js";
|
|
3
|
+
import { parseTable, describeWithAtlas, withProject } from "./atlasHelpers.js";
|
|
4
|
+
|
|
5
|
+
describeWithAtlas("alerts", (integration) => {
|
|
6
|
+
describe("atlas-list-alerts", () => {
|
|
7
|
+
it("should have correct metadata", async () => {
|
|
8
|
+
const { tools } = await integration.mcpClient().listTools();
|
|
9
|
+
const listAlerts = tools.find((tool) => tool.name === "atlas-list-alerts");
|
|
10
|
+
expectDefined(listAlerts);
|
|
11
|
+
expect(listAlerts.inputSchema.type).toBe("object");
|
|
12
|
+
expectDefined(listAlerts.inputSchema.properties);
|
|
13
|
+
expect(listAlerts.inputSchema.properties).toHaveProperty("projectId");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
withProject(integration, ({ getProjectId }) => {
|
|
17
|
+
it("returns alerts in table format", async () => {
|
|
18
|
+
const response = (await integration.mcpClient().callTool({
|
|
19
|
+
name: "atlas-list-alerts",
|
|
20
|
+
arguments: { projectId: getProjectId() },
|
|
21
|
+
})) as CallToolResult;
|
|
22
|
+
|
|
23
|
+
expect(response.content).toBeArray();
|
|
24
|
+
expect(response.content).toHaveLength(1);
|
|
25
|
+
|
|
26
|
+
const data = parseTable(response.content[0]?.text as string);
|
|
27
|
+
expect(data).toBeArray();
|
|
28
|
+
|
|
29
|
+
// Since we can't guarantee alerts will exist, we just verify the table structure
|
|
30
|
+
if (data.length > 0) {
|
|
31
|
+
const alert = data[0];
|
|
32
|
+
expect(alert).toHaveProperty("Alert ID");
|
|
33
|
+
expect(alert).toHaveProperty("Status");
|
|
34
|
+
expect(alert).toHaveProperty("Created");
|
|
35
|
+
expect(alert).toHaveProperty("Updated");
|
|
36
|
+
expect(alert).toHaveProperty("Type");
|
|
37
|
+
expect(alert).toHaveProperty("Comment");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -75,7 +75,9 @@ export function parseTable(text: string): Record<string, string>[] {
|
|
|
75
75
|
.map((cells) => {
|
|
76
76
|
const row: Record<string, string> = {};
|
|
77
77
|
cells.forEach((cell, index) => {
|
|
78
|
-
|
|
78
|
+
if (headers) {
|
|
79
|
+
row[headers[index] ?? ""] = cell;
|
|
80
|
+
}
|
|
79
81
|
});
|
|
80
82
|
return row;
|
|
81
83
|
});
|
|
@@ -87,14 +89,14 @@ async function createProject(apiClient: ApiClient): Promise<Group> {
|
|
|
87
89
|
const projectName: string = `testProj-` + randomId;
|
|
88
90
|
|
|
89
91
|
const orgs = await apiClient.listOrganizations();
|
|
90
|
-
if (!orgs?.results?.length || !orgs.results[0]
|
|
92
|
+
if (!orgs?.results?.length || !orgs.results[0]?.id) {
|
|
91
93
|
throw new Error("No orgs found");
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const group = await apiClient.createProject({
|
|
95
97
|
body: {
|
|
96
98
|
name: projectName,
|
|
97
|
-
orgId: orgs.results[0]
|
|
99
|
+
orgId: orgs.results[0]?.id ?? "",
|
|
98
100
|
} as Group,
|
|
99
101
|
});
|
|
100
102
|
|
|
@@ -88,7 +88,7 @@ describeWithAtlas("clusters", (integration) => {
|
|
|
88
88
|
})) as CallToolResult;
|
|
89
89
|
expect(response.content).toBeArray();
|
|
90
90
|
expect(response.content).toHaveLength(2);
|
|
91
|
-
expect(response.content[0]
|
|
91
|
+
expect(response.content[0]?.text).toContain("has been created");
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -113,7 +113,7 @@ describeWithAtlas("clusters", (integration) => {
|
|
|
113
113
|
})) as CallToolResult;
|
|
114
114
|
expect(response.content).toBeArray();
|
|
115
115
|
expect(response.content).toHaveLength(1);
|
|
116
|
-
expect(response.content[0]
|
|
116
|
+
expect(response.content[0]?.text).toContain(`${clusterName} | `);
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
|
|
@@ -135,7 +135,7 @@ describeWithAtlas("clusters", (integration) => {
|
|
|
135
135
|
.callTool({ name: "atlas-list-clusters", arguments: { projectId } })) as CallToolResult;
|
|
136
136
|
expect(response.content).toBeArray();
|
|
137
137
|
expect(response.content).toHaveLength(2);
|
|
138
|
-
expect(response.content[1]
|
|
138
|
+
expect(response.content[1]?.text).toContain(`${clusterName} | `);
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
141
|
|
|
@@ -178,7 +178,7 @@ describeWithAtlas("clusters", (integration) => {
|
|
|
178
178
|
})) as CallToolResult;
|
|
179
179
|
expect(response.content).toBeArray();
|
|
180
180
|
expect(response.content).toHaveLength(1);
|
|
181
|
-
expect(response.content[0]
|
|
181
|
+
expect(response.content[0]?.text).toContain(`Connected to cluster "${clusterName}"`);
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
});
|
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { Session } from "../../../../src/session.js";
|
|
3
2
|
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
|
|
4
|
-
import { expectDefined } from "../../helpers.js";
|
|
3
|
+
import { expectDefined, getResponseElements } from "../../helpers.js";
|
|
4
|
+
import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js";
|
|
5
5
|
|
|
6
6
|
describeWithAtlas("db users", (integration) => {
|
|
7
|
-
const userName = "testuser-" + randomId;
|
|
8
7
|
withProject(integration, ({ getProjectId }) => {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
let userName: string;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
userName = "testuser-" + randomId;
|
|
11
|
+
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
await
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
const createUserWithMCP = async (password?: string): Promise<unknown> => {
|
|
14
|
+
return await integration.mcpClient().callTool({
|
|
15
|
+
name: "atlas-create-db-user",
|
|
16
|
+
arguments: {
|
|
17
|
+
projectId: getProjectId(),
|
|
18
|
+
username: userName,
|
|
19
|
+
password,
|
|
20
|
+
roles: [
|
|
21
|
+
{
|
|
22
|
+
roleName: "readWrite",
|
|
23
|
+
databaseName: "admin",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
20
26
|
},
|
|
21
27
|
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
try {
|
|
32
|
+
await integration.mcpServer().session.apiClient.deleteDatabaseUser({
|
|
33
|
+
params: {
|
|
34
|
+
path: {
|
|
35
|
+
groupId: getProjectId(),
|
|
36
|
+
username: userName,
|
|
37
|
+
databaseName: "admin",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Ignore 404 errors when deleting the user
|
|
43
|
+
if (!(error instanceof ApiClientError) || error.response?.status !== 404) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
});
|
|
23
48
|
|
|
24
49
|
describe("atlas-create-db-user", () => {
|
|
@@ -34,26 +59,24 @@ describeWithAtlas("db users", (integration) => {
|
|
|
34
59
|
expect(createDbUser.inputSchema.properties).toHaveProperty("roles");
|
|
35
60
|
expect(createDbUser.inputSchema.properties).toHaveProperty("clusters");
|
|
36
61
|
});
|
|
37
|
-
it("should create a database user", async () => {
|
|
38
|
-
const projectId = getProjectId();
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
expect(
|
|
63
|
+
it("should create a database user with supplied password", async () => {
|
|
64
|
+
const response = await createUserWithMCP("testpassword");
|
|
65
|
+
|
|
66
|
+
const elements = getResponseElements(response);
|
|
67
|
+
expect(elements).toHaveLength(1);
|
|
68
|
+
expect(elements[0]?.text).toContain("created successfully");
|
|
69
|
+
expect(elements[0]?.text).toContain(userName);
|
|
70
|
+
expect(elements[0]?.text).not.toContain("testpassword");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should create a database user with generated password", async () => {
|
|
74
|
+
const response = await createUserWithMCP();
|
|
75
|
+
const elements = getResponseElements(response);
|
|
76
|
+
expect(elements).toHaveLength(1);
|
|
77
|
+
expect(elements[0]?.text).toContain("created successfully");
|
|
78
|
+
expect(elements[0]?.text).toContain(userName);
|
|
79
|
+
expect(elements[0]?.text).toContain("with password: `");
|
|
57
80
|
});
|
|
58
81
|
});
|
|
59
82
|
describe("atlas-list-db-users", () => {
|
|
@@ -68,12 +91,14 @@ describeWithAtlas("db users", (integration) => {
|
|
|
68
91
|
it("returns database users by project", async () => {
|
|
69
92
|
const projectId = getProjectId();
|
|
70
93
|
|
|
94
|
+
await createUserWithMCP();
|
|
95
|
+
|
|
71
96
|
const response = (await integration
|
|
72
97
|
.mcpClient()
|
|
73
98
|
.callTool({ name: "atlas-list-db-users", arguments: { projectId } })) as CallToolResult;
|
|
74
99
|
expect(response.content).toBeArray();
|
|
75
100
|
expect(response.content).toHaveLength(1);
|
|
76
|
-
expect(response.content[0]
|
|
101
|
+
expect(response.content[0]?.text).toContain(userName);
|
|
77
102
|
});
|
|
78
103
|
});
|
|
79
104
|
});
|
|
@@ -16,9 +16,9 @@ describeWithAtlas("orgs", (integration) => {
|
|
|
16
16
|
.callTool({ name: "atlas-list-orgs", arguments: {} })) as CallToolResult;
|
|
17
17
|
expect(response.content).toBeArray();
|
|
18
18
|
expect(response.content).toHaveLength(1);
|
|
19
|
-
const data = parseTable(response.content[0]
|
|
19
|
+
const data = parseTable(response.content[0]?.text as string);
|
|
20
20
|
expect(data).toHaveLength(1);
|
|
21
|
-
expect(data[0]["Organization Name"]).toEqual("MongoDB MCP Test");
|
|
21
|
+
expect(data[0]?.["Organization Name"]).toEqual("MongoDB MCP Test");
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
});
|
|
@@ -43,7 +43,7 @@ describeWithAtlas("projects", (integration) => {
|
|
|
43
43
|
})) as CallToolResult;
|
|
44
44
|
expect(response.content).toBeArray();
|
|
45
45
|
expect(response.content).toHaveLength(1);
|
|
46
|
-
expect(response.content[0]
|
|
46
|
+
expect(response.content[0]?.text).toContain(projName);
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
describe("atlas-list-projects", () => {
|
|
@@ -62,8 +62,8 @@ describeWithAtlas("projects", (integration) => {
|
|
|
62
62
|
.callTool({ name: "atlas-list-projects", arguments: {} })) as CallToolResult;
|
|
63
63
|
expect(response.content).toBeArray();
|
|
64
64
|
expect(response.content).toHaveLength(1);
|
|
65
|
-
expect(response.content[0]
|
|
66
|
-
const data = parseTable(response.content[0]
|
|
65
|
+
expect(response.content[0]?.text).toContain(projName);
|
|
66
|
+
const data = parseTable(response.content[0]?.text as string);
|
|
67
67
|
expect(data).toBeArray();
|
|
68
68
|
expect(data.length).toBeGreaterThan(0);
|
|
69
69
|
let found = false;
|
|
@@ -34,7 +34,7 @@ describeWithMongoDB("createCollection tool", (integration) => {
|
|
|
34
34
|
|
|
35
35
|
collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray();
|
|
36
36
|
expect(collections).toHaveLength(1);
|
|
37
|
-
expect(collections[0]
|
|
37
|
+
expect(collections[0]?.name).toEqual("bar");
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -78,7 +78,7 @@ describeWithMongoDB("createCollection tool", (integration) => {
|
|
|
78
78
|
expect(content).toEqual(`Collection "collection1" created in database "${integration.randomDbName()}".`);
|
|
79
79
|
collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray();
|
|
80
80
|
expect(collections).toHaveLength(1);
|
|
81
|
-
expect(collections[0]
|
|
81
|
+
expect(collections[0]?.name).toEqual("collection1");
|
|
82
82
|
|
|
83
83
|
// Make sure we didn't drop the existing collection
|
|
84
84
|
documents = await mongoClient.db(integration.randomDbName()).collection("collection1").find({}).toArray();
|
|
@@ -38,10 +38,10 @@ describeWithMongoDB("createIndex tool", (integration) => {
|
|
|
38
38
|
const mongoClient = integration.mongoClient();
|
|
39
39
|
const collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray();
|
|
40
40
|
expect(collections).toHaveLength(1);
|
|
41
|
-
expect(collections[0]
|
|
41
|
+
expect(collections[0]?.name).toEqual("coll1");
|
|
42
42
|
const indexes = await mongoClient.db(integration.randomDbName()).collection(collection).indexes();
|
|
43
43
|
expect(indexes).toHaveLength(expected.length + 1);
|
|
44
|
-
expect(indexes[0]
|
|
44
|
+
expect(indexes[0]?.name).toEqual("_id_");
|
|
45
45
|
for (const index of expected) {
|
|
46
46
|
const foundIndex = indexes.find((i) => i.name === index.name);
|
|
47
47
|
expectDefined(foundIndex);
|
|
@@ -82,7 +82,7 @@ describeWithMongoDB("insertMany tool", (integration) => {
|
|
|
82
82
|
const content = getResponseContent(response.content);
|
|
83
83
|
expect(content).toContain("Error running insert-many");
|
|
84
84
|
expect(content).toContain("duplicate key error");
|
|
85
|
-
expect(content).toContain(insertedIds[0]
|
|
85
|
+
expect(content).toContain(insertedIds[0]?.toString());
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
validateAutoConnectBehavior(integration, "insert-many", () => {
|
|
@@ -54,7 +54,7 @@ describeWithMongoDB("dropCollection tool", (integration) => {
|
|
|
54
54
|
);
|
|
55
55
|
const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray();
|
|
56
56
|
expect(collections).toHaveLength(1);
|
|
57
|
-
expect(collections[0]
|
|
57
|
+
expect(collections[0]?.name).toBe("coll2");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
validateAutoConnectBehavior(integration, "drop-collection", () => {
|
|
@@ -132,11 +132,11 @@ describeWithMongoDB("collectionSchema tool", (integration) => {
|
|
|
132
132
|
expect(items).toHaveLength(2);
|
|
133
133
|
|
|
134
134
|
// Expect to find _id, name, age
|
|
135
|
-
expect(items[0]
|
|
135
|
+
expect(items[0]?.text).toEqual(
|
|
136
136
|
`Found ${Object.entries(testCase.expectedSchema).length} fields in the schema for "${integration.randomDbName()}.foo"`
|
|
137
137
|
);
|
|
138
138
|
|
|
139
|
-
const schema = JSON.parse(items[1]
|
|
139
|
+
const schema = JSON.parse(items[1]?.text ?? "{}") as SimplifiedSchema;
|
|
140
140
|
expect(schema).toEqual(testCase.expectedSchema);
|
|
141
141
|
});
|
|
142
142
|
}
|
|
@@ -2,8 +2,6 @@ import { describeWithMongoDB } from "../mongodbHelpers.js";
|
|
|
2
2
|
import { getResponseContent, validateThrowsForInvalidArguments, validateToolMetadata } from "../../../helpers.js";
|
|
3
3
|
import { config } from "../../../../../src/config.js";
|
|
4
4
|
|
|
5
|
-
// These tests are temporarily skipped because the connect tool is disabled for the initial release.
|
|
6
|
-
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
|
|
7
5
|
describeWithMongoDB(
|
|
8
6
|
"switchConnection tool",
|
|
9
7
|
(integration) => {
|
|
@@ -77,8 +75,7 @@ describeWithMongoDB(
|
|
|
77
75
|
(mdbIntegration) => ({
|
|
78
76
|
...config,
|
|
79
77
|
connectionString: mdbIntegration.connectionString(),
|
|
80
|
-
})
|
|
81
|
-
describe.skip
|
|
78
|
+
})
|
|
82
79
|
);
|
|
83
80
|
describeWithMongoDB(
|
|
84
81
|
"Connect tool",
|
|
@@ -127,6 +124,5 @@ describeWithMongoDB(
|
|
|
127
124
|
});
|
|
128
125
|
});
|
|
129
126
|
},
|
|
130
|
-
() => config
|
|
131
|
-
describe.skip
|
|
127
|
+
() => config
|
|
132
128
|
);
|
|
@@ -28,9 +28,9 @@ describeWithMongoDB("dbStats tool", (integration) => {
|
|
|
28
28
|
});
|
|
29
29
|
const elements = getResponseElements(response.content);
|
|
30
30
|
expect(elements).toHaveLength(2);
|
|
31
|
-
expect(elements[0]
|
|
31
|
+
expect(elements[0]?.text).toBe(`Statistics for database ${integration.randomDbName()}`);
|
|
32
32
|
|
|
33
|
-
const stats = JSON.parse(elements[1]
|
|
33
|
+
const stats = JSON.parse(elements[1]?.text ?? "{}") as {
|
|
34
34
|
db: string;
|
|
35
35
|
collections: number;
|
|
36
36
|
storageSize: number;
|
|
@@ -75,9 +75,9 @@ describeWithMongoDB("dbStats tool", (integration) => {
|
|
|
75
75
|
});
|
|
76
76
|
const elements = getResponseElements(response.content);
|
|
77
77
|
expect(elements).toHaveLength(2);
|
|
78
|
-
expect(elements[0]
|
|
78
|
+
expect(elements[0]?.text).toBe(`Statistics for database ${integration.randomDbName()}`);
|
|
79
79
|
|
|
80
|
-
const stats = JSON.parse(elements[1]
|
|
80
|
+
const stats = JSON.parse(elements[1]?.text ?? "{}") as {
|
|
81
81
|
db: string;
|
|
82
82
|
collections: unknown;
|
|
83
83
|
storageSize: unknown;
|
|
@@ -89,12 +89,12 @@ describeWithMongoDB("explain tool", (integration) => {
|
|
|
89
89
|
|
|
90
90
|
const content = getResponseElements(response.content);
|
|
91
91
|
expect(content).toHaveLength(2);
|
|
92
|
-
expect(content[0]
|
|
92
|
+
expect(content[0]?.text).toEqual(
|
|
93
93
|
`Here is some information about the winning plan chosen by the query optimizer for running the given \`${testCase.method}\` operation in "${integration.randomDbName()}.coll1". This information can be used to understand how the query was executed and to optimize the query performance.`
|
|
94
94
|
);
|
|
95
95
|
|
|
96
|
-
expect(content[1]
|
|
97
|
-
expect(content[1]
|
|
96
|
+
expect(content[1]?.text).toContain("queryPlanner");
|
|
97
|
+
expect(content[1]?.text).toContain("winningPlan");
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
});
|
|
@@ -139,22 +139,22 @@ describeWithMongoDB("explain tool", (integration) => {
|
|
|
139
139
|
|
|
140
140
|
const content = getResponseElements(response.content);
|
|
141
141
|
expect(content).toHaveLength(2);
|
|
142
|
-
expect(content[0]
|
|
142
|
+
expect(content[0]?.text).toEqual(
|
|
143
143
|
`Here is some information about the winning plan chosen by the query optimizer for running the given \`${testCase.method}\` operation in "${integration.randomDbName()}.people". This information can be used to understand how the query was executed and to optimize the query performance.`
|
|
144
144
|
);
|
|
145
145
|
|
|
146
|
-
expect(content[1]
|
|
147
|
-
expect(content[1]
|
|
146
|
+
expect(content[1]?.text).toContain("queryPlanner");
|
|
147
|
+
expect(content[1]?.text).toContain("winningPlan");
|
|
148
148
|
|
|
149
149
|
if (indexed) {
|
|
150
150
|
if (testCase.method === "count") {
|
|
151
|
-
expect(content[1]
|
|
151
|
+
expect(content[1]?.text).toContain("COUNT_SCAN");
|
|
152
152
|
} else {
|
|
153
|
-
expect(content[1]
|
|
153
|
+
expect(content[1]?.text).toContain("IXSCAN");
|
|
154
154
|
}
|
|
155
|
-
expect(content[1]
|
|
155
|
+
expect(content[1]?.text).toContain("name_1");
|
|
156
156
|
} else {
|
|
157
|
-
expect(content[1]
|
|
157
|
+
expect(content[1]?.text).toContain("COLLSCAN");
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
}
|
|
@@ -45,7 +45,7 @@ describeWithMongoDB("listCollections tool", (integration) => {
|
|
|
45
45
|
});
|
|
46
46
|
const items = getResponseElements(response.content);
|
|
47
47
|
expect(items).toHaveLength(1);
|
|
48
|
-
expect(items[0]
|
|
48
|
+
expect(items[0]?.text).toContain('Name: "collection-1"');
|
|
49
49
|
|
|
50
50
|
await mongoClient.db(integration.randomDbName()).createCollection("collection-2");
|
|
51
51
|
|
|
@@ -65,9 +65,13 @@ describeWithMongoDB("listDatabases tool", (integration) => {
|
|
|
65
65
|
|
|
66
66
|
function getDbNames(content: unknown): (string | null)[] {
|
|
67
67
|
const responseItems = getResponseElements(content);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
return responseItems
|
|
69
|
+
.map((item) => {
|
|
70
|
+
if (item && typeof item.text === "string") {
|
|
71
|
+
const match = item.text.match(/Name: ([^,]+), Size: \d+ bytes/);
|
|
72
|
+
return match ? match[1] : null;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
})
|
|
76
|
+
.filter((item): item is string | null => item !== undefined);
|
|
73
77
|
}
|
|
@@ -37,11 +37,11 @@ describeWithMongoDB("logs tool", (integration) => {
|
|
|
37
37
|
|
|
38
38
|
// Default limit is 50
|
|
39
39
|
expect(elements.length).toBeLessThanOrEqual(51);
|
|
40
|
-
expect(elements[0]
|
|
40
|
+
expect(elements[0]?.text).toMatch(/Found: \d+ messages/);
|
|
41
41
|
|
|
42
42
|
for (let i = 1; i < elements.length; i++) {
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
44
|
-
const log = JSON.parse(elements[i]
|
|
44
|
+
const log = JSON.parse(elements[i]?.text ?? "{}");
|
|
45
45
|
expect(log).toHaveProperty("t");
|
|
46
46
|
expect(log).toHaveProperty("msg");
|
|
47
47
|
}
|
|
@@ -59,7 +59,7 @@ describeWithMongoDB("logs tool", (integration) => {
|
|
|
59
59
|
const elements = getResponseElements(response);
|
|
60
60
|
expect(elements.length).toBeLessThanOrEqual(51);
|
|
61
61
|
for (let i = 1; i < elements.length; i++) {
|
|
62
|
-
const log = JSON.parse(elements[i]
|
|
62
|
+
const log = JSON.parse(elements[i]?.text ?? "{}") as { tags: string[] };
|
|
63
63
|
expect(log).toHaveProperty("t");
|
|
64
64
|
expect(log).toHaveProperty("msg");
|
|
65
65
|
expect(log).toHaveProperty("tags");
|
|
@@ -76,7 +76,7 @@ describeWithMongoDB("logs tool", (integration) => {
|
|
|
76
76
|
validate: (content) => {
|
|
77
77
|
const elements = getResponseElements(content);
|
|
78
78
|
expect(elements.length).toBeLessThanOrEqual(51);
|
|
79
|
-
expect(elements[0]
|
|
79
|
+
expect(elements[0]?.text).toMatch(/Found: \d+ messages/);
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
});
|
|
@@ -17,42 +17,32 @@ interface MongoDBIntegrationTest {
|
|
|
17
17
|
export function describeWithMongoDB(
|
|
18
18
|
name: string,
|
|
19
19
|
fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise<void> }) => void,
|
|
20
|
-
getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig
|
|
21
|
-
describeFn = describe
|
|
20
|
+
getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig
|
|
22
21
|
) {
|
|
23
|
-
|
|
22
|
+
describe(name, () => {
|
|
24
23
|
const mdbIntegration = setupMongoDBIntegrationTest();
|
|
25
24
|
const integration = setupIntegrationTest(() => ({
|
|
26
25
|
...getUserConfig(mdbIntegration),
|
|
27
|
-
connectionString: mdbIntegration.connectionString(),
|
|
28
26
|
}));
|
|
29
27
|
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
integration.mcpServer().userConfig.connectionString = mdbIntegration.connectionString();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
28
|
fn({
|
|
35
29
|
...integration,
|
|
36
30
|
...mdbIntegration,
|
|
37
31
|
connectMcpClient: async () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
const { tools } = await integration.mcpClient().listTools();
|
|
33
|
+
if (tools.find((tool) => tool.name === "connect")) {
|
|
34
|
+
await integration.mcpClient().callTool({
|
|
35
|
+
name: "connect",
|
|
36
|
+
arguments: { connectionString: mdbIntegration.connectionString() },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
44
39
|
},
|
|
45
40
|
});
|
|
46
41
|
});
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
|
|
50
|
-
let mongoCluster:
|
|
51
|
-
| {
|
|
52
|
-
connectionString: string;
|
|
53
|
-
close: () => Promise<void>;
|
|
54
|
-
}
|
|
55
|
-
| undefined;
|
|
45
|
+
let mongoCluster: MongoCluster | undefined;
|
|
56
46
|
let mongoClient: MongoClient | undefined;
|
|
57
47
|
let randomDbName: string;
|
|
58
48
|
|
|
@@ -76,8 +66,6 @@ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
|
|
|
76
66
|
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
|
|
77
67
|
for (let i = 0; i < 10; i++) {
|
|
78
68
|
try {
|
|
79
|
-
// TODO: Fix this type once mongodb-runner is updated.
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
81
69
|
mongoCluster = await MongoCluster.start({
|
|
82
70
|
tmpDir: dbsDir,
|
|
83
71
|
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
|
|
@@ -141,12 +129,15 @@ export function validateAutoConnectBehavior(
|
|
|
141
129
|
},
|
|
142
130
|
beforeEachImpl?: () => Promise<void>
|
|
143
131
|
): void {
|
|
144
|
-
|
|
145
|
-
describe.skip("when not connected", () => {
|
|
132
|
+
describe("when not connected", () => {
|
|
146
133
|
if (beforeEachImpl) {
|
|
147
134
|
beforeEach(() => beforeEachImpl());
|
|
148
135
|
}
|
|
149
136
|
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
integration.mcpServer().userConfig.connectionString = undefined;
|
|
139
|
+
});
|
|
140
|
+
|
|
150
141
|
it("connects automatically if connection string is configured", async () => {
|
|
151
142
|
integration.mcpServer().userConfig.connectionString = integration.connectionString();
|
|
152
143
|
|