mongodb-mcp-server 0.0.4 → 0.0.6
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/.github/CODEOWNERS +3 -0
- package/.github/dependabot.yml +10 -0
- package/.github/workflows/code_health.yaml +53 -22
- package/.github/workflows/code_health_fork.yaml +106 -0
- package/.github/workflows/codeql.yml +34 -0
- package/.github/workflows/lint.yml +37 -0
- package/.github/workflows/prepare_release.yaml +6 -4
- package/.github/workflows/publish.yaml +6 -3
- package/.prettierrc.json +1 -1
- package/README.md +18 -0
- package/dist/common/atlas/apiClient.js +28 -4
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/config.js +4 -7
- package/dist/config.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js +12 -7
- package/dist/index.js.map +1 -1
- package/dist/logger.js +72 -28
- package/dist/logger.js.map +1 -1
- package/dist/packageInfo.js +6 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/server.js +114 -10
- package/dist/server.js.map +1 -1
- package/dist/session.js +66 -16
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +15 -0
- package/dist/telemetry/constants.js.map +1 -0
- package/dist/telemetry/eventCache.js +53 -0
- package/dist/telemetry/eventCache.js.map +1 -0
- package/dist/telemetry/telemetry.js +97 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/telemetry/types.js +2 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/tools/atlas/atlasTool.js +8 -3
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/{createAccessList.js → create/createAccessList.js} +1 -2
- package/dist/tools/atlas/create/createAccessList.js.map +1 -0
- package/dist/tools/atlas/{createDBUser.js → create/createDBUser.js} +1 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -0
- package/dist/tools/atlas/{createFreeCluster.js → create/createFreeCluster.js} +5 -3
- package/dist/tools/atlas/create/createFreeCluster.js.map +1 -0
- package/dist/tools/atlas/{createProject.js → create/createProject.js} +1 -2
- package/dist/tools/atlas/create/createProject.js.map +1 -0
- package/dist/tools/atlas/metadata/connectCluster.js +97 -0
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -0
- package/dist/tools/atlas/{inspectAccessList.js → read/inspectAccessList.js} +1 -2
- package/dist/tools/atlas/read/inspectAccessList.js.map +1 -0
- package/dist/tools/atlas/{inspectCluster.js → read/inspectCluster.js} +1 -2
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -0
- package/dist/tools/atlas/{listClusters.js → read/listClusters.js} +1 -2
- package/dist/tools/atlas/read/listClusters.js.map +1 -0
- package/dist/tools/atlas/{listDBUsers.js → read/listDBUsers.js} +1 -2
- package/dist/tools/atlas/read/listDBUsers.js.map +1 -0
- package/dist/tools/atlas/{listOrgs.js → read/listOrgs.js} +1 -2
- package/dist/tools/atlas/read/listOrgs.js.map +1 -0
- package/dist/tools/atlas/{listProjects.js → read/listProjects.js} +11 -5
- package/dist/tools/atlas/read/listProjects.js.map +1 -0
- package/dist/tools/atlas/tools.js +12 -10
- package/dist/tools/atlas/tools.js.map +1 -1
- package/dist/tools/mongodb/create/insertMany.js +1 -1
- package/dist/tools/mongodb/create/insertMany.js.map +1 -1
- package/dist/tools/mongodb/delete/deleteMany.js +1 -2
- package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
- package/dist/tools/mongodb/metadata/collectionSchema.js +15 -13
- package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -1
- package/dist/tools/mongodb/metadata/collectionStorageSize.js +32 -3
- package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -1
- package/dist/tools/mongodb/metadata/connect.js +59 -72
- package/dist/tools/mongodb/metadata/connect.js.map +1 -1
- package/dist/tools/mongodb/metadata/dbStats.js +6 -1
- package/dist/tools/mongodb/metadata/dbStats.js.map +1 -1
- package/dist/tools/mongodb/metadata/explain.js +14 -7
- package/dist/tools/mongodb/metadata/explain.js.map +1 -1
- package/dist/tools/mongodb/metadata/logs.js +45 -0
- package/dist/tools/mongodb/metadata/logs.js.map +1 -0
- package/dist/tools/mongodb/mongodbTool.js +42 -36
- package/dist/tools/mongodb/mongodbTool.js.map +1 -1
- package/dist/tools/mongodb/read/aggregate.js +4 -4
- package/dist/tools/mongodb/read/aggregate.js.map +1 -1
- package/dist/tools/mongodb/read/collectionIndexes.js +24 -5
- package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +1 -2
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/mongodb/read/find.js +5 -6
- package/dist/tools/mongodb/read/find.js.map +1 -1
- package/dist/tools/mongodb/tools.js +6 -2
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/dist/tools/mongodb/update/renameCollection.js +28 -4
- package/dist/tools/mongodb/update/renameCollection.js.map +1 -1
- package/dist/tools/mongodb/update/updateMany.js +7 -11
- package/dist/tools/mongodb/update/updateMany.js.map +1 -1
- package/dist/tools/tool.js +68 -15
- package/dist/tools/tool.js.map +1 -1
- package/eslint.config.js +29 -10
- package/global.d.ts +1 -0
- package/package.json +7 -3
- package/scripts/apply.ts +9 -7
- package/scripts/filter.ts +3 -2
- package/src/common/atlas/apiClient.ts +44 -11
- package/src/config.ts +16 -17
- package/src/errors.ts +1 -1
- package/src/index.ts +12 -8
- package/src/logger.ts +92 -29
- package/src/packageInfo.ts +6 -0
- package/src/server.ts +160 -11
- package/src/session.ts +102 -22
- package/src/telemetry/constants.ts +15 -0
- package/src/telemetry/eventCache.ts +62 -0
- package/src/telemetry/telemetry.ts +125 -0
- package/src/telemetry/types.ts +77 -0
- package/src/tools/atlas/atlasTool.ts +7 -5
- package/src/tools/atlas/{createAccessList.ts → create/createAccessList.ts} +2 -4
- package/src/tools/atlas/{createDBUser.ts → create/createDBUser.ts} +3 -5
- package/src/tools/atlas/{createFreeCluster.ts → create/createFreeCluster.ts} +7 -6
- package/src/tools/atlas/{createProject.ts → create/createProject.ts} +3 -4
- package/src/tools/atlas/metadata/connectCluster.ts +114 -0
- package/src/tools/atlas/{inspectAccessList.ts → read/inspectAccessList.ts} +2 -4
- package/src/tools/atlas/{inspectCluster.ts → read/inspectCluster.ts} +3 -5
- package/src/tools/atlas/{listClusters.ts → read/listClusters.ts} +3 -5
- package/src/tools/atlas/{listDBUsers.ts → read/listDBUsers.ts} +3 -5
- package/src/tools/atlas/{listOrgs.ts → read/listOrgs.ts} +2 -4
- package/src/tools/atlas/{listProjects.ts → read/listProjects.ts} +15 -7
- package/src/tools/atlas/tools.ts +12 -10
- package/src/tools/mongodb/create/insertMany.ts +1 -1
- package/src/tools/mongodb/delete/deleteMany.ts +1 -2
- package/src/tools/mongodb/metadata/collectionSchema.ts +16 -14
- package/src/tools/mongodb/metadata/collectionStorageSize.ts +41 -3
- package/src/tools/mongodb/metadata/connect.ts +78 -76
- package/src/tools/mongodb/metadata/dbStats.ts +6 -1
- package/src/tools/mongodb/metadata/explain.ts +20 -7
- package/src/tools/mongodb/metadata/logs.ts +55 -0
- package/src/tools/mongodb/mongodbTool.ts +47 -40
- package/src/tools/mongodb/read/aggregate.ts +4 -4
- package/src/tools/mongodb/read/collectionIndexes.ts +29 -5
- package/src/tools/mongodb/read/count.ts +1 -2
- package/src/tools/mongodb/read/find.ts +5 -6
- package/src/tools/mongodb/tools.ts +6 -2
- package/src/tools/mongodb/update/renameCollection.ts +33 -4
- package/src/tools/mongodb/update/updateMany.ts +7 -11
- package/src/tools/tool.ts +89 -26
- package/tests/integration/helpers.ts +94 -107
- package/tests/integration/inMemoryTransport.ts +3 -2
- package/tests/integration/server.test.ts +75 -23
- package/tests/integration/tools/atlas/accessLists.test.ts +13 -15
- package/tests/integration/tools/atlas/atlasHelpers.ts +10 -13
- package/tests/integration/tools/atlas/clusters.test.ts +79 -16
- package/tests/integration/tools/atlas/dbUsers.test.ts +9 -9
- package/tests/integration/tools/atlas/orgs.test.ts +4 -4
- package/tests/integration/tools/atlas/projects.test.ts +10 -12
- package/tests/integration/tools/mongodb/create/createCollection.test.ts +19 -62
- package/tests/integration/tools/mongodb/create/createIndex.test.ts +41 -87
- package/tests/integration/tools/mongodb/create/insertMany.test.ts +35 -78
- package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +25 -62
- package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +22 -71
- package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +29 -63
- package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +154 -0
- package/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +86 -0
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +88 -93
- package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +104 -0
- package/tests/integration/tools/mongodb/metadata/explain.test.ts +171 -0
- package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +28 -56
- package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +32 -26
- package/tests/integration/tools/mongodb/metadata/logs.test.ts +83 -0
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +176 -0
- package/tests/integration/tools/mongodb/read/aggregate.test.ts +99 -0
- package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +99 -0
- package/tests/integration/tools/mongodb/read/count.test.ts +31 -79
- package/tests/integration/tools/mongodb/read/find.test.ts +182 -0
- package/tests/integration/tools/mongodb/update/renameCollection.test.ts +194 -0
- package/tests/integration/tools/mongodb/update/updateMany.test.ts +238 -0
- package/tests/unit/telemetry.test.ts +200 -0
- package/tsconfig.jest.json +2 -1
- package/tsconfig.json +1 -1
- package/tsconfig.lint.json +8 -0
- package/dist/tools/atlas/createAccessList.js.map +0 -1
- package/dist/tools/atlas/createDBUser.js.map +0 -1
- package/dist/tools/atlas/createFreeCluster.js.map +0 -1
- package/dist/tools/atlas/createProject.js.map +0 -1
- package/dist/tools/atlas/inspectAccessList.js.map +0 -1
- package/dist/tools/atlas/inspectCluster.js.map +0 -1
- package/dist/tools/atlas/listClusters.js.map +0 -1
- package/dist/tools/atlas/listDBUsers.js.map +0 -1
- package/dist/tools/atlas/listOrgs.js.map +0 -1
- package/dist/tools/atlas/listProjects.js.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
databaseCollectionParameters,
|
|
3
|
+
validateToolMetadata,
|
|
4
|
+
validateThrowsForInvalidArguments,
|
|
5
|
+
getResponseElements,
|
|
6
|
+
} from "../../../helpers.js";
|
|
7
|
+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
|
|
8
|
+
|
|
9
|
+
describeWithMongoDB("explain tool", (integration) => {
|
|
10
|
+
validateToolMetadata(
|
|
11
|
+
integration,
|
|
12
|
+
"explain",
|
|
13
|
+
"Returns statistics describing the execution of the winning plan chosen by the query optimizer for the evaluated method",
|
|
14
|
+
[
|
|
15
|
+
...databaseCollectionParameters,
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
name: "method",
|
|
19
|
+
description: "The method and its arguments to run",
|
|
20
|
+
type: "array",
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
validateThrowsForInvalidArguments(integration, "explain", [
|
|
27
|
+
{},
|
|
28
|
+
{ database: 123, collection: "bar", method: [{ name: "find", arguments: {} }] },
|
|
29
|
+
{ database: "test", collection: true, method: [{ name: "find", arguments: {} }] },
|
|
30
|
+
{ database: "test", collection: "bar", method: [{ name: "dnif", arguments: {} }] },
|
|
31
|
+
{ database: "test", collection: "bar", method: "find" },
|
|
32
|
+
{ database: "test", collection: "bar", method: { name: "find", arguments: {} } },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const testCases = [
|
|
36
|
+
{
|
|
37
|
+
method: "aggregate",
|
|
38
|
+
arguments: { pipeline: [{ $match: { name: "Peter" } }] },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
method: "find",
|
|
42
|
+
arguments: { filter: { name: "Peter" } },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
method: "count",
|
|
46
|
+
arguments: {
|
|
47
|
+
query: { name: "Peter" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (const testType of ["database", "collection"] as const) {
|
|
53
|
+
describe(`with non-existing ${testType}`, () => {
|
|
54
|
+
for (const testCase of testCases) {
|
|
55
|
+
it(`should return the explain plan for ${testCase.method}`, async () => {
|
|
56
|
+
if (testType === "database") {
|
|
57
|
+
const { databases } = await integration.mongoClient().db("").admin().listDatabases();
|
|
58
|
+
expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
|
|
59
|
+
} else if (testType === "collection") {
|
|
60
|
+
await integration
|
|
61
|
+
.mongoClient()
|
|
62
|
+
.db(integration.randomDbName())
|
|
63
|
+
.createCollection("some-collection");
|
|
64
|
+
|
|
65
|
+
const collections = await integration
|
|
66
|
+
.mongoClient()
|
|
67
|
+
.db(integration.randomDbName())
|
|
68
|
+
.listCollections()
|
|
69
|
+
.toArray();
|
|
70
|
+
|
|
71
|
+
expect(collections.find((collection) => collection.name === "coll1")).toBeUndefined();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await integration.connectMcpClient();
|
|
75
|
+
|
|
76
|
+
const response = await integration.mcpClient().callTool({
|
|
77
|
+
name: "explain",
|
|
78
|
+
arguments: {
|
|
79
|
+
database: integration.randomDbName(),
|
|
80
|
+
collection: "coll1",
|
|
81
|
+
method: [
|
|
82
|
+
{
|
|
83
|
+
name: testCase.method,
|
|
84
|
+
arguments: testCase.arguments,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const content = getResponseElements(response.content);
|
|
91
|
+
expect(content).toHaveLength(2);
|
|
92
|
+
expect(content[0].text).toEqual(
|
|
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
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(content[1].text).toContain("queryPlanner");
|
|
97
|
+
expect(content[1].text).toContain("winningPlan");
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe("with existing database and collection", () => {
|
|
104
|
+
for (const indexed of [true, false] as const) {
|
|
105
|
+
describe(`with ${indexed ? "an index" : "no index"}`, () => {
|
|
106
|
+
beforeEach(async () => {
|
|
107
|
+
await integration
|
|
108
|
+
.mongoClient()
|
|
109
|
+
.db(integration.randomDbName())
|
|
110
|
+
.collection("people")
|
|
111
|
+
.insertMany([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }]);
|
|
112
|
+
|
|
113
|
+
if (indexed) {
|
|
114
|
+
await integration
|
|
115
|
+
.mongoClient()
|
|
116
|
+
.db(integration.randomDbName())
|
|
117
|
+
.collection("people")
|
|
118
|
+
.createIndex({ name: 1 });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const testCase of testCases) {
|
|
123
|
+
it(`should return the explain plan for ${testCase.method}`, async () => {
|
|
124
|
+
await integration.connectMcpClient();
|
|
125
|
+
|
|
126
|
+
const response = await integration.mcpClient().callTool({
|
|
127
|
+
name: "explain",
|
|
128
|
+
arguments: {
|
|
129
|
+
database: integration.randomDbName(),
|
|
130
|
+
collection: "people",
|
|
131
|
+
method: [
|
|
132
|
+
{
|
|
133
|
+
name: testCase.method,
|
|
134
|
+
arguments: testCase.arguments,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const content = getResponseElements(response.content);
|
|
141
|
+
expect(content).toHaveLength(2);
|
|
142
|
+
expect(content[0].text).toEqual(
|
|
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
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(content[1].text).toContain("queryPlanner");
|
|
147
|
+
expect(content[1].text).toContain("winningPlan");
|
|
148
|
+
|
|
149
|
+
if (indexed) {
|
|
150
|
+
if (testCase.method === "count") {
|
|
151
|
+
expect(content[1].text).toContain("COUNT_SCAN");
|
|
152
|
+
} else {
|
|
153
|
+
expect(content[1].text).toContain("IXSCAN");
|
|
154
|
+
}
|
|
155
|
+
expect(content[1].text).toContain("name_1");
|
|
156
|
+
} else {
|
|
157
|
+
expect(content[1].text).toContain("COLLSCAN");
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
validateAutoConnectBehavior(integration, "explain", () => {
|
|
166
|
+
return {
|
|
167
|
+
args: { database: integration.randomDbName(), collection: "coll1", method: [] },
|
|
168
|
+
expectedResponse: "No method provided. Expected one of the following: `aggregate`, `find`, or `count`",
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -1,41 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { toIncludeSameMembers } from "jest-extended";
|
|
3
|
-
import { McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import config from "../../../../../src/config.js";
|
|
5
|
-
import { ObjectId } from "bson";
|
|
1
|
+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import {
|
|
4
|
+
getResponseElements,
|
|
5
|
+
getResponseContent,
|
|
6
|
+
validateToolMetadata,
|
|
7
|
+
validateThrowsForInvalidArguments,
|
|
8
|
+
databaseInvalidArgs,
|
|
9
|
+
databaseParameters,
|
|
10
|
+
} from "../../../helpers.js";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
describeWithMongoDB("listCollections tool", (integration) => {
|
|
13
|
+
validateToolMetadata(
|
|
14
|
+
integration,
|
|
15
|
+
"list-collections",
|
|
16
|
+
"List all collections for a given database",
|
|
17
|
+
databaseParameters
|
|
18
|
+
);
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
{ name: "database", description: "Database name", type: "string", required: true },
|
|
18
|
-
]);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe("with invalid arguments", () => {
|
|
22
|
-
const args = [{}, { database: 123 }, { foo: "bar", database: "test" }, { database: [] }];
|
|
23
|
-
for (const arg of args) {
|
|
24
|
-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
|
|
25
|
-
await integration.connectMcpClient();
|
|
26
|
-
try {
|
|
27
|
-
await integration.mcpClient().callTool({ name: "list-collections", arguments: arg });
|
|
28
|
-
expect.fail("Expected an error to be thrown");
|
|
29
|
-
} catch (error) {
|
|
30
|
-
expect(error).toBeInstanceOf(McpError);
|
|
31
|
-
const mcpError = error as McpError;
|
|
32
|
-
expect(mcpError.code).toEqual(-32602);
|
|
33
|
-
expect(mcpError.message).toContain("Invalid arguments for tool list-collections");
|
|
34
|
-
expect(mcpError.message).toContain('"expected": "string"');
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
});
|
|
20
|
+
validateThrowsForInvalidArguments(integration, "list-collections", databaseInvalidArgs);
|
|
39
21
|
|
|
40
22
|
describe("with non-existent database", () => {
|
|
41
23
|
it("returns no collections", async () => {
|
|
@@ -46,7 +28,7 @@ describe("listCollections tool", () => {
|
|
|
46
28
|
});
|
|
47
29
|
const content = getResponseContent(response.content);
|
|
48
30
|
expect(content).toEqual(
|
|
49
|
-
|
|
31
|
+
'No collections found for database "non-existent". To create a collection, use the "create-collection" tool.'
|
|
50
32
|
);
|
|
51
33
|
});
|
|
52
34
|
});
|
|
@@ -80,25 +62,15 @@ describe("listCollections tool", () => {
|
|
|
80
62
|
});
|
|
81
63
|
});
|
|
82
64
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const response = await integration
|
|
88
|
-
.mcpClient()
|
|
89
|
-
.callTool({ name: "list-collections", arguments: { database: integration.randomDbName() } });
|
|
90
|
-
const content = getResponseContent(response.content);
|
|
91
|
-
expect(content).toEqual(
|
|
92
|
-
`No collections found for database "${integration.randomDbName()}". To create a collection, use the "create-collection" tool.`
|
|
93
|
-
);
|
|
94
|
-
});
|
|
65
|
+
validateAutoConnectBehavior(
|
|
66
|
+
integration,
|
|
67
|
+
"list-collections",
|
|
95
68
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
69
|
+
() => {
|
|
70
|
+
return {
|
|
71
|
+
args: { database: integration.randomDbName() },
|
|
72
|
+
expectedResponse: `No collections found for database "${integration.randomDbName()}". To create a collection, use the "create-collection" tool.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
);
|
|
104
76
|
});
|
|
@@ -1,44 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getResponseElements, getParameters,
|
|
3
|
-
import { toIncludeSameMembers } from "jest-extended";
|
|
1
|
+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
|
|
2
|
+
import { getResponseElements, getParameters, expectDefined } from "../../../helpers.js";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
const
|
|
4
|
+
describeWithMongoDB("listDatabases tool", (integration) => {
|
|
5
|
+
const defaultDatabases = ["admin", "config", "local"];
|
|
7
6
|
|
|
8
7
|
it("should have correct metadata", async () => {
|
|
9
8
|
const { tools } = await integration.mcpClient().listTools();
|
|
10
|
-
const listDatabases = tools.find((tool) => tool.name === "list-databases")
|
|
11
|
-
|
|
9
|
+
const listDatabases = tools.find((tool) => tool.name === "list-databases");
|
|
10
|
+
expectDefined(listDatabases);
|
|
12
11
|
expect(listDatabases.description).toBe("List all databases for a MongoDB connection");
|
|
13
12
|
|
|
14
13
|
const parameters = getParameters(listDatabases);
|
|
15
14
|
expect(parameters).toHaveLength(0);
|
|
16
15
|
});
|
|
17
16
|
|
|
18
|
-
describe("when not connected", () => {
|
|
19
|
-
it("connects automatically if connection string is configured", async () => {
|
|
20
|
-
config.connectionString = integration.connectionString();
|
|
21
|
-
|
|
22
|
-
const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
|
|
23
|
-
const dbNames = getDbNames(response.content);
|
|
24
|
-
|
|
25
|
-
expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("throws an error if connection string is not configured", async () => {
|
|
29
|
-
const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
|
|
30
|
-
const content = getResponseContent(response.content);
|
|
31
|
-
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
17
|
describe("with no preexisting databases", () => {
|
|
36
18
|
it("returns only the system databases", async () => {
|
|
37
19
|
await integration.connectMcpClient();
|
|
38
20
|
const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
|
|
39
21
|
const dbNames = getDbNames(response.content);
|
|
40
22
|
|
|
41
|
-
expect(
|
|
23
|
+
expect(defaultDatabases).toIncludeAllMembers(dbNames);
|
|
42
24
|
});
|
|
43
25
|
});
|
|
44
26
|
|
|
@@ -52,9 +34,33 @@ describe("listDatabases tool", () => {
|
|
|
52
34
|
|
|
53
35
|
const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
|
|
54
36
|
const dbNames = getDbNames(response.content);
|
|
55
|
-
expect(dbNames).toIncludeSameMembers([
|
|
37
|
+
expect(dbNames).toIncludeSameMembers([...defaultDatabases, "foo", "baz"]);
|
|
56
38
|
});
|
|
57
39
|
});
|
|
40
|
+
|
|
41
|
+
validateAutoConnectBehavior(
|
|
42
|
+
integration,
|
|
43
|
+
"list-databases",
|
|
44
|
+
() => {
|
|
45
|
+
return {
|
|
46
|
+
args: {},
|
|
47
|
+
validate: (content) => {
|
|
48
|
+
const dbNames = getDbNames(content);
|
|
49
|
+
|
|
50
|
+
expect(defaultDatabases).toIncludeAllMembers(dbNames);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
async () => {
|
|
55
|
+
const mongoClient = integration.mongoClient();
|
|
56
|
+
const { databases } = await mongoClient.db("admin").command({ listDatabases: 1, nameOnly: true });
|
|
57
|
+
for (const db of databases as { name: string }[]) {
|
|
58
|
+
if (!defaultDatabases.includes(db.name)) {
|
|
59
|
+
await mongoClient.db(db.name).dropDatabase();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
function getDbNames(content: unknown): (string | null)[] {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { validateToolMetadata, validateThrowsForInvalidArguments, getResponseElements } from "../../../helpers.js";
|
|
2
|
+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
|
|
3
|
+
|
|
4
|
+
describeWithMongoDB("logs tool", (integration) => {
|
|
5
|
+
validateToolMetadata(integration, "mongodb-logs", "Returns the most recent logged mongod events", [
|
|
6
|
+
{
|
|
7
|
+
type: "string",
|
|
8
|
+
name: "type",
|
|
9
|
+
description:
|
|
10
|
+
"The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started.",
|
|
11
|
+
required: false,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
type: "integer",
|
|
15
|
+
name: "limit",
|
|
16
|
+
description: "The maximum number of log entries to return.",
|
|
17
|
+
required: false,
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
validateThrowsForInvalidArguments(integration, "mongodb-logs", [
|
|
22
|
+
{ type: 123 },
|
|
23
|
+
{ type: "something" },
|
|
24
|
+
{ limit: 0 },
|
|
25
|
+
{ limit: true },
|
|
26
|
+
{ limit: 1025 },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
it("should return global logs", async () => {
|
|
30
|
+
await integration.connectMcpClient();
|
|
31
|
+
const response = await integration.mcpClient().callTool({
|
|
32
|
+
name: "mongodb-logs",
|
|
33
|
+
arguments: {},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const elements = getResponseElements(response);
|
|
37
|
+
|
|
38
|
+
// Default limit is 50
|
|
39
|
+
expect(elements.length).toBeLessThanOrEqual(51);
|
|
40
|
+
expect(elements[0].text).toMatch(/Found: \d+ messages/);
|
|
41
|
+
|
|
42
|
+
for (let i = 1; i < elements.length; i++) {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
44
|
+
const log = JSON.parse(elements[i].text);
|
|
45
|
+
expect(log).toHaveProperty("t");
|
|
46
|
+
expect(log).toHaveProperty("msg");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return startupWarnings logs", async () => {
|
|
51
|
+
await integration.connectMcpClient();
|
|
52
|
+
const response = await integration.mcpClient().callTool({
|
|
53
|
+
name: "mongodb-logs",
|
|
54
|
+
arguments: {
|
|
55
|
+
type: "startupWarnings",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const elements = getResponseElements(response);
|
|
60
|
+
expect(elements.length).toBeLessThanOrEqual(51);
|
|
61
|
+
for (let i = 1; i < elements.length; i++) {
|
|
62
|
+
const log = JSON.parse(elements[i].text) as { tags: string[] };
|
|
63
|
+
expect(log).toHaveProperty("t");
|
|
64
|
+
expect(log).toHaveProperty("msg");
|
|
65
|
+
expect(log).toHaveProperty("tags");
|
|
66
|
+
expect(log.tags).toContain("startupWarnings");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
validateAutoConnectBehavior(integration, "mongodb-logs", () => {
|
|
71
|
+
return {
|
|
72
|
+
args: {
|
|
73
|
+
database: integration.randomDbName(),
|
|
74
|
+
collection: "foo",
|
|
75
|
+
},
|
|
76
|
+
validate: (content) => {
|
|
77
|
+
const elements = getResponseElements(content);
|
|
78
|
+
expect(elements.length).toBeLessThanOrEqual(51);
|
|
79
|
+
expect(elements[0].text).toMatch(/Found: \d+ messages/);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { MongoCluster } from "mongodb-runner";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { MongoClient, ObjectId } from "mongodb";
|
|
5
|
+
import { getResponseContent, IntegrationTest, setupIntegrationTest } from "../../helpers.js";
|
|
6
|
+
import { config, UserConfig } from "../../../../src/config.js";
|
|
7
|
+
|
|
8
|
+
interface MongoDBIntegrationTest {
|
|
9
|
+
mongoClient: () => MongoClient;
|
|
10
|
+
connectionString: () => string;
|
|
11
|
+
randomDbName: () => string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function describeWithMongoDB(
|
|
15
|
+
name: string,
|
|
16
|
+
fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise<void> }) => void,
|
|
17
|
+
getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => config,
|
|
18
|
+
describeFn = describe
|
|
19
|
+
) {
|
|
20
|
+
describeFn(name, () => {
|
|
21
|
+
const mdbIntegration = setupMongoDBIntegrationTest();
|
|
22
|
+
const integration = setupIntegrationTest(() => ({
|
|
23
|
+
...getUserConfig(mdbIntegration),
|
|
24
|
+
connectionString: mdbIntegration.connectionString(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
integration.mcpServer().userConfig.connectionString = mdbIntegration.connectionString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
fn({
|
|
32
|
+
...integration,
|
|
33
|
+
...mdbIntegration,
|
|
34
|
+
connectMcpClient: async () => {
|
|
35
|
+
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when
|
|
36
|
+
// the connect tool is reenabled
|
|
37
|
+
// await integration.mcpClient().callTool({
|
|
38
|
+
// name: "connect",
|
|
39
|
+
// arguments: { connectionString: mdbIntegration.connectionString() },
|
|
40
|
+
// });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
|
|
47
|
+
let mongoCluster: // TODO: Fix this type once mongodb-runner is updated.
|
|
48
|
+
| {
|
|
49
|
+
connectionString: string;
|
|
50
|
+
close: () => Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
| undefined;
|
|
53
|
+
let mongoClient: MongoClient | undefined;
|
|
54
|
+
let randomDbName: string;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
randomDbName = new ObjectId().toString();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await mongoClient?.close();
|
|
62
|
+
mongoClient = undefined;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
beforeAll(async function () {
|
|
66
|
+
// Downloading Windows executables in CI takes a long time because
|
|
67
|
+
// they include debug symbols...
|
|
68
|
+
const tmpDir = path.join(__dirname, "..", "..", "..", "tmp");
|
|
69
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
// On Windows, we may have a situation where mongod.exe is not fully released by the OS
|
|
72
|
+
// before we attempt to run it again, so we add a retry.
|
|
73
|
+
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
|
|
74
|
+
for (let i = 0; i < 10; i++) {
|
|
75
|
+
try {
|
|
76
|
+
// TODO: Fix this type once mongodb-runner is updated.
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
78
|
+
mongoCluster = await MongoCluster.start({
|
|
79
|
+
tmpDir: dbsDir,
|
|
80
|
+
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
|
|
81
|
+
topology: "standalone",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (i < 5) {
|
|
87
|
+
// Just wait a little bit and retry
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
89
|
+
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
91
|
+
} else {
|
|
92
|
+
// If we still fail after 5 seconds, try another db dir
|
|
93
|
+
console.error(
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
95
|
+
`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
|
|
96
|
+
);
|
|
97
|
+
dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error("Failed to start cluster after 10 attempts");
|
|
103
|
+
}, 120_000);
|
|
104
|
+
|
|
105
|
+
afterAll(async function () {
|
|
106
|
+
await mongoCluster?.close();
|
|
107
|
+
mongoCluster = undefined;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const getConnectionString = () => {
|
|
111
|
+
if (!mongoCluster) {
|
|
112
|
+
throw new Error("beforeAll() hook not ran yet");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return mongoCluster.connectionString;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
mongoClient: () => {
|
|
120
|
+
if (!mongoClient) {
|
|
121
|
+
mongoClient = new MongoClient(getConnectionString());
|
|
122
|
+
}
|
|
123
|
+
return mongoClient;
|
|
124
|
+
},
|
|
125
|
+
connectionString: getConnectionString,
|
|
126
|
+
|
|
127
|
+
randomDbName: () => randomDbName,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function validateAutoConnectBehavior(
|
|
132
|
+
integration: IntegrationTest & MongoDBIntegrationTest,
|
|
133
|
+
name: string,
|
|
134
|
+
validation: () => {
|
|
135
|
+
args: { [x: string]: unknown };
|
|
136
|
+
expectedResponse?: string;
|
|
137
|
+
validate?: (content: unknown) => void;
|
|
138
|
+
},
|
|
139
|
+
beforeEachImpl?: () => Promise<void>
|
|
140
|
+
): void {
|
|
141
|
+
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
|
|
142
|
+
describe.skip("when not connected", () => {
|
|
143
|
+
if (beforeEachImpl) {
|
|
144
|
+
beforeEach(() => beforeEachImpl());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it("connects automatically if connection string is configured", async () => {
|
|
148
|
+
integration.mcpServer().userConfig.connectionString = integration.connectionString();
|
|
149
|
+
|
|
150
|
+
const validationInfo = validation();
|
|
151
|
+
|
|
152
|
+
const response = await integration.mcpClient().callTool({
|
|
153
|
+
name,
|
|
154
|
+
arguments: validationInfo.args,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (validationInfo.expectedResponse) {
|
|
158
|
+
const content = getResponseContent(response.content);
|
|
159
|
+
expect(content).toContain(validationInfo.expectedResponse);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (validationInfo.validate) {
|
|
163
|
+
validationInfo.validate(response.content);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("throws an error if connection string is not configured", async () => {
|
|
168
|
+
const response = await integration.mcpClient().callTool({
|
|
169
|
+
name,
|
|
170
|
+
arguments: validation().args,
|
|
171
|
+
});
|
|
172
|
+
const content = getResponseContent(response.content);
|
|
173
|
+
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|