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,7 +1,7 @@
1
1
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
3
3
  import { ToolArgs, OperationType } from "../../tool.js";
4
- import { parseSchema, SchemaField } from "mongodb-schema";
4
+ import { getSimplifiedSchema } from "mongodb-schema";
5
5
 
6
6
  export class CollectionSchemaTool extends MongoDBToolBase {
7
7
  protected name = "collection-schema";
@@ -13,29 +13,31 @@ export class CollectionSchemaTool extends MongoDBToolBase {
13
13
  protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
14
14
  const provider = await this.ensureConnected();
15
15
  const documents = await provider.find(database, collection, {}, { limit: 5 }).toArray();
16
- const schema = await parseSchema(documents);
16
+ const schema = await getSimplifiedSchema(documents);
17
+
18
+ const fieldsCount = Object.entries(schema).length;
19
+ if (fieldsCount === 0) {
20
+ return {
21
+ content: [
22
+ {
23
+ text: `Could not deduce the schema for "${database}.${collection}". This may be because it doesn't exist or is empty.`,
24
+ type: "text",
25
+ },
26
+ ],
27
+ };
28
+ }
17
29
 
18
30
  return {
19
31
  content: [
20
32
  {
21
- text: `Found ${schema.fields.length} fields in the schema for \`${database}.${collection}\``,
33
+ text: `Found ${fieldsCount} fields in the schema for "${database}.${collection}"`,
22
34
  type: "text",
23
35
  },
24
36
  {
25
- text: this.formatFieldOutput(schema.fields),
37
+ text: JSON.stringify(schema),
26
38
  type: "text",
27
39
  },
28
40
  ],
29
41
  };
30
42
  }
31
-
32
- private formatFieldOutput(fields: SchemaField[]): string {
33
- let result = "| Field | Type | Confidence |\n";
34
- result += "|-------|------|-------------|\n";
35
- for (const field of fields) {
36
- const fieldType = Array.isArray(field.type) ? field.type.join(", ") : field.type;
37
- result += `| ${field.name} | \`${fieldType}\` | ${(field.probability * 100).toFixed(0)}% |\n`;
38
- }
39
- return result;
40
- }
41
43
  }
@@ -4,7 +4,7 @@ import { ToolArgs, OperationType } from "../../tool.js";
4
4
 
5
5
  export class CollectionStorageSizeTool extends MongoDBToolBase {
6
6
  protected name = "collection-storage-size";
7
- protected description = "Gets the size of the collection in MB";
7
+ protected description = "Gets the size of the collection";
8
8
  protected argsShape = DbOperationArgs;
9
9
 
10
10
  protected operationType: OperationType = "metadata";
@@ -14,17 +14,55 @@ export class CollectionStorageSizeTool extends MongoDBToolBase {
14
14
  const [{ value }] = (await provider
15
15
  .aggregate(database, collection, [
16
16
  { $collStats: { storageStats: {} } },
17
- { $group: { _id: null, value: { $sum: "$storageStats.storageSize" } } },
17
+ { $group: { _id: null, value: { $sum: "$storageStats.size" } } },
18
18
  ])
19
19
  .toArray()) as [{ value: number }];
20
20
 
21
+ const { units, value: scaledValue } = CollectionStorageSizeTool.getStats(value);
22
+
21
23
  return {
22
24
  content: [
23
25
  {
24
- text: `The size of \`${database}.${collection}\` is \`${(value / 1024 / 1024).toFixed(2)} MB\``,
26
+ text: `The size of "${database}.${collection}" is \`${scaledValue.toFixed(2)} ${units}\``,
25
27
  type: "text",
26
28
  },
27
29
  ],
28
30
  };
29
31
  }
32
+
33
+ protected handleError(
34
+ error: unknown,
35
+ args: ToolArgs<typeof this.argsShape>
36
+ ): Promise<CallToolResult> | CallToolResult {
37
+ if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
38
+ return {
39
+ content: [
40
+ {
41
+ text: `The size of "${args.database}.${args.collection}" cannot be determined because the collection does not exist.`,
42
+ type: "text",
43
+ },
44
+ ],
45
+ };
46
+ }
47
+
48
+ return super.handleError(error, args);
49
+ }
50
+
51
+ private static getStats(value: number): { value: number; units: string } {
52
+ const kb = 1024;
53
+ const mb = kb * 1024;
54
+ const gb = mb * 1024;
55
+
56
+ if (value > gb) {
57
+ return { value: value / gb, units: "GB" };
58
+ }
59
+
60
+ if (value > mb) {
61
+ return { value: value / mb, units: "MB" };
62
+ }
63
+ if (value > kb) {
64
+ return { value: value / kb, units: "KB" };
65
+ }
66
+ return { value, units: "bytes" };
67
+ }
30
68
  }
@@ -2,93 +2,95 @@ import { z } from "zod";
2
2
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { MongoDBToolBase } from "../mongodbTool.js";
4
4
  import { ToolArgs, OperationType } from "../../tool.js";
5
- import config from "../../../config.js";
6
- import { MongoError as DriverError } from "mongodb";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import assert from "assert";
7
+ import { UserConfig } from "../../../config.js";
8
+ import { Telemetry } from "../../../telemetry/telemetry.js";
9
+ import { Session } from "../../../session.js";
10
+
11
+ const disconnectedSchema = z
12
+ .object({
13
+ connectionString: z.string().describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format)"),
14
+ })
15
+ .describe("Options for connecting to MongoDB.");
16
+
17
+ const connectedSchema = z
18
+ .object({
19
+ connectionString: z
20
+ .string()
21
+ .optional()
22
+ .describe("MongoDB connection string to switch to (in the mongodb:// or mongodb+srv:// format)"),
23
+ })
24
+ .describe(
25
+ "Options for switching the current MongoDB connection. If a connection string is not provided, the connection string from the config will be used."
26
+ );
27
+
28
+ const connectedName = "switch-connection" as const;
29
+ const disconnectedName = "connect" as const;
30
+
31
+ const connectedDescription =
32
+ "Switch to a different MongoDB connection. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new instance.";
33
+ const disconnectedDescription = "Connect to a MongoDB instance";
7
34
 
8
35
  export class ConnectTool extends MongoDBToolBase {
9
- protected name = "connect";
10
- protected description = "Connect to a MongoDB instance";
36
+ protected name: typeof connectedName | typeof disconnectedName = disconnectedName;
37
+ protected description: typeof connectedDescription | typeof disconnectedDescription = disconnectedDescription;
38
+
39
+ // Here the default is empty just to trigger registration, but we're going to override it with the correct
40
+ // schema in the register method.
11
41
  protected argsShape = {
12
- options: z
13
- .array(
14
- z
15
- .union([
16
- z.object({
17
- connectionString: z
18
- .string()
19
- .describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format)"),
20
- }),
21
- z.object({
22
- clusterName: z.string().describe("MongoDB cluster name"),
23
- }),
24
- ])
25
- .optional()
26
- )
27
- .optional()
28
- .describe(
29
- "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."
30
- ),
42
+ connectionString: z.string().optional(),
31
43
  };
32
44
 
33
45
  protected operationType: OperationType = "metadata";
34
46
 
35
- protected async execute({ options: optionsArr }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
36
- const options = optionsArr?.[0];
37
- let connectionString: string;
38
- if (!options && !config.connectionString) {
39
- return {
40
- content: [
41
- { type: "text", text: "No connection details provided." },
42
- { type: "text", text: "Please provide either a connection string or a cluster name" },
43
- ],
44
- };
45
- }
47
+ constructor(session: Session, config: UserConfig, telemetry: Telemetry) {
48
+ super(session, config, telemetry);
49
+ session.on("close", () => {
50
+ this.updateMetadata();
51
+ });
52
+ }
46
53
 
47
- if (!options) {
48
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
49
- connectionString = config.connectionString!;
50
- } else if ("connectionString" in options) {
51
- connectionString = options.connectionString;
52
- } else {
53
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/19
54
- // We don't support connecting via cluster name since we'd need to obtain the user credentials
55
- // and fill in the connection string.
56
- return {
57
- content: [
58
- {
59
- type: "text",
60
- text: `Connecting via cluster name not supported yet. Please provide a connection string.`,
61
- },
62
- ],
63
- };
54
+ protected async execute({ connectionString }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
55
+ switch (this.name) {
56
+ case disconnectedName:
57
+ assert(connectionString, "Connection string is required");
58
+ break;
59
+ case connectedName:
60
+ connectionString ??= this.config.connectionString;
61
+ assert(
62
+ connectionString,
63
+ "Cannot switch to a new connection because no connection string was provided and no default connection string is configured."
64
+ );
65
+ break;
64
66
  }
65
67
 
66
- try {
67
- await this.connectToMongoDB(connectionString);
68
- return {
69
- content: [{ type: "text", text: `Successfully connected to ${connectionString}.` }],
70
- };
71
- } catch (error) {
72
- // Sometimes the model will supply an incorrect connection string. If the user has configured
73
- // a different one as environment variable or a cli argument, suggest using that one instead.
74
- if (
75
- config.connectionString &&
76
- error instanceof DriverError &&
77
- config.connectionString !== connectionString
78
- ) {
79
- return {
80
- content: [
81
- {
82
- type: "text",
83
- text:
84
- `Failed to connect to MongoDB at '${connectionString}' due to error: '${error.message}.` +
85
- `Your config lists a different connection string: '${config.connectionString}' - do you want to try connecting to it instead?`,
86
- },
87
- ],
88
- };
89
- }
68
+ await this.connectToMongoDB(connectionString);
69
+ this.updateMetadata();
70
+ return {
71
+ content: [{ type: "text", text: "Successfully connected to MongoDB." }],
72
+ };
73
+ }
74
+
75
+ public register(server: McpServer): void {
76
+ super.register(server);
90
77
 
91
- throw error;
78
+ this.updateMetadata();
79
+ }
80
+
81
+ private updateMetadata(): void {
82
+ if (this.config.connectionString || this.session.serviceProvider) {
83
+ this.update?.({
84
+ name: connectedName,
85
+ description: connectedDescription,
86
+ inputSchema: connectedSchema,
87
+ });
88
+ } else {
89
+ this.update?.({
90
+ name: disconnectedName,
91
+ description: disconnectedDescription,
92
+ inputSchema: disconnectedSchema,
93
+ });
92
94
  }
93
95
  }
94
96
  }
@@ -1,6 +1,7 @@
1
1
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
3
3
  import { ToolArgs, OperationType } from "../../tool.js";
4
+ import { EJSON } from "bson";
4
5
 
5
6
  export class DbStatsTool extends MongoDBToolBase {
6
7
  protected name = "db-stats";
@@ -21,7 +22,11 @@ export class DbStatsTool extends MongoDBToolBase {
21
22
  return {
22
23
  content: [
23
24
  {
24
- text: `Statistics for database ${database}: ${JSON.stringify(result)}`,
25
+ text: `Statistics for database ${database}`,
26
+ type: "text",
27
+ },
28
+ {
29
+ text: EJSON.stringify(result),
25
30
  type: "text",
26
31
  },
27
32
  ],
@@ -47,14 +47,24 @@ export class ExplainTool extends MongoDBToolBase {
47
47
  const method = methods[0];
48
48
 
49
49
  if (!method) {
50
- throw new Error("No method provided");
50
+ throw new Error("No method provided. Expected one of the following: `aggregate`, `find`, or `count`");
51
51
  }
52
52
 
53
53
  let result: Document;
54
54
  switch (method.name) {
55
55
  case "aggregate": {
56
56
  const { pipeline } = method.arguments;
57
- result = await provider.aggregate(database, collection, pipeline).explain(ExplainTool.defaultVerbosity);
57
+ result = await provider
58
+ .aggregate(
59
+ database,
60
+ collection,
61
+ pipeline,
62
+ {},
63
+ {
64
+ writeConcern: undefined,
65
+ }
66
+ )
67
+ .explain(ExplainTool.defaultVerbosity);
58
68
  break;
59
69
  }
60
70
  case "find": {
@@ -66,10 +76,13 @@ export class ExplainTool extends MongoDBToolBase {
66
76
  }
67
77
  case "count": {
68
78
  const { query } = method.arguments;
69
- // This helper doesn't have explain() command but does have the argument explain
70
- result = (await provider.count(database, collection, query, {
71
- explain: ExplainTool.defaultVerbosity,
72
- })) as unknown as Document;
79
+ result = await provider.mongoClient.db(database).command({
80
+ explain: {
81
+ count: collection,
82
+ query,
83
+ },
84
+ verbosity: ExplainTool.defaultVerbosity,
85
+ });
73
86
  break;
74
87
  }
75
88
  }
@@ -77,7 +90,7 @@ export class ExplainTool extends MongoDBToolBase {
77
90
  return {
78
91
  content: [
79
92
  {
80
- text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in \`${database}.${collection}\`. This information can be used to understand how the query was executed and to optimize the query performance.`,
93
+ text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in "${database}.${collection}". This information can be used to understand how the query was executed and to optimize the query performance.`,
81
94
  type: "text",
82
95
  },
83
96
  {
@@ -0,0 +1,55 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import { MongoDBToolBase } from "../mongodbTool.js";
3
+ import { ToolArgs, OperationType } from "../../tool.js";
4
+ import { z } from "zod";
5
+
6
+ export class LogsTool extends MongoDBToolBase {
7
+ protected name = "mongodb-logs";
8
+ protected description = "Returns the most recent logged mongod events";
9
+ protected argsShape = {
10
+ type: z
11
+ .enum(["global", "startupWarnings"])
12
+ .optional()
13
+ .default("global")
14
+ .describe(
15
+ "The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started."
16
+ ),
17
+ limit: z
18
+ .number()
19
+ .int()
20
+ .max(1024)
21
+ .min(1)
22
+ .optional()
23
+ .default(50)
24
+ .describe("The maximum number of log entries to return."),
25
+ };
26
+
27
+ protected operationType: OperationType = "metadata";
28
+
29
+ protected async execute({ type, limit }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
30
+ const provider = await this.ensureConnected();
31
+
32
+ const result = await provider.runCommandWithCheck("admin", {
33
+ getLog: type,
34
+ });
35
+
36
+ const logs = (result.log as string[]).slice(0, limit);
37
+
38
+ return {
39
+ content: [
40
+ {
41
+ text: `Found: ${result.totalLinesWritten} messages`,
42
+ type: "text",
43
+ },
44
+
45
+ ...logs.map(
46
+ (log) =>
47
+ ({
48
+ text: log,
49
+ type: "text",
50
+ }) as const
51
+ ),
52
+ ],
53
+ };
54
+ }
55
+ }
@@ -1,10 +1,9 @@
1
1
  import { z } from "zod";
2
- import { ToolBase, ToolCategory } from "../tool.js";
3
- import { Session } from "../../session.js";
2
+ import { ToolArgs, ToolBase, ToolCategory } from "../tool.js";
4
3
  import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
5
4
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
6
5
  import { ErrorCodes, MongoDBError } from "../../errors.js";
7
- import config from "../../config.js";
6
+ import logger, { LogId } from "../../logger.js";
8
7
 
9
8
  export const DbOperationArgs = {
10
9
  database: z.string().describe("Database name"),
@@ -12,15 +11,20 @@ export const DbOperationArgs = {
12
11
  };
13
12
 
14
13
  export abstract class MongoDBToolBase extends ToolBase {
15
- constructor(session: Session) {
16
- super(session);
17
- }
18
-
19
14
  protected category: ToolCategory = "mongodb";
20
15
 
21
16
  protected async ensureConnected(): Promise<NodeDriverServiceProvider> {
22
- if (!this.session.serviceProvider && config.connectionString) {
23
- await this.connectToMongoDB(config.connectionString);
17
+ if (!this.session.serviceProvider && this.config.connectionString) {
18
+ try {
19
+ await this.connectToMongoDB(this.config.connectionString);
20
+ } catch (error) {
21
+ logger.error(
22
+ LogId.mongodbConnectFailure,
23
+ "mongodbTool",
24
+ `Failed to connect to MongoDB instance using the connection string from the config: ${error as string}`
25
+ );
26
+ throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, "Not connected to MongoDB.");
27
+ }
24
28
  }
25
29
 
26
30
  if (!this.session.serviceProvider) {
@@ -30,40 +34,43 @@ export abstract class MongoDBToolBase extends ToolBase {
30
34
  return this.session.serviceProvider;
31
35
  }
32
36
 
33
- protected handleError(error: unknown): Promise<CallToolResult> | CallToolResult {
34
- if (error instanceof MongoDBError && error.code === ErrorCodes.NotConnectedToMongoDB) {
35
- return {
36
- content: [
37
- {
38
- type: "text",
39
- text: "You need to connect to a MongoDB instance before you can access its data.",
40
- },
41
- {
42
- type: "text",
43
- text: "Please use the 'connect' tool to connect to a MongoDB instance.",
44
- },
45
- ],
46
- isError: true,
47
- };
37
+ protected handleError(
38
+ error: unknown,
39
+ args: ToolArgs<typeof this.argsShape>
40
+ ): Promise<CallToolResult> | CallToolResult {
41
+ if (error instanceof MongoDBError) {
42
+ switch (error.code) {
43
+ case ErrorCodes.NotConnectedToMongoDB:
44
+ return {
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: "You need to connect to a MongoDB instance before you can access its data.",
49
+ },
50
+ {
51
+ type: "text",
52
+ text: "Please use the 'connect' or 'switch-connection' tool to connect to a MongoDB instance.",
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ case ErrorCodes.MisconfiguredConnectionString:
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text",
62
+ text: "The configured connection string is not valid. Please check the connection string and confirm it points to a valid MongoDB instance. Alternatively, use the 'switch-connection' tool to connect to a different instance.",
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
48
68
  }
49
69
 
50
- return super.handleError(error);
70
+ return super.handleError(error, args);
51
71
  }
52
72
 
53
- protected async connectToMongoDB(connectionString: string): Promise<void> {
54
- const provider = await NodeDriverServiceProvider.connect(connectionString, {
55
- productDocsLink: "https://docs.mongodb.com/todo-mcp",
56
- productName: "MongoDB MCP",
57
- readConcern: {
58
- level: config.connectOptions.readConcern,
59
- },
60
- readPreference: config.connectOptions.readPreference,
61
- writeConcern: {
62
- w: config.connectOptions.writeConcern,
63
- },
64
- timeoutMS: config.connectOptions.timeoutMS,
65
- });
66
-
67
- this.session.serviceProvider = provider;
73
+ protected connectToMongoDB(connectionString: string): Promise<void> {
74
+ return this.session.connectToMongoDB(connectionString, this.config.connectOptions);
68
75
  }
69
76
  }
@@ -2,10 +2,10 @@ import { z } from "zod";
2
2
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4
4
  import { ToolArgs, OperationType } from "../../tool.js";
5
+ import { EJSON } from "bson";
5
6
 
6
7
  export const AggregateArgs = {
7
- pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
8
- limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
8
+ pipeline: z.array(z.record(z.string(), z.unknown())).describe("An array of aggregation stages to execute"),
9
9
  };
10
10
 
11
11
  export class AggregateTool extends MongoDBToolBase {
@@ -27,12 +27,12 @@ export class AggregateTool extends MongoDBToolBase {
27
27
 
28
28
  const content: Array<{ text: string; type: "text" }> = [
29
29
  {
30
- text: `Found ${documents.length} documents in the collection \`${collection}\`:`,
30
+ text: `Found ${documents.length} documents in the collection "${collection}":`,
31
31
  type: "text",
32
32
  },
33
33
  ...documents.map((doc) => {
34
34
  return {
35
- text: JSON.stringify(doc),
35
+ text: EJSON.stringify(doc),
36
36
  type: "text",
37
37
  } as { text: string; type: "text" };
38
38
  }),
@@ -13,12 +13,36 @@ export class CollectionIndexesTool extends MongoDBToolBase {
13
13
  const indexes = await provider.getIndexes(database, collection);
14
14
 
15
15
  return {
16
- content: indexes.map((indexDefinition) => {
17
- return {
18
- text: `Field: ${indexDefinition.name}: ${JSON.stringify(indexDefinition.key)}`,
16
+ content: [
17
+ {
18
+ text: `Found ${indexes.length} indexes in the collection "${collection}":`,
19
19
  type: "text",
20
- };
21
- }),
20
+ },
21
+ ...(indexes.map((indexDefinition) => {
22
+ return {
23
+ text: `Name "${indexDefinition.name}", definition: ${JSON.stringify(indexDefinition.key)}`,
24
+ type: "text",
25
+ };
26
+ }) as { text: string; type: "text" }[]),
27
+ ],
22
28
  };
23
29
  }
30
+
31
+ protected handleError(
32
+ error: unknown,
33
+ args: ToolArgs<typeof this.argsShape>
34
+ ): Promise<CallToolResult> | CallToolResult {
35
+ if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
36
+ return {
37
+ content: [
38
+ {
39
+ text: `The indexes for "${args.database}.${args.collection}" cannot be determined because the collection does not exist.`,
40
+ type: "text",
41
+ },
42
+ ],
43
+ };
44
+ }
45
+
46
+ return super.handleError(error, args);
47
+ }
24
48
  }
@@ -5,8 +5,7 @@ import { z } from "zod";
5
5
 
6
6
  export const CountArgs = {
7
7
  query: z
8
- .object({})
9
- .passthrough()
8
+ .record(z.string(), z.unknown())
10
9
  .optional()
11
10
  .describe(
12
11
  "The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()"
@@ -3,16 +3,15 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
4
4
  import { ToolArgs, OperationType } from "../../tool.js";
5
5
  import { SortDirection } from "mongodb";
6
+ import { EJSON } from "bson";
6
7
 
7
8
  export const FindArgs = {
8
9
  filter: z
9
- .object({})
10
- .passthrough()
10
+ .record(z.string(), z.unknown())
11
11
  .optional()
12
12
  .describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
13
13
  projection: z
14
- .object({})
15
- .passthrough()
14
+ .record(z.string(), z.unknown())
16
15
  .optional()
17
16
  .describe("The projection, matching the syntax of the projection argument of db.collection.find()"),
18
17
  limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
@@ -44,12 +43,12 @@ export class FindTool extends MongoDBToolBase {
44
43
 
45
44
  const content: Array<{ text: string; type: "text" }> = [
46
45
  {
47
- text: `Found ${documents.length} documents in the collection \`${collection}\`:`,
46
+ text: `Found ${documents.length} documents in the collection "${collection}":`,
48
47
  type: "text",
49
48
  },
50
49
  ...documents.map((doc) => {
51
50
  return {
52
- text: JSON.stringify(doc),
51
+ text: EJSON.stringify(doc),
53
52
  type: "text",
54
53
  } as { text: string; type: "text" };
55
54
  }),