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.
Files changed (185) hide show
  1. package/.github/CODEOWNERS +3 -0
  2. package/.github/dependabot.yml +10 -0
  3. package/.github/workflows/code_health.yaml +53 -22
  4. package/.github/workflows/code_health_fork.yaml +106 -0
  5. package/.github/workflows/codeql.yml +34 -0
  6. package/.github/workflows/lint.yml +37 -0
  7. package/.github/workflows/prepare_release.yaml +6 -4
  8. package/.github/workflows/publish.yaml +6 -3
  9. package/.prettierrc.json +1 -1
  10. package/README.md +18 -0
  11. package/dist/common/atlas/apiClient.js +28 -4
  12. package/dist/common/atlas/apiClient.js.map +1 -1
  13. package/dist/config.js +4 -7
  14. package/dist/config.js.map +1 -1
  15. package/dist/errors.js +1 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/index.js +12 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/logger.js +72 -28
  20. package/dist/logger.js.map +1 -1
  21. package/dist/packageInfo.js +6 -0
  22. package/dist/packageInfo.js.map +1 -0
  23. package/dist/server.js +114 -10
  24. package/dist/server.js.map +1 -1
  25. package/dist/session.js +66 -16
  26. package/dist/session.js.map +1 -1
  27. package/dist/telemetry/constants.js +15 -0
  28. package/dist/telemetry/constants.js.map +1 -0
  29. package/dist/telemetry/eventCache.js +53 -0
  30. package/dist/telemetry/eventCache.js.map +1 -0
  31. package/dist/telemetry/telemetry.js +97 -0
  32. package/dist/telemetry/telemetry.js.map +1 -0
  33. package/dist/telemetry/types.js +2 -0
  34. package/dist/telemetry/types.js.map +1 -0
  35. package/dist/tools/atlas/atlasTool.js +8 -3
  36. package/dist/tools/atlas/atlasTool.js.map +1 -1
  37. package/dist/tools/atlas/{createAccessList.js → create/createAccessList.js} +1 -2
  38. package/dist/tools/atlas/create/createAccessList.js.map +1 -0
  39. package/dist/tools/atlas/{createDBUser.js → create/createDBUser.js} +1 -2
  40. package/dist/tools/atlas/create/createDBUser.js.map +1 -0
  41. package/dist/tools/atlas/{createFreeCluster.js → create/createFreeCluster.js} +5 -3
  42. package/dist/tools/atlas/create/createFreeCluster.js.map +1 -0
  43. package/dist/tools/atlas/{createProject.js → create/createProject.js} +1 -2
  44. package/dist/tools/atlas/create/createProject.js.map +1 -0
  45. package/dist/tools/atlas/metadata/connectCluster.js +97 -0
  46. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -0
  47. package/dist/tools/atlas/{inspectAccessList.js → read/inspectAccessList.js} +1 -2
  48. package/dist/tools/atlas/read/inspectAccessList.js.map +1 -0
  49. package/dist/tools/atlas/{inspectCluster.js → read/inspectCluster.js} +1 -2
  50. package/dist/tools/atlas/read/inspectCluster.js.map +1 -0
  51. package/dist/tools/atlas/{listClusters.js → read/listClusters.js} +1 -2
  52. package/dist/tools/atlas/read/listClusters.js.map +1 -0
  53. package/dist/tools/atlas/{listDBUsers.js → read/listDBUsers.js} +1 -2
  54. package/dist/tools/atlas/read/listDBUsers.js.map +1 -0
  55. package/dist/tools/atlas/{listOrgs.js → read/listOrgs.js} +1 -2
  56. package/dist/tools/atlas/read/listOrgs.js.map +1 -0
  57. package/dist/tools/atlas/{listProjects.js → read/listProjects.js} +11 -5
  58. package/dist/tools/atlas/read/listProjects.js.map +1 -0
  59. package/dist/tools/atlas/tools.js +12 -10
  60. package/dist/tools/atlas/tools.js.map +1 -1
  61. package/dist/tools/mongodb/create/insertMany.js +1 -1
  62. package/dist/tools/mongodb/create/insertMany.js.map +1 -1
  63. package/dist/tools/mongodb/delete/deleteMany.js +1 -2
  64. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
  65. package/dist/tools/mongodb/metadata/collectionSchema.js +15 -13
  66. package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -1
  67. package/dist/tools/mongodb/metadata/collectionStorageSize.js +32 -3
  68. package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -1
  69. package/dist/tools/mongodb/metadata/connect.js +59 -72
  70. package/dist/tools/mongodb/metadata/connect.js.map +1 -1
  71. package/dist/tools/mongodb/metadata/dbStats.js +6 -1
  72. package/dist/tools/mongodb/metadata/dbStats.js.map +1 -1
  73. package/dist/tools/mongodb/metadata/explain.js +14 -7
  74. package/dist/tools/mongodb/metadata/explain.js.map +1 -1
  75. package/dist/tools/mongodb/metadata/logs.js +45 -0
  76. package/dist/tools/mongodb/metadata/logs.js.map +1 -0
  77. package/dist/tools/mongodb/mongodbTool.js +42 -36
  78. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  79. package/dist/tools/mongodb/read/aggregate.js +4 -4
  80. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  81. package/dist/tools/mongodb/read/collectionIndexes.js +24 -5
  82. package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -1
  83. package/dist/tools/mongodb/read/count.js +1 -2
  84. package/dist/tools/mongodb/read/count.js.map +1 -1
  85. package/dist/tools/mongodb/read/find.js +5 -6
  86. package/dist/tools/mongodb/read/find.js.map +1 -1
  87. package/dist/tools/mongodb/tools.js +6 -2
  88. package/dist/tools/mongodb/tools.js.map +1 -1
  89. package/dist/tools/mongodb/update/renameCollection.js +28 -4
  90. package/dist/tools/mongodb/update/renameCollection.js.map +1 -1
  91. package/dist/tools/mongodb/update/updateMany.js +7 -11
  92. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  93. package/dist/tools/tool.js +68 -15
  94. package/dist/tools/tool.js.map +1 -1
  95. package/eslint.config.js +29 -10
  96. package/global.d.ts +1 -0
  97. package/package.json +7 -3
  98. package/scripts/apply.ts +9 -7
  99. package/scripts/filter.ts +3 -2
  100. package/src/common/atlas/apiClient.ts +44 -11
  101. package/src/config.ts +16 -17
  102. package/src/errors.ts +1 -1
  103. package/src/index.ts +12 -8
  104. package/src/logger.ts +92 -29
  105. package/src/packageInfo.ts +6 -0
  106. package/src/server.ts +160 -11
  107. package/src/session.ts +102 -22
  108. package/src/telemetry/constants.ts +15 -0
  109. package/src/telemetry/eventCache.ts +62 -0
  110. package/src/telemetry/telemetry.ts +125 -0
  111. package/src/telemetry/types.ts +77 -0
  112. package/src/tools/atlas/atlasTool.ts +7 -5
  113. package/src/tools/atlas/{createAccessList.ts → create/createAccessList.ts} +2 -4
  114. package/src/tools/atlas/{createDBUser.ts → create/createDBUser.ts} +3 -5
  115. package/src/tools/atlas/{createFreeCluster.ts → create/createFreeCluster.ts} +7 -6
  116. package/src/tools/atlas/{createProject.ts → create/createProject.ts} +3 -4
  117. package/src/tools/atlas/metadata/connectCluster.ts +114 -0
  118. package/src/tools/atlas/{inspectAccessList.ts → read/inspectAccessList.ts} +2 -4
  119. package/src/tools/atlas/{inspectCluster.ts → read/inspectCluster.ts} +3 -5
  120. package/src/tools/atlas/{listClusters.ts → read/listClusters.ts} +3 -5
  121. package/src/tools/atlas/{listDBUsers.ts → read/listDBUsers.ts} +3 -5
  122. package/src/tools/atlas/{listOrgs.ts → read/listOrgs.ts} +2 -4
  123. package/src/tools/atlas/{listProjects.ts → read/listProjects.ts} +15 -7
  124. package/src/tools/atlas/tools.ts +12 -10
  125. package/src/tools/mongodb/create/insertMany.ts +1 -1
  126. package/src/tools/mongodb/delete/deleteMany.ts +1 -2
  127. package/src/tools/mongodb/metadata/collectionSchema.ts +16 -14
  128. package/src/tools/mongodb/metadata/collectionStorageSize.ts +41 -3
  129. package/src/tools/mongodb/metadata/connect.ts +78 -76
  130. package/src/tools/mongodb/metadata/dbStats.ts +6 -1
  131. package/src/tools/mongodb/metadata/explain.ts +20 -7
  132. package/src/tools/mongodb/metadata/logs.ts +55 -0
  133. package/src/tools/mongodb/mongodbTool.ts +47 -40
  134. package/src/tools/mongodb/read/aggregate.ts +4 -4
  135. package/src/tools/mongodb/read/collectionIndexes.ts +29 -5
  136. package/src/tools/mongodb/read/count.ts +1 -2
  137. package/src/tools/mongodb/read/find.ts +5 -6
  138. package/src/tools/mongodb/tools.ts +6 -2
  139. package/src/tools/mongodb/update/renameCollection.ts +33 -4
  140. package/src/tools/mongodb/update/updateMany.ts +7 -11
  141. package/src/tools/tool.ts +89 -26
  142. package/tests/integration/helpers.ts +94 -107
  143. package/tests/integration/inMemoryTransport.ts +3 -2
  144. package/tests/integration/server.test.ts +75 -23
  145. package/tests/integration/tools/atlas/accessLists.test.ts +13 -15
  146. package/tests/integration/tools/atlas/atlasHelpers.ts +10 -13
  147. package/tests/integration/tools/atlas/clusters.test.ts +79 -16
  148. package/tests/integration/tools/atlas/dbUsers.test.ts +9 -9
  149. package/tests/integration/tools/atlas/orgs.test.ts +4 -4
  150. package/tests/integration/tools/atlas/projects.test.ts +10 -12
  151. package/tests/integration/tools/mongodb/create/createCollection.test.ts +19 -62
  152. package/tests/integration/tools/mongodb/create/createIndex.test.ts +41 -87
  153. package/tests/integration/tools/mongodb/create/insertMany.test.ts +35 -78
  154. package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +25 -62
  155. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +22 -71
  156. package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +29 -63
  157. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +154 -0
  158. package/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +86 -0
  159. package/tests/integration/tools/mongodb/metadata/connect.test.ts +88 -93
  160. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +104 -0
  161. package/tests/integration/tools/mongodb/metadata/explain.test.ts +171 -0
  162. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +28 -56
  163. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +32 -26
  164. package/tests/integration/tools/mongodb/metadata/logs.test.ts +83 -0
  165. package/tests/integration/tools/mongodb/mongodbHelpers.ts +176 -0
  166. package/tests/integration/tools/mongodb/read/aggregate.test.ts +99 -0
  167. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +99 -0
  168. package/tests/integration/tools/mongodb/read/count.test.ts +31 -79
  169. package/tests/integration/tools/mongodb/read/find.test.ts +182 -0
  170. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +194 -0
  171. package/tests/integration/tools/mongodb/update/updateMany.test.ts +238 -0
  172. package/tests/unit/telemetry.test.ts +200 -0
  173. package/tsconfig.jest.json +2 -1
  174. package/tsconfig.json +1 -1
  175. package/tsconfig.lint.json +8 -0
  176. package/dist/tools/atlas/createAccessList.js.map +0 -1
  177. package/dist/tools/atlas/createDBUser.js.map +0 -1
  178. package/dist/tools/atlas/createFreeCluster.js.map +0 -1
  179. package/dist/tools/atlas/createProject.js.map +0 -1
  180. package/dist/tools/atlas/inspectAccessList.js.map +0 -1
  181. package/dist/tools/atlas/inspectCluster.js.map +0 -1
  182. package/dist/tools/atlas/listClusters.js.map +0 -1
  183. package/dist/tools/atlas/listDBUsers.js.map +0 -1
  184. package/dist/tools/atlas/listOrgs.js.map +0 -1
  185. package/dist/tools/atlas/listProjects.js.map +0 -1
@@ -1,4 +1,5 @@
1
- import { ConnectTool } from "./metadata/connect.js";
1
+ // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
2
+ // import { ConnectTool } from "./metadata/connect.js";
2
3
  import { ListCollectionsTool } from "./metadata/listCollections.js";
3
4
  import { CollectionIndexesTool } from "./read/collectionIndexes.js";
4
5
  import { ListDatabasesTool } from "./metadata/listDatabases.js";
@@ -17,9 +18,11 @@ import { DropDatabaseTool } from "./delete/dropDatabase.js";
17
18
  import { DropCollectionTool } from "./delete/dropCollection.js";
18
19
  import { ExplainTool } from "./metadata/explain.js";
19
20
  import { CreateCollectionTool } from "./create/createCollection.js";
21
+ import { LogsTool } from "./metadata/logs.js";
20
22
 
21
23
  export const MongoDbTools = [
22
- ConnectTool,
24
+ // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
25
+ // ConnectTool,
23
26
  ListCollectionsTool,
24
27
  ListDatabasesTool,
25
28
  CollectionIndexesTool,
@@ -38,4 +41,5 @@ export const MongoDbTools = [
38
41
  DropCollectionTool,
39
42
  ExplainTool,
40
43
  CreateCollectionTool,
44
+ LogsTool,
41
45
  ];
@@ -1,14 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
- import { MongoDBToolBase } from "../mongodbTool.js";
3
+ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4
4
  import { ToolArgs, OperationType } from "../../tool.js";
5
5
 
6
6
  export class RenameCollectionTool extends MongoDBToolBase {
7
7
  protected name = "rename-collection";
8
8
  protected description = "Renames a collection in a MongoDB database";
9
9
  protected argsShape = {
10
- collection: z.string().describe("Collection name"),
11
- database: z.string().describe("Database name"),
10
+ ...DbOperationArgs,
12
11
  newName: z.string().describe("The new name for the collection"),
13
12
  dropTarget: z.boolean().optional().default(false).describe("If true, drops the target collection if it exists"),
14
13
  };
@@ -28,10 +27,40 @@ export class RenameCollectionTool extends MongoDBToolBase {
28
27
  return {
29
28
  content: [
30
29
  {
31
- text: `Collection \`${collection}\` renamed to \`${result.collectionName}\` in database \`${database}\`.`,
30
+ text: `Collection "${collection}" renamed to "${result.collectionName}" in database "${database}".`,
32
31
  type: "text",
33
32
  },
34
33
  ],
35
34
  };
36
35
  }
36
+
37
+ protected handleError(
38
+ error: unknown,
39
+ args: ToolArgs<typeof this.argsShape>
40
+ ): Promise<CallToolResult> | CallToolResult {
41
+ if (error instanceof Error && "codeName" in error) {
42
+ switch (error.codeName) {
43
+ case "NamespaceNotFound":
44
+ return {
45
+ content: [
46
+ {
47
+ text: `Cannot rename "${args.database}.${args.collection}" because it doesn't exist.`,
48
+ type: "text",
49
+ },
50
+ ],
51
+ };
52
+ case "NamespaceExists":
53
+ return {
54
+ content: [
55
+ {
56
+ text: `Cannot rename "${args.database}.${args.collection}" to "${args.newName}" because the target collection already exists. If you want to overwrite it, set the "dropTarget" argument to true.`,
57
+ type: "text",
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ }
63
+
64
+ return super.handleError(error, args);
65
+ }
37
66
  }
@@ -1,25 +1,21 @@
1
1
  import { z } from "zod";
2
2
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
- import { MongoDBToolBase } from "../mongodbTool.js";
3
+ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4
4
  import { ToolArgs, OperationType } from "../../tool.js";
5
5
 
6
6
  export class UpdateManyTool extends MongoDBToolBase {
7
7
  protected name = "update-many";
8
8
  protected description = "Updates all documents that match the specified filter for a collection";
9
9
  protected argsShape = {
10
- collection: z.string().describe("Collection name"),
11
- database: z.string().describe("Database name"),
10
+ ...DbOperationArgs,
12
11
  filter: z
13
- .object({})
14
- .passthrough()
12
+ .record(z.string(), z.unknown())
15
13
  .optional()
16
14
  .describe(
17
15
  "The selection criteria for the update, matching the syntax of the filter argument of db.collection.updateOne()"
18
16
  ),
19
17
  update: z
20
- .object({})
21
- .passthrough()
22
- .optional()
18
+ .record(z.string(), z.unknown())
23
19
  .describe("An update document describing the modifications to apply using update operator expressions"),
24
20
  upsert: z
25
21
  .boolean()
@@ -41,15 +37,15 @@ export class UpdateManyTool extends MongoDBToolBase {
41
37
  });
42
38
 
43
39
  let message = "";
44
- if (result.matchedCount === 0) {
45
- message = `No documents matched the filter.`;
40
+ if (result.matchedCount === 0 && result.modifiedCount === 0 && result.upsertedCount === 0) {
41
+ message = "No documents matched the filter.";
46
42
  } else {
47
43
  message = `Matched ${result.matchedCount} document(s).`;
48
44
  if (result.modifiedCount > 0) {
49
45
  message += ` Modified ${result.modifiedCount} document(s).`;
50
46
  }
51
47
  if (result.upsertedCount > 0) {
52
- message += ` Upserted ${result.upsertedCount} document(s) (with id: ${result.upsertedId?.toString()}).`;
48
+ message += ` Upserted ${result.upsertedCount} document with id: ${result.upsertedId?.toString()}.`;
53
49
  }
54
50
  }
55
51
 
package/src/tools/tool.ts CHANGED
@@ -1,14 +1,15 @@
1
- import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { z, ZodNever, ZodRawShape } from "zod";
3
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
1
+ import { z, type ZodRawShape, type ZodNever, AnyZodObject } from "zod";
2
+ import type { McpServer, RegisteredTool, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { Session } from "../session.js";
5
- import logger from "../logger.js";
6
- import { mongoLogId } from "mongodb-log-writer";
7
- import config from "../config.js";
5
+ import logger, { LogId } from "../logger.js";
6
+ import { Telemetry } from "../telemetry/telemetry.js";
7
+ import { type ToolEvent } from "../telemetry/types.js";
8
+ import { UserConfig } from "../config.js";
8
9
 
9
10
  export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
10
11
 
11
- export type OperationType = "metadata" | "read" | "create" | "delete" | "update" | "cluster";
12
+ export type OperationType = "metadata" | "read" | "create" | "delete" | "update";
12
13
  export type ToolCategory = "mongodb" | "atlas";
13
14
 
14
15
  export abstract class ToolBase {
@@ -24,7 +25,34 @@ export abstract class ToolBase {
24
25
 
25
26
  protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
26
27
 
27
- protected constructor(protected session: Session) {}
28
+ constructor(
29
+ protected readonly session: Session,
30
+ protected readonly config: UserConfig,
31
+ protected readonly telemetry: Telemetry
32
+ ) {}
33
+
34
+ /**
35
+ * Creates and emits a tool telemetry event
36
+ * @param startTime - Start time in milliseconds
37
+ * @param result - Whether the command succeeded or failed
38
+ * @param error - Optional error if the command failed
39
+ */
40
+ private async emitToolEvent(startTime: number, result: CallToolResult): Promise<void> {
41
+ const duration = Date.now() - startTime;
42
+ const event: ToolEvent = {
43
+ timestamp: new Date().toISOString(),
44
+ source: "mdbmcp",
45
+ properties: {
46
+ ...this.telemetry.getCommonProperties(),
47
+ command: this.name,
48
+ category: this.category,
49
+ component: "tool",
50
+ duration_ms: duration,
51
+ result: result.isError ? "failure" : "success",
52
+ },
53
+ };
54
+ await this.telemetry.emitEvents([event]);
55
+ }
28
56
 
29
57
  public register(server: McpServer): void {
30
58
  if (!this.verifyAllowed()) {
@@ -32,39 +60,70 @@ export abstract class ToolBase {
32
60
  }
33
61
 
34
62
  const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
63
+ const startTime = Date.now();
35
64
  try {
36
- // TODO: add telemetry here
37
- logger.debug(
38
- mongoLogId(1_000_006),
39
- "tool",
40
- `Executing ${this.name} with args: ${JSON.stringify(args)}`
41
- );
42
-
43
- return await this.execute(...args);
44
- } catch (error: unknown) {
45
- logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`);
65
+ logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);
46
66
 
47
- return await this.handleError(error);
67
+ const result = await this.execute(...args);
68
+ await this.emitToolEvent(startTime, result);
69
+ return result;
70
+ } catch (error: unknown) {
71
+ logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
72
+ const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
73
+ await this.emitToolEvent(startTime, toolResult).catch(() => {});
74
+ return toolResult;
48
75
  }
49
76
  };
50
77
 
51
78
  server.tool(this.name, this.description, this.argsShape, callback);
79
+
80
+ // This is very similar to RegisteredTool.update, but without the bugs around the name.
81
+ // In the upstream update method, the name is captured in the closure and not updated when
82
+ // the tool name changes. This means that you only get one name update before things end up
83
+ // in a broken state.
84
+ this.update = (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => {
85
+ const tools = server["_registeredTools"] as { [toolName: string]: RegisteredTool };
86
+ const existingTool = tools[this.name];
87
+
88
+ if (updates.name && updates.name !== this.name) {
89
+ delete tools[this.name];
90
+ this.name = updates.name;
91
+ tools[this.name] = existingTool;
92
+ }
93
+
94
+ if (updates.description) {
95
+ existingTool.description = updates.description;
96
+ this.description = updates.description;
97
+ }
98
+
99
+ if (updates.inputSchema) {
100
+ existingTool.inputSchema = updates.inputSchema;
101
+ }
102
+
103
+ server.sendToolListChanged();
104
+ };
52
105
  }
53
106
 
107
+ protected update?: (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => void;
108
+
54
109
  // Checks if a tool is allowed to run based on the config
55
- private verifyAllowed(): boolean {
110
+ protected verifyAllowed(): boolean {
56
111
  let errorClarification: string | undefined;
57
- if (config.disabledTools.includes(this.category)) {
112
+
113
+ // Check read-only mode first
114
+ if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) {
115
+ errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`;
116
+ } else if (this.config.disabledTools.includes(this.category)) {
58
117
  errorClarification = `its category, \`${this.category}\`,`;
59
- } else if (config.disabledTools.includes(this.operationType)) {
118
+ } else if (this.config.disabledTools.includes(this.operationType)) {
60
119
  errorClarification = `its operation type, \`${this.operationType}\`,`;
61
- } else if (config.disabledTools.includes(this.name)) {
120
+ } else if (this.config.disabledTools.includes(this.name)) {
62
121
  errorClarification = `it`;
63
122
  }
64
123
 
65
124
  if (errorClarification) {
66
125
  logger.debug(
67
- mongoLogId(1_000_010),
126
+ LogId.toolDisabled,
68
127
  "tool",
69
128
  `Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`
70
129
  );
@@ -76,15 +135,19 @@ export abstract class ToolBase {
76
135
  }
77
136
 
78
137
  // This method is intended to be overridden by subclasses to handle errors
79
- protected handleError(error: unknown): Promise<CallToolResult> | CallToolResult {
138
+ protected handleError(
139
+ error: unknown,
140
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
+ args: ToolArgs<typeof this.argsShape>
142
+ ): Promise<CallToolResult> | CallToolResult {
80
143
  return {
81
144
  content: [
82
145
  {
83
146
  type: "text",
84
147
  text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
148
+ isError: true,
85
149
  },
86
150
  ],
87
- isError: true,
88
151
  };
89
152
  }
90
153
  }
@@ -1,14 +1,10 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { InMemoryTransport } from "./inMemoryTransport.js";
3
3
  import { Server } from "../../src/server.js";
4
- import runner, { MongoCluster } from "mongodb-runner";
5
- import path from "path";
6
- import fs from "fs/promises";
7
- import { Session } from "../../src/session.js";
4
+ import { UserConfig } from "../../src/config.js";
5
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
8
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
- import { MongoClient, ObjectId } from "mongodb";
10
- import { toIncludeAllMembers } from "jest-extended";
11
- import config from "../../src/config.js";
7
+ import { Session } from "../../src/session.js";
12
8
 
13
9
  interface ParameterInfo {
14
10
  name: string;
@@ -22,30 +18,22 @@ type ToolInfo = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
22
18
  export interface IntegrationTest {
23
19
  mcpClient: () => Client;
24
20
  mcpServer: () => Server;
25
- mongoClient: () => MongoClient;
26
- connectionString: () => string;
27
- connectMcpClient: () => Promise<void>;
28
- randomDbName: () => string;
29
21
  }
30
22
 
31
- export function setupIntegrationTest(): IntegrationTest {
32
- let mongoCluster: runner.MongoCluster | undefined;
33
- let mongoClient: MongoClient | undefined;
34
-
23
+ export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest {
35
24
  let mcpClient: Client | undefined;
36
25
  let mcpServer: Server | undefined;
37
26
 
38
- let randomDbName: string;
39
-
40
27
  beforeAll(async () => {
28
+ const userConfig = getUserConfig();
41
29
  const clientTransport = new InMemoryTransport();
42
30
  const serverTransport = new InMemoryTransport();
43
31
 
44
32
  await serverTransport.start();
45
33
  await clientTransport.start();
46
34
 
47
- clientTransport.output.pipeTo(serverTransport.input);
48
- serverTransport.output.pipeTo(clientTransport.input);
35
+ void clientTransport.output.pipeTo(serverTransport.input);
36
+ void serverTransport.output.pipeTo(clientTransport.input);
49
37
 
50
38
  mcpClient = new Client(
51
39
  {
@@ -57,19 +45,35 @@ export function setupIntegrationTest(): IntegrationTest {
57
45
  }
58
46
  );
59
47
 
48
+ const session = new Session({
49
+ apiBaseUrl: userConfig.apiBaseUrl,
50
+ apiClientId: userConfig.apiClientId,
51
+ apiClientSecret: userConfig.apiClientSecret,
52
+ });
53
+
54
+ userConfig.telemetry = "disabled";
60
55
  mcpServer = new Server({
56
+ session,
57
+ userConfig,
61
58
  mcpServer: new McpServer({
62
59
  name: "test-server",
63
60
  version: "1.2.3",
64
61
  }),
65
- session: new Session(),
66
62
  });
67
63
  await mcpServer.connect(serverTransport);
68
64
  await mcpClient.connect(clientTransport);
69
65
  });
70
66
 
71
- beforeEach(async () => {
72
- randomDbName = new ObjectId().toString();
67
+ beforeEach(() => {
68
+ if (mcpServer) {
69
+ mcpServer.userConfig.telemetry = "disabled";
70
+ }
71
+ });
72
+
73
+ afterEach(async () => {
74
+ if (mcpServer) {
75
+ await mcpServer.session.close();
76
+ }
73
77
  });
74
78
 
75
79
  afterAll(async () => {
@@ -80,52 +84,6 @@ export function setupIntegrationTest(): IntegrationTest {
80
84
  mcpServer = undefined;
81
85
  });
82
86
 
83
- afterEach(async () => {
84
- await mcpServer?.session.close();
85
- config.connectionString = undefined;
86
- });
87
-
88
- beforeAll(async function () {
89
- // Downloading Windows executables in CI takes a long time because
90
- // they include debug symbols...
91
- const tmpDir = path.join(__dirname, "..", "tmp");
92
- await fs.mkdir(tmpDir, { recursive: true });
93
-
94
- // On Windows, we may have a situation where mongod.exe is not fully released by the OS
95
- // before we attempt to run it again, so we add a retry.
96
- let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
97
- for (let i = 0; i < 10; i++) {
98
- try {
99
- mongoCluster = await MongoCluster.start({
100
- tmpDir: dbsDir,
101
- logDir: path.join(tmpDir, "mongodb-runner", "logs"),
102
- topology: "standalone",
103
- });
104
-
105
- return;
106
- } catch (err) {
107
- if (i < 5) {
108
- // Just wait a little bit and retry
109
- console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
110
- await new Promise((resolve) => setTimeout(resolve, 1000));
111
- } else {
112
- // If we still fail after 5 seconds, try another db dir
113
- console.error(
114
- `Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
115
- );
116
- dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
117
- }
118
- }
119
- }
120
-
121
- throw new Error("Failed to start cluster after 10 attempts");
122
- }, 120_000);
123
-
124
- afterAll(async function () {
125
- await mongoCluster?.close();
126
- mongoCluster = undefined;
127
- });
128
-
129
87
  const getMcpClient = () => {
130
88
  if (!mcpClient) {
131
89
  throw new Error("beforeEach() hook not ran yet");
@@ -142,41 +100,25 @@ export function setupIntegrationTest(): IntegrationTest {
142
100
  return mcpServer;
143
101
  };
144
102
 
145
- const getConnectionString = () => {
146
- if (!mongoCluster) {
147
- throw new Error("beforeAll() hook not ran yet");
148
- }
149
-
150
- return mongoCluster.connectionString;
151
- };
152
-
153
103
  return {
154
104
  mcpClient: getMcpClient,
155
105
  mcpServer: getMcpServer,
156
- mongoClient: () => {
157
- if (!mongoClient) {
158
- mongoClient = new MongoClient(getConnectionString());
159
- }
160
- return mongoClient;
161
- },
162
- connectionString: getConnectionString,
163
- connectMcpClient: async () => {
164
- await getMcpClient().callTool({
165
- name: "connect",
166
- arguments: { options: [{ connectionString: getConnectionString() }] },
167
- });
168
- },
169
- randomDbName: () => randomDbName,
170
106
  };
171
107
  }
172
108
 
173
- export function getResponseContent(content: unknown): string {
109
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
110
+ export function getResponseContent(content: unknown | { content: unknown }): string {
174
111
  return getResponseElements(content)
175
112
  .map((item) => item.text)
176
113
  .join("\n");
177
114
  }
178
115
 
179
- export function getResponseElements(content: unknown): { type: string; text: string }[] {
116
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
117
+ export function getResponseElements(content: unknown | { content: unknown }): { type: string; text: string }[] {
118
+ if (typeof content === "object" && content !== null && "content" in content) {
119
+ content = (content as { content: unknown }).content;
120
+ }
121
+
180
122
  expect(Array.isArray(content)).toBe(true);
181
123
 
182
124
  const response = content as { type: string; text: string }[];
@@ -198,9 +140,9 @@ export async function connect(client: Client, connectionString: string): Promise
198
140
 
199
141
  export function getParameters(tool: ToolInfo): ParameterInfo[] {
200
142
  expect(tool.inputSchema.type).toBe("object");
201
- expect(tool.inputSchema.properties).toBeDefined();
143
+ expectDefined(tool.inputSchema.properties);
202
144
 
203
- return Object.entries(tool.inputSchema.properties!)
145
+ return Object.entries(tool.inputSchema.properties)
204
146
  .sort((a, b) => a[0].localeCompare(b[0]))
205
147
  .map(([key, value]) => {
206
148
  expect(value).toHaveProperty("type");
@@ -218,24 +160,69 @@ export function getParameters(tool: ToolInfo): ParameterInfo[] {
218
160
  });
219
161
  }
220
162
 
221
- export const dbOperationParameters: ParameterInfo[] = [
163
+ export const databaseParameters: ParameterInfo[] = [
222
164
  { name: "database", type: "string", description: "Database name", required: true },
165
+ ];
166
+
167
+ export const databaseCollectionParameters: ParameterInfo[] = [
168
+ ...databaseParameters,
223
169
  { name: "collection", type: "string", description: "Collection name", required: true },
224
170
  ];
225
171
 
226
- export function validateParameters(tool: ToolInfo, parameters: ParameterInfo[]): void {
227
- const toolParameters = getParameters(tool);
228
- expect(toolParameters).toHaveLength(parameters.length);
229
- expect(toolParameters).toIncludeAllMembers(parameters);
172
+ export const databaseCollectionInvalidArgs = [
173
+ {},
174
+ { database: "test" },
175
+ { collection: "foo" },
176
+ { database: 123, collection: "foo" },
177
+ { database: "test", collection: 123 },
178
+ { database: [], collection: "foo" },
179
+ { database: "test", collection: [] },
180
+ ];
181
+
182
+ export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }];
183
+
184
+ export function validateToolMetadata(
185
+ integration: IntegrationTest,
186
+ name: string,
187
+ description: string,
188
+ parameters: ParameterInfo[]
189
+ ): void {
190
+ it("should have correct metadata", async () => {
191
+ const { tools } = await integration.mcpClient().listTools();
192
+ const tool = tools.find((tool) => tool.name === name);
193
+ expectDefined(tool);
194
+ expect(tool.description).toBe(description);
195
+
196
+ const toolParameters = getParameters(tool);
197
+ expect(toolParameters).toHaveLength(parameters.length);
198
+ expect(toolParameters).toIncludeAllMembers(parameters);
199
+ });
230
200
  }
231
201
 
232
- export function describeAtlas(name: number | string | Function | jest.FunctionLike, fn: jest.EmptyFunction) {
233
- if (!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length) {
234
- return describe.skip("atlas", () => {
235
- describe(name, fn);
236
- });
237
- }
238
- return describe("atlas", () => {
239
- describe(name, fn);
202
+ export function validateThrowsForInvalidArguments(
203
+ integration: IntegrationTest,
204
+ name: string,
205
+ args: { [x: string]: unknown }[]
206
+ ): void {
207
+ describe("with invalid arguments", () => {
208
+ for (const arg of args) {
209
+ it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
210
+ try {
211
+ await integration.mcpClient().callTool({ name, arguments: arg });
212
+ throw new Error("Expected an error to be thrown");
213
+ } catch (error) {
214
+ expect((error as Error).message).not.toEqual("Expected an error to be thrown");
215
+ expect(error).toBeInstanceOf(McpError);
216
+ const mcpError = error as McpError;
217
+ expect(mcpError.code).toEqual(-32602);
218
+ expect(mcpError.message).toContain(`Invalid arguments for tool ${name}`);
219
+ }
220
+ });
221
+ }
240
222
  });
241
223
  }
224
+
225
+ /** Expects the argument being defined and asserts it */
226
+ export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
227
+ expect(arg).toBeDefined();
228
+ }
@@ -39,6 +39,7 @@ export class InMemoryTransport implements Transport {
39
39
  return Promise.resolve();
40
40
  }
41
41
 
42
+ // eslint-disable-next-line @typescript-eslint/require-await
42
43
  async close(): Promise<void> {
43
44
  this.outputController.close();
44
45
  this.onclose?.();
@@ -49,10 +50,10 @@ export class InMemoryTransport implements Transport {
49
50
  sessionId?: string | undefined;
50
51
 
51
52
  private static getPromise(): [Promise<void>, resolve: () => void] {
52
- let resolve: () => void;
53
+ let resolve: () => void = () => {};
53
54
  const promise = new Promise<void>((res) => {
54
55
  resolve = res;
55
56
  });
56
- return [promise, resolve!];
57
+ return [promise, resolve];
57
58
  }
58
59
  }