mongodb-mcp-server 0.0.4

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.
Files changed (164) hide show
  1. package/.github/workflows/code_health.yaml +65 -0
  2. package/.github/workflows/prepare_release.yaml +45 -0
  3. package/.github/workflows/publish.yaml +70 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc.json +37 -0
  6. package/.vscode/launch.json +17 -0
  7. package/CONTRIBUTING.md +185 -0
  8. package/LICENSE +202 -0
  9. package/README.md +234 -0
  10. package/dist/common/atlas/apiClient.js +147 -0
  11. package/dist/common/atlas/apiClient.js.map +1 -0
  12. package/dist/common/atlas/apiClientError.js +17 -0
  13. package/dist/common/atlas/apiClientError.js.map +1 -0
  14. package/dist/config.js +85 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/errors.js +12 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.js +26 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/logger.js +97 -0
  21. package/dist/logger.js.map +1 -0
  22. package/dist/server.js +45 -0
  23. package/dist/server.js.map +1 -0
  24. package/dist/session.js +30 -0
  25. package/dist/session.js.map +1 -0
  26. package/dist/tools/atlas/atlasTool.js +9 -0
  27. package/dist/tools/atlas/atlasTool.js.map +1 -0
  28. package/dist/tools/atlas/createAccessList.js +64 -0
  29. package/dist/tools/atlas/createAccessList.js.map +1 -0
  30. package/dist/tools/atlas/createDBUser.js +58 -0
  31. package/dist/tools/atlas/createDBUser.js.map +1 -0
  32. package/dist/tools/atlas/createFreeCluster.js +51 -0
  33. package/dist/tools/atlas/createFreeCluster.js.map +1 -0
  34. package/dist/tools/atlas/createProject.js +53 -0
  35. package/dist/tools/atlas/createProject.js.map +1 -0
  36. package/dist/tools/atlas/inspectAccessList.js +41 -0
  37. package/dist/tools/atlas/inspectAccessList.js.map +1 -0
  38. package/dist/tools/atlas/inspectCluster.js +42 -0
  39. package/dist/tools/atlas/inspectCluster.js.map +1 -0
  40. package/dist/tools/atlas/listClusters.js +97 -0
  41. package/dist/tools/atlas/listClusters.js.map +1 -0
  42. package/dist/tools/atlas/listDBUsers.js +52 -0
  43. package/dist/tools/atlas/listDBUsers.js.map +1 -0
  44. package/dist/tools/atlas/listOrgs.js +30 -0
  45. package/dist/tools/atlas/listOrgs.js.map +1 -0
  46. package/dist/tools/atlas/listProjects.js +42 -0
  47. package/dist/tools/atlas/listProjects.js.map +1 -0
  48. package/dist/tools/atlas/tools.js +23 -0
  49. package/dist/tools/atlas/tools.js.map +1 -0
  50. package/dist/tools/mongodb/create/createCollection.js +23 -0
  51. package/dist/tools/mongodb/create/createCollection.js.map +1 -0
  52. package/dist/tools/mongodb/create/createIndex.js +33 -0
  53. package/dist/tools/mongodb/create/createIndex.js.map +1 -0
  54. package/dist/tools/mongodb/create/insertMany.js +33 -0
  55. package/dist/tools/mongodb/create/insertMany.js.map +1 -0
  56. package/dist/tools/mongodb/delete/deleteMany.js +31 -0
  57. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -0
  58. package/dist/tools/mongodb/delete/dropCollection.js +25 -0
  59. package/dist/tools/mongodb/delete/dropCollection.js.map +1 -0
  60. package/dist/tools/mongodb/delete/dropDatabase.js +25 -0
  61. package/dist/tools/mongodb/delete/dropDatabase.js.map +1 -0
  62. package/dist/tools/mongodb/metadata/collectionSchema.js +38 -0
  63. package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -0
  64. package/dist/tools/mongodb/metadata/collectionStorageSize.js +28 -0
  65. package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -0
  66. package/dist/tools/mongodb/metadata/connect.js +86 -0
  67. package/dist/tools/mongodb/metadata/connect.js.map +1 -0
  68. package/dist/tools/mongodb/metadata/dbStats.js +28 -0
  69. package/dist/tools/mongodb/metadata/dbStats.js.map +1 -0
  70. package/dist/tools/mongodb/metadata/explain.js +77 -0
  71. package/dist/tools/mongodb/metadata/explain.js.map +1 -0
  72. package/dist/tools/mongodb/metadata/listCollections.js +35 -0
  73. package/dist/tools/mongodb/metadata/listCollections.js.map +1 -0
  74. package/dist/tools/mongodb/metadata/listDatabases.js +23 -0
  75. package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -0
  76. package/dist/tools/mongodb/mongodbTool.js +58 -0
  77. package/dist/tools/mongodb/mongodbTool.js.map +1 -0
  78. package/dist/tools/mongodb/read/aggregate.js +38 -0
  79. package/dist/tools/mongodb/read/aggregate.js.map +1 -0
  80. package/dist/tools/mongodb/read/collectionIndexes.js +23 -0
  81. package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -0
  82. package/dist/tools/mongodb/read/count.js +34 -0
  83. package/dist/tools/mongodb/read/count.js.map +1 -0
  84. package/dist/tools/mongodb/read/find.js +51 -0
  85. package/dist/tools/mongodb/read/find.js.map +1 -0
  86. package/dist/tools/mongodb/tools.js +41 -0
  87. package/dist/tools/mongodb/tools.js.map +1 -0
  88. package/dist/tools/mongodb/update/renameCollection.js +31 -0
  89. package/dist/tools/mongodb/update/renameCollection.js.map +1 -0
  90. package/dist/tools/mongodb/update/updateMany.js +56 -0
  91. package/dist/tools/mongodb/update/updateMany.js.map +1 -0
  92. package/dist/tools/tool.js +56 -0
  93. package/dist/tools/tool.js.map +1 -0
  94. package/eslint.config.js +35 -0
  95. package/jest.config.js +22 -0
  96. package/package.json +76 -0
  97. package/scripts/apply.ts +129 -0
  98. package/scripts/filter.ts +67 -0
  99. package/scripts/generate.sh +11 -0
  100. package/src/common/atlas/apiClient.ts +202 -0
  101. package/src/common/atlas/apiClientError.ts +21 -0
  102. package/src/common/atlas/openapi.d.ts +5849 -0
  103. package/src/config.ts +124 -0
  104. package/src/errors.ts +13 -0
  105. package/src/index.ts +30 -0
  106. package/src/logger.ts +117 -0
  107. package/src/server.ts +64 -0
  108. package/src/session.ts +37 -0
  109. package/src/tools/atlas/atlasTool.ts +10 -0
  110. package/src/tools/atlas/createAccessList.ts +78 -0
  111. package/src/tools/atlas/createDBUser.ts +70 -0
  112. package/src/tools/atlas/createFreeCluster.ts +55 -0
  113. package/src/tools/atlas/createProject.ts +63 -0
  114. package/src/tools/atlas/inspectAccessList.ts +44 -0
  115. package/src/tools/atlas/inspectCluster.ts +47 -0
  116. package/src/tools/atlas/listClusters.ts +104 -0
  117. package/src/tools/atlas/listDBUsers.ts +62 -0
  118. package/src/tools/atlas/listOrgs.ts +34 -0
  119. package/src/tools/atlas/listProjects.ts +46 -0
  120. package/src/tools/atlas/tools.ts +23 -0
  121. package/src/tools/mongodb/create/createCollection.ts +26 -0
  122. package/src/tools/mongodb/create/createIndex.ts +41 -0
  123. package/src/tools/mongodb/create/insertMany.ts +40 -0
  124. package/src/tools/mongodb/delete/deleteMany.ts +38 -0
  125. package/src/tools/mongodb/delete/dropCollection.ts +27 -0
  126. package/src/tools/mongodb/delete/dropDatabase.ts +26 -0
  127. package/src/tools/mongodb/metadata/collectionSchema.ts +41 -0
  128. package/src/tools/mongodb/metadata/collectionStorageSize.ts +30 -0
  129. package/src/tools/mongodb/metadata/connect.ts +94 -0
  130. package/src/tools/mongodb/metadata/dbStats.ts +30 -0
  131. package/src/tools/mongodb/metadata/explain.ts +90 -0
  132. package/src/tools/mongodb/metadata/listCollections.ts +38 -0
  133. package/src/tools/mongodb/metadata/listDatabases.ts +26 -0
  134. package/src/tools/mongodb/mongodbTool.ts +69 -0
  135. package/src/tools/mongodb/read/aggregate.ts +45 -0
  136. package/src/tools/mongodb/read/collectionIndexes.ts +24 -0
  137. package/src/tools/mongodb/read/count.ts +39 -0
  138. package/src/tools/mongodb/read/find.ts +62 -0
  139. package/src/tools/mongodb/tools.ts +41 -0
  140. package/src/tools/mongodb/update/renameCollection.ts +37 -0
  141. package/src/tools/mongodb/update/updateMany.ts +65 -0
  142. package/src/tools/tool.ts +90 -0
  143. package/src/types/mongodb-redact.d.ts +4 -0
  144. package/tests/integration/helpers.ts +241 -0
  145. package/tests/integration/inMemoryTransport.ts +58 -0
  146. package/tests/integration/server.test.ts +35 -0
  147. package/tests/integration/tools/atlas/accessLists.test.ts +100 -0
  148. package/tests/integration/tools/atlas/atlasHelpers.ts +110 -0
  149. package/tests/integration/tools/atlas/clusters.test.ts +122 -0
  150. package/tests/integration/tools/atlas/dbUsers.test.ts +80 -0
  151. package/tests/integration/tools/atlas/orgs.test.ts +24 -0
  152. package/tests/integration/tools/atlas/projects.test.ts +80 -0
  153. package/tests/integration/tools/mongodb/create/createCollection.test.ts +138 -0
  154. package/tests/integration/tools/mongodb/create/createIndex.test.ts +249 -0
  155. package/tests/integration/tools/mongodb/create/insertMany.test.ts +141 -0
  156. package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +191 -0
  157. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +118 -0
  158. package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +114 -0
  159. package/tests/integration/tools/mongodb/metadata/connect.test.ts +137 -0
  160. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +104 -0
  161. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +67 -0
  162. package/tests/integration/tools/mongodb/read/count.test.ts +138 -0
  163. package/tsconfig.jest.json +10 -0
  164. package/tsconfig.json +19 -0
@@ -0,0 +1,118 @@
1
+ import {
2
+ getResponseContent,
3
+ validateParameters,
4
+ dbOperationParameters,
5
+ setupIntegrationTest,
6
+ } from "../../../helpers.js";
7
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
8
+ import config from "../../../../../src/config.js";
9
+
10
+ describe("dropCollection tool", () => {
11
+ const integration = setupIntegrationTest();
12
+
13
+ it("should have correct metadata", async () => {
14
+ const { tools } = await integration.mcpClient().listTools();
15
+ const dropCollection = tools.find((tool) => tool.name === "drop-collection")!;
16
+ expect(dropCollection).toBeDefined();
17
+ expect(dropCollection.description).toBe(
18
+ "Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection."
19
+ );
20
+
21
+ validateParameters(dropCollection, [...dbOperationParameters]);
22
+ });
23
+
24
+ describe("with invalid arguments", () => {
25
+ const args = [
26
+ {},
27
+ { database: 123, collection: "bar" },
28
+ { foo: "bar", database: "test", collection: "bar" },
29
+ { collection: [], database: "test" },
30
+ ];
31
+ for (const arg of args) {
32
+ it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
33
+ await integration.connectMcpClient();
34
+ try {
35
+ await integration.mcpClient().callTool({ name: "drop-collection", arguments: arg });
36
+ expect.fail("Expected an error to be thrown");
37
+ } catch (error) {
38
+ expect(error).toBeInstanceOf(McpError);
39
+ const mcpError = error as McpError;
40
+ expect(mcpError.code).toEqual(-32602);
41
+ expect(mcpError.message).toContain("Invalid arguments for tool drop-collection");
42
+ }
43
+ });
44
+ }
45
+ });
46
+
47
+ it("can drop non-existing collection", async () => {
48
+ await integration.connectMcpClient();
49
+ const response = await integration.mcpClient().callTool({
50
+ name: "drop-collection",
51
+ arguments: {
52
+ database: integration.randomDbName(),
53
+ collection: "coll1",
54
+ },
55
+ });
56
+
57
+ const content = getResponseContent(response.content);
58
+ expect(content).toContain(
59
+ `Successfully dropped collection "coll1" from database "${integration.randomDbName()}"`
60
+ );
61
+
62
+ const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray();
63
+ expect(collections).toHaveLength(0);
64
+ });
65
+
66
+ it("removes the collection if it exists", async () => {
67
+ await integration.connectMcpClient();
68
+ await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1");
69
+ await integration.mongoClient().db(integration.randomDbName()).createCollection("coll2");
70
+ const response = await integration.mcpClient().callTool({
71
+ name: "drop-collection",
72
+ arguments: {
73
+ database: integration.randomDbName(),
74
+ collection: "coll1",
75
+ },
76
+ });
77
+ const content = getResponseContent(response.content);
78
+ expect(content).toContain(
79
+ `Successfully dropped collection "coll1" from database "${integration.randomDbName()}"`
80
+ );
81
+ const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray();
82
+ expect(collections).toHaveLength(1);
83
+ expect(collections[0].name).toBe("coll2");
84
+ });
85
+
86
+ describe("when not connected", () => {
87
+ it("connects automatically if connection string is configured", async () => {
88
+ await integration.connectMcpClient();
89
+ await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1");
90
+
91
+ config.connectionString = integration.connectionString();
92
+
93
+ const response = await integration.mcpClient().callTool({
94
+ name: "drop-collection",
95
+ arguments: {
96
+ database: integration.randomDbName(),
97
+ collection: "coll1",
98
+ },
99
+ });
100
+ const content = getResponseContent(response.content);
101
+ expect(content).toContain(
102
+ `Successfully dropped collection "coll1" from database "${integration.randomDbName()}"`
103
+ );
104
+ });
105
+
106
+ it("throws an error if connection string is not configured", async () => {
107
+ const response = await integration.mcpClient().callTool({
108
+ name: "drop-collection",
109
+ arguments: {
110
+ database: integration.randomDbName(),
111
+ collection: "coll1",
112
+ },
113
+ });
114
+ const content = getResponseContent(response.content);
115
+ expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,114 @@
1
+ import {
2
+ getResponseContent,
3
+ validateParameters,
4
+ dbOperationParameters,
5
+ setupIntegrationTest,
6
+ } from "../../../helpers.js";
7
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
8
+ import config from "../../../../../src/config.js";
9
+
10
+ describe("dropDatabase tool", () => {
11
+ const integration = setupIntegrationTest();
12
+
13
+ it("should have correct metadata", async () => {
14
+ const { tools } = await integration.mcpClient().listTools();
15
+ const dropDatabase = tools.find((tool) => tool.name === "drop-database")!;
16
+ expect(dropDatabase).toBeDefined();
17
+ expect(dropDatabase.description).toBe("Removes the specified database, deleting the associated data files");
18
+
19
+ validateParameters(dropDatabase, [dbOperationParameters.find((d) => d.name === "database")!]);
20
+ });
21
+
22
+ describe("with invalid arguments", () => {
23
+ const args = [{}, { database: 123 }, { foo: "bar", database: "test" }];
24
+ for (const arg of args) {
25
+ it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
26
+ await integration.connectMcpClient();
27
+ try {
28
+ await integration.mcpClient().callTool({ name: "drop-database", arguments: arg });
29
+ expect.fail("Expected an error to be thrown");
30
+ } catch (error) {
31
+ expect(error).toBeInstanceOf(McpError);
32
+ const mcpError = error as McpError;
33
+ expect(mcpError.code).toEqual(-32602);
34
+ expect(mcpError.message).toContain("Invalid arguments for tool drop-database");
35
+ }
36
+ });
37
+ }
38
+ });
39
+
40
+ it("can drop non-existing database", async () => {
41
+ let { databases } = await integration.mongoClient().db("").admin().listDatabases();
42
+
43
+ const preDropLength = databases.length;
44
+
45
+ await integration.connectMcpClient();
46
+ const response = await integration.mcpClient().callTool({
47
+ name: "drop-database",
48
+ arguments: {
49
+ database: integration.randomDbName(),
50
+ },
51
+ });
52
+
53
+ const content = getResponseContent(response.content);
54
+ expect(content).toContain(`Successfully dropped database "${integration.randomDbName()}"`);
55
+
56
+ ({ databases } = await integration.mongoClient().db("").admin().listDatabases());
57
+
58
+ expect(databases).toHaveLength(preDropLength);
59
+ expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
60
+ });
61
+
62
+ it("removes the database along with its collections", async () => {
63
+ await integration.connectMcpClient();
64
+ await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1");
65
+ await integration.mongoClient().db(integration.randomDbName()).createCollection("coll2");
66
+
67
+ let { databases } = await integration.mongoClient().db("").admin().listDatabases();
68
+ expect(databases.find((db) => db.name === integration.randomDbName())).toBeDefined();
69
+
70
+ const response = await integration.mcpClient().callTool({
71
+ name: "drop-database",
72
+ arguments: {
73
+ database: integration.randomDbName(),
74
+ },
75
+ });
76
+ const content = getResponseContent(response.content);
77
+ expect(content).toContain(`Successfully dropped database "${integration.randomDbName()}"`);
78
+
79
+ ({ databases } = await integration.mongoClient().db("").admin().listDatabases());
80
+ expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
81
+
82
+ const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray();
83
+ expect(collections).toHaveLength(0);
84
+ });
85
+
86
+ describe("when not connected", () => {
87
+ it("connects automatically if connection string is configured", async () => {
88
+ await integration.connectMcpClient();
89
+ await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1");
90
+
91
+ config.connectionString = integration.connectionString();
92
+
93
+ const response = await integration.mcpClient().callTool({
94
+ name: "drop-database",
95
+ arguments: {
96
+ database: integration.randomDbName(),
97
+ },
98
+ });
99
+ const content = getResponseContent(response.content);
100
+ expect(content).toContain(`Successfully dropped database "${integration.randomDbName()}"`);
101
+ });
102
+
103
+ it("throws an error if connection string is not configured", async () => {
104
+ const response = await integration.mcpClient().callTool({
105
+ name: "drop-database",
106
+ arguments: {
107
+ database: integration.randomDbName(),
108
+ },
109
+ });
110
+ const content = getResponseContent(response.content);
111
+ expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,137 @@
1
+ import { getResponseContent, validateParameters, setupIntegrationTest } from "../../../helpers.js";
2
+
3
+ import config from "../../../../../src/config.js";
4
+
5
+ describe("Connect tool", () => {
6
+ const integration = setupIntegrationTest();
7
+
8
+ it("should have correct metadata", async () => {
9
+ const { tools } = await integration.mcpClient().listTools();
10
+ const connectTool = tools.find((tool) => tool.name === "connect")!;
11
+ expect(connectTool).toBeDefined();
12
+ expect(connectTool.description).toBe("Connect to a MongoDB instance");
13
+
14
+ validateParameters(connectTool, [
15
+ {
16
+ name: "options",
17
+ description:
18
+ "Options for connecting to MongoDB. If not provided, the connection string from the config://connection-string resource will be used. If the user hasn't specified Atlas cluster name or a connection string explicitly and the `config://connection-string` resource is present, always invoke this with no arguments.",
19
+ type: "array",
20
+ required: false,
21
+ },
22
+ ]);
23
+ });
24
+
25
+ describe("with default config", () => {
26
+ describe("without connection string", () => {
27
+ it("prompts for connection string", async () => {
28
+ const response = await integration.mcpClient().callTool({ name: "connect", arguments: {} });
29
+ const content = getResponseContent(response.content);
30
+ expect(content).toContain("No connection details provided");
31
+ });
32
+ });
33
+
34
+ describe("with connection string", () => {
35
+ it("connects to the database", async () => {
36
+ const response = await integration.mcpClient().callTool({
37
+ name: "connect",
38
+ arguments: {
39
+ options: [
40
+ {
41
+ connectionString: integration.connectionString(),
42
+ },
43
+ ],
44
+ },
45
+ });
46
+ const content = getResponseContent(response.content);
47
+ expect(content).toContain("Successfully connected");
48
+ expect(content).toContain(integration.connectionString());
49
+ });
50
+ });
51
+
52
+ describe("with invalid connection string", () => {
53
+ it("returns error message", async () => {
54
+ const response = await integration.mcpClient().callTool({
55
+ name: "connect",
56
+ arguments: { options: [{ connectionString: "mongodb://localhost:12345" }] },
57
+ });
58
+ const content = getResponseContent(response.content);
59
+ expect(content).toContain("Error running connect");
60
+
61
+ // Should not suggest using the config connection string (because we don't have one)
62
+ expect(content).not.toContain("Your config lists a different connection string");
63
+ });
64
+ });
65
+ });
66
+
67
+ describe("with connection string in config", () => {
68
+ beforeEach(async () => {
69
+ config.connectionString = integration.connectionString();
70
+ });
71
+
72
+ it("uses the connection string from config", async () => {
73
+ const response = await integration.mcpClient().callTool({ name: "connect", arguments: {} });
74
+ const content = getResponseContent(response.content);
75
+ expect(content).toContain("Successfully connected");
76
+ expect(content).toContain(integration.connectionString());
77
+ });
78
+
79
+ it("prefers connection string from arguments", async () => {
80
+ const newConnectionString = `${integration.connectionString()}?appName=foo-bar`;
81
+ const response = await integration.mcpClient().callTool({
82
+ name: "connect",
83
+ arguments: {
84
+ options: [
85
+ {
86
+ connectionString: newConnectionString,
87
+ },
88
+ ],
89
+ },
90
+ });
91
+ const content = getResponseContent(response.content);
92
+ expect(content).toContain("Successfully connected");
93
+ expect(content).toContain(newConnectionString);
94
+ });
95
+
96
+ describe("when the arugment connection string is invalid", () => {
97
+ it("suggests the config connection string if set", async () => {
98
+ const response = await integration.mcpClient().callTool({
99
+ name: "connect",
100
+ arguments: {
101
+ options: [
102
+ {
103
+ connectionString: "mongodb://localhost:12345",
104
+ },
105
+ ],
106
+ },
107
+ });
108
+ const content = getResponseContent(response.content);
109
+ expect(content).toContain("Failed to connect to MongoDB at 'mongodb://localhost:12345'");
110
+ expect(content).toContain(
111
+ `Your config lists a different connection string: '${config.connectionString}' - do you want to try connecting to it instead?`
112
+ );
113
+ });
114
+
115
+ it("returns error message if the config connection string matches the argument", async () => {
116
+ config.connectionString = "mongodb://localhost:12345";
117
+ const response = await integration.mcpClient().callTool({
118
+ name: "connect",
119
+ arguments: {
120
+ options: [
121
+ {
122
+ connectionString: "mongodb://localhost:12345",
123
+ },
124
+ ],
125
+ },
126
+ });
127
+
128
+ const content = getResponseContent(response.content);
129
+
130
+ // Should be handled by default error handler and not suggest the config connection string
131
+ // because it matches the argument connection string
132
+ expect(content).toContain("Error running connect");
133
+ expect(content).not.toContain("Your config lists a different connection string");
134
+ });
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,104 @@
1
+ import { getResponseElements, getResponseContent, validateParameters, setupIntegrationTest } from "../../../helpers.js";
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";
6
+
7
+ describe("listCollections tool", () => {
8
+ const integration = setupIntegrationTest();
9
+
10
+ it("should have correct metadata", async () => {
11
+ const { tools } = await integration.mcpClient().listTools();
12
+ const listCollections = tools.find((tool) => tool.name === "list-collections")!;
13
+ expect(listCollections).toBeDefined();
14
+ expect(listCollections.description).toBe("List all collections for a given database");
15
+
16
+ validateParameters(listCollections, [
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
+ });
39
+
40
+ describe("with non-existent database", () => {
41
+ it("returns no collections", async () => {
42
+ await integration.connectMcpClient();
43
+ const response = await integration.mcpClient().callTool({
44
+ name: "list-collections",
45
+ arguments: { database: "non-existent" },
46
+ });
47
+ const content = getResponseContent(response.content);
48
+ expect(content).toEqual(
49
+ `No collections found for database "non-existent". To create a collection, use the "create-collection" tool.`
50
+ );
51
+ });
52
+ });
53
+
54
+ describe("with existing database", () => {
55
+ it("returns collections", async () => {
56
+ const mongoClient = integration.mongoClient();
57
+ await mongoClient.db(integration.randomDbName()).createCollection("collection-1");
58
+
59
+ await integration.connectMcpClient();
60
+ const response = await integration.mcpClient().callTool({
61
+ name: "list-collections",
62
+ arguments: { database: integration.randomDbName() },
63
+ });
64
+ const items = getResponseElements(response.content);
65
+ expect(items).toHaveLength(1);
66
+ expect(items[0].text).toContain('Name: "collection-1"');
67
+
68
+ await mongoClient.db(integration.randomDbName()).createCollection("collection-2");
69
+
70
+ const response2 = await integration.mcpClient().callTool({
71
+ name: "list-collections",
72
+ arguments: { database: integration.randomDbName() },
73
+ });
74
+ const items2 = getResponseElements(response2.content);
75
+ expect(items2).toHaveLength(2);
76
+ expect(items2.map((item) => item.text)).toIncludeSameMembers([
77
+ 'Name: "collection-1"',
78
+ 'Name: "collection-2"',
79
+ ]);
80
+ });
81
+ });
82
+
83
+ describe("when not connected", () => {
84
+ it("connects automatically if connection string is configured", async () => {
85
+ config.connectionString = integration.connectionString();
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
+ });
95
+
96
+ it("throws an error if connection string is not configured", async () => {
97
+ const response = await integration
98
+ .mcpClient()
99
+ .callTool({ name: "list-collections", arguments: { database: integration.randomDbName() } });
100
+ const content = getResponseContent(response.content);
101
+ expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,67 @@
1
+ import config from "../../../../../src/config.js";
2
+ import { getResponseElements, getParameters, setupIntegrationTest, getResponseContent } from "../../../helpers.js";
3
+ import { toIncludeSameMembers } from "jest-extended";
4
+
5
+ describe("listDatabases tool", () => {
6
+ const integration = setupIntegrationTest();
7
+
8
+ it("should have correct metadata", async () => {
9
+ const { tools } = await integration.mcpClient().listTools();
10
+ const listDatabases = tools.find((tool) => tool.name === "list-databases")!;
11
+ expect(listDatabases).toBeDefined();
12
+ expect(listDatabases.description).toBe("List all databases for a MongoDB connection");
13
+
14
+ const parameters = getParameters(listDatabases);
15
+ expect(parameters).toHaveLength(0);
16
+ });
17
+
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
+ describe("with no preexisting databases", () => {
36
+ it("returns only the system databases", async () => {
37
+ await integration.connectMcpClient();
38
+ const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
39
+ const dbNames = getDbNames(response.content);
40
+
41
+ expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]);
42
+ });
43
+ });
44
+
45
+ describe("with preexisting databases", () => {
46
+ it("returns their names and sizes", async () => {
47
+ const mongoClient = integration.mongoClient();
48
+ await mongoClient.db("foo").collection("bar").insertOne({ test: "test" });
49
+ await mongoClient.db("baz").collection("qux").insertOne({ test: "test" });
50
+
51
+ await integration.connectMcpClient();
52
+
53
+ const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
54
+ const dbNames = getDbNames(response.content);
55
+ expect(dbNames).toIncludeSameMembers(["admin", "config", "local", "foo", "baz"]);
56
+ });
57
+ });
58
+ });
59
+
60
+ function getDbNames(content: unknown): (string | null)[] {
61
+ const responseItems = getResponseElements(content);
62
+
63
+ return responseItems.map((item) => {
64
+ const match = item.text.match(/Name: (.*), Size: \d+ bytes/);
65
+ return match ? match[1] : null;
66
+ });
67
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ getResponseContent,
3
+ validateParameters,
4
+ dbOperationParameters,
5
+ setupIntegrationTest,
6
+ } from "../../../helpers.js";
7
+ import { toIncludeSameMembers } from "jest-extended";
8
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
9
+ import { ObjectId } from "mongodb";
10
+ import config from "../../../../../src/config.js";
11
+
12
+ describe("count tool", () => {
13
+ const integration = setupIntegrationTest();
14
+
15
+ let randomDbName: string;
16
+ beforeEach(() => {
17
+ randomDbName = new ObjectId().toString();
18
+ });
19
+
20
+ it("should have correct metadata", async () => {
21
+ const { tools } = await integration.mcpClient().listTools();
22
+ const listCollections = tools.find((tool) => tool.name === "count")!;
23
+ expect(listCollections).toBeDefined();
24
+ expect(listCollections.description).toBe("Gets the number of documents in a MongoDB collection");
25
+
26
+ validateParameters(listCollections, [
27
+ {
28
+ name: "query",
29
+ description:
30
+ "The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()",
31
+ type: "object",
32
+ required: false,
33
+ },
34
+ ...dbOperationParameters,
35
+ ]);
36
+ });
37
+
38
+ describe("with invalid arguments", () => {
39
+ const args = [
40
+ {},
41
+ { database: 123, collection: "bar" },
42
+ { foo: "bar", database: "test", collection: "bar" },
43
+ { collection: [], database: "test" },
44
+ { collection: "bar", database: "test", query: "{ $gt: { foo: 5 } }" },
45
+ ];
46
+ for (const arg of args) {
47
+ it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
48
+ await integration.connectMcpClient();
49
+ try {
50
+ await integration.mcpClient().callTool({ name: "count", arguments: arg });
51
+ expect.fail("Expected an error to be thrown");
52
+ } catch (error) {
53
+ expect(error).toBeInstanceOf(McpError);
54
+ const mcpError = error as McpError;
55
+ expect(mcpError.code).toEqual(-32602);
56
+ expect(mcpError.message).toContain("Invalid arguments for tool count");
57
+ }
58
+ });
59
+ }
60
+ });
61
+
62
+ it("returns 0 when database doesn't exist", async () => {
63
+ await integration.connectMcpClient();
64
+ const response = await integration.mcpClient().callTool({
65
+ name: "count",
66
+ arguments: { database: "non-existent", collection: "foos" },
67
+ });
68
+ const content = getResponseContent(response.content);
69
+ expect(content).toEqual('Found 0 documents in the collection "foos"');
70
+ });
71
+
72
+ it("returns 0 when collection doesn't exist", async () => {
73
+ await integration.connectMcpClient();
74
+ const mongoClient = integration.mongoClient();
75
+ await mongoClient.db(randomDbName).collection("bar").insertOne({});
76
+ const response = await integration.mcpClient().callTool({
77
+ name: "count",
78
+ arguments: { database: randomDbName, collection: "non-existent" },
79
+ });
80
+ const content = getResponseContent(response.content);
81
+ expect(content).toEqual('Found 0 documents in the collection "non-existent"');
82
+ });
83
+
84
+ describe("with existing database", () => {
85
+ beforeEach(async () => {
86
+ const mongoClient = integration.mongoClient();
87
+ await mongoClient
88
+ .db(randomDbName)
89
+ .collection("foo")
90
+ .insertMany([
91
+ { name: "Peter", age: 5 },
92
+ { name: "Parker", age: 10 },
93
+ { name: "George", age: 15 },
94
+ ]);
95
+ });
96
+
97
+ const testCases = [
98
+ { filter: undefined, expectedCount: 3 },
99
+ { filter: {}, expectedCount: 3 },
100
+ { filter: { age: { $lt: 15 } }, expectedCount: 2 },
101
+ { filter: { age: { $gt: 5 }, name: { $regex: "^P" } }, expectedCount: 1 },
102
+ ];
103
+ for (const testCase of testCases) {
104
+ it(`returns ${testCase.expectedCount} documents for filter ${JSON.stringify(testCase.filter)}`, async () => {
105
+ await integration.connectMcpClient();
106
+ const response = await integration.mcpClient().callTool({
107
+ name: "count",
108
+ arguments: { database: randomDbName, collection: "foo", query: testCase.filter },
109
+ });
110
+
111
+ const content = getResponseContent(response.content);
112
+ expect(content).toEqual(`Found ${testCase.expectedCount} documents in the collection "foo"`);
113
+ });
114
+ }
115
+ });
116
+
117
+ describe("when not connected", () => {
118
+ it("connects automatically if connection string is configured", async () => {
119
+ config.connectionString = integration.connectionString();
120
+
121
+ const response = await integration.mcpClient().callTool({
122
+ name: "count",
123
+ arguments: { database: randomDbName, collection: "coll1" },
124
+ });
125
+ const content = getResponseContent(response.content);
126
+ expect(content).toEqual('Found 0 documents in the collection "coll1"');
127
+ });
128
+
129
+ it("throws an error if connection string is not configured", async () => {
130
+ const response = await integration.mcpClient().callTool({
131
+ name: "count",
132
+ arguments: { database: randomDbName, collection: "coll1" },
133
+ });
134
+ const content = getResponseContent(response.content);
135
+ expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
136
+ });
137
+ });
138
+ });