mongodb-mcp-server 0.0.4 → 0.0.5

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 (149) hide show
  1. package/.github/workflows/code_health.yaml +53 -4
  2. package/.github/workflows/prepare_release.yaml +4 -4
  3. package/README.md +2 -0
  4. package/dist/common/atlas/apiClient.js +30 -2
  5. package/dist/common/atlas/apiClient.js.map +1 -1
  6. package/dist/config.js +2 -7
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.js +10 -4
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger.js +52 -27
  11. package/dist/logger.js.map +1 -1
  12. package/dist/packageInfo.js +6 -0
  13. package/dist/packageInfo.js.map +1 -0
  14. package/dist/server.js +71 -8
  15. package/dist/server.js.map +1 -1
  16. package/dist/session.js +17 -12
  17. package/dist/session.js.map +1 -1
  18. package/dist/telemetry/constants.js +15 -0
  19. package/dist/telemetry/constants.js.map +1 -0
  20. package/dist/telemetry/eventCache.js +53 -0
  21. package/dist/telemetry/eventCache.js.map +1 -0
  22. package/dist/telemetry/telemetry.js +96 -0
  23. package/dist/telemetry/telemetry.js.map +1 -0
  24. package/dist/telemetry/types.js +2 -0
  25. package/dist/telemetry/types.js.map +1 -0
  26. package/dist/tools/atlas/atlasTool.js +8 -3
  27. package/dist/tools/atlas/atlasTool.js.map +1 -1
  28. package/dist/tools/atlas/createAccessList.js +0 -1
  29. package/dist/tools/atlas/createAccessList.js.map +1 -1
  30. package/dist/tools/atlas/createDBUser.js +0 -1
  31. package/dist/tools/atlas/createDBUser.js.map +1 -1
  32. package/dist/tools/atlas/createFreeCluster.js +0 -1
  33. package/dist/tools/atlas/createFreeCluster.js.map +1 -1
  34. package/dist/tools/atlas/createProject.js +0 -1
  35. package/dist/tools/atlas/createProject.js.map +1 -1
  36. package/dist/tools/atlas/inspectAccessList.js +0 -1
  37. package/dist/tools/atlas/inspectAccessList.js.map +1 -1
  38. package/dist/tools/atlas/inspectCluster.js +0 -1
  39. package/dist/tools/atlas/inspectCluster.js.map +1 -1
  40. package/dist/tools/atlas/listClusters.js +0 -1
  41. package/dist/tools/atlas/listClusters.js.map +1 -1
  42. package/dist/tools/atlas/listDBUsers.js +0 -1
  43. package/dist/tools/atlas/listDBUsers.js.map +1 -1
  44. package/dist/tools/atlas/listOrgs.js +0 -1
  45. package/dist/tools/atlas/listOrgs.js.map +1 -1
  46. package/dist/tools/atlas/listProjects.js +10 -4
  47. package/dist/tools/atlas/listProjects.js.map +1 -1
  48. package/dist/tools/mongodb/metadata/collectionSchema.js +15 -13
  49. package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -1
  50. package/dist/tools/mongodb/metadata/collectionStorageSize.js +32 -3
  51. package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -1
  52. package/dist/tools/mongodb/metadata/connect.js +5 -6
  53. package/dist/tools/mongodb/metadata/connect.js.map +1 -1
  54. package/dist/tools/mongodb/metadata/dbStats.js +6 -1
  55. package/dist/tools/mongodb/metadata/dbStats.js.map +1 -1
  56. package/dist/tools/mongodb/metadata/explain.js +14 -7
  57. package/dist/tools/mongodb/metadata/explain.js.map +1 -1
  58. package/dist/tools/mongodb/metadata/logs.js +45 -0
  59. package/dist/tools/mongodb/metadata/logs.js.map +1 -0
  60. package/dist/tools/mongodb/mongodbTool.js +10 -11
  61. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  62. package/dist/tools/mongodb/read/aggregate.js +3 -3
  63. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  64. package/dist/tools/mongodb/read/collectionIndexes.js +24 -5
  65. package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -1
  66. package/dist/tools/mongodb/read/find.js +3 -2
  67. package/dist/tools/mongodb/read/find.js.map +1 -1
  68. package/dist/tools/mongodb/tools.js +2 -0
  69. package/dist/tools/mongodb/tools.js.map +1 -1
  70. package/dist/tools/mongodb/update/renameCollection.js +28 -4
  71. package/dist/tools/mongodb/update/renameCollection.js.map +1 -1
  72. package/dist/tools/mongodb/update/updateMany.js +5 -7
  73. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  74. package/dist/tools/tool.js +39 -10
  75. package/dist/tools/tool.js.map +1 -1
  76. package/eslint.config.js +29 -10
  77. package/global.d.ts +1 -0
  78. package/package.json +6 -2
  79. package/scripts/apply.ts +9 -7
  80. package/scripts/filter.ts +3 -2
  81. package/src/common/atlas/apiClient.ts +48 -7
  82. package/src/config.ts +4 -10
  83. package/src/index.ts +10 -6
  84. package/src/logger.ts +66 -28
  85. package/src/packageInfo.ts +6 -0
  86. package/src/server.ts +103 -9
  87. package/src/session.ts +34 -17
  88. package/src/telemetry/constants.ts +15 -0
  89. package/src/telemetry/eventCache.ts +62 -0
  90. package/src/telemetry/telemetry.ts +137 -0
  91. package/src/telemetry/types.ts +60 -0
  92. package/src/tools/atlas/atlasTool.ts +7 -5
  93. package/src/tools/atlas/createAccessList.ts +0 -2
  94. package/src/tools/atlas/createDBUser.ts +0 -2
  95. package/src/tools/atlas/createFreeCluster.ts +0 -2
  96. package/src/tools/atlas/createProject.ts +0 -1
  97. package/src/tools/atlas/inspectAccessList.ts +0 -2
  98. package/src/tools/atlas/inspectCluster.ts +0 -2
  99. package/src/tools/atlas/listClusters.ts +0 -2
  100. package/src/tools/atlas/listDBUsers.ts +0 -2
  101. package/src/tools/atlas/listOrgs.ts +0 -2
  102. package/src/tools/atlas/listProjects.ts +12 -4
  103. package/src/tools/mongodb/metadata/collectionSchema.ts +16 -14
  104. package/src/tools/mongodb/metadata/collectionStorageSize.ts +41 -3
  105. package/src/tools/mongodb/metadata/connect.ts +5 -6
  106. package/src/tools/mongodb/metadata/dbStats.ts +6 -1
  107. package/src/tools/mongodb/metadata/explain.ts +20 -7
  108. package/src/tools/mongodb/metadata/logs.ts +55 -0
  109. package/src/tools/mongodb/mongodbTool.ts +12 -15
  110. package/src/tools/mongodb/read/aggregate.ts +3 -3
  111. package/src/tools/mongodb/read/collectionIndexes.ts +29 -5
  112. package/src/tools/mongodb/read/find.ts +3 -2
  113. package/src/tools/mongodb/tools.ts +2 -0
  114. package/src/tools/mongodb/update/renameCollection.ts +33 -4
  115. package/src/tools/mongodb/update/updateMany.ts +5 -7
  116. package/src/tools/tool.ts +51 -15
  117. package/tests/integration/helpers.ts +84 -107
  118. package/tests/integration/inMemoryTransport.ts +3 -2
  119. package/tests/integration/server.test.ts +47 -21
  120. package/tests/integration/tools/atlas/accessLists.test.ts +13 -15
  121. package/tests/integration/tools/atlas/atlasHelpers.ts +3 -8
  122. package/tests/integration/tools/atlas/clusters.test.ts +12 -13
  123. package/tests/integration/tools/atlas/dbUsers.test.ts +9 -9
  124. package/tests/integration/tools/atlas/orgs.test.ts +4 -4
  125. package/tests/integration/tools/atlas/projects.test.ts +10 -12
  126. package/tests/integration/tools/mongodb/create/createCollection.test.ts +19 -62
  127. package/tests/integration/tools/mongodb/create/createIndex.test.ts +41 -87
  128. package/tests/integration/tools/mongodb/create/insertMany.test.ts +35 -78
  129. package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +25 -62
  130. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +22 -71
  131. package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +29 -63
  132. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +154 -0
  133. package/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +86 -0
  134. package/tests/integration/tools/mongodb/metadata/connect.test.ts +33 -23
  135. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +104 -0
  136. package/tests/integration/tools/mongodb/metadata/explain.test.ts +171 -0
  137. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +28 -56
  138. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +32 -26
  139. package/tests/integration/tools/mongodb/metadata/logs.test.ts +83 -0
  140. package/tests/integration/tools/mongodb/mongodbHelpers.ts +165 -0
  141. package/tests/integration/tools/mongodb/read/aggregate.test.ts +99 -0
  142. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +99 -0
  143. package/tests/integration/tools/mongodb/read/count.test.ts +31 -79
  144. package/tests/integration/tools/mongodb/read/find.test.ts +182 -0
  145. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +194 -0
  146. package/tests/integration/tools/mongodb/update/updateMany.test.ts +238 -0
  147. package/tsconfig.jest.json +2 -1
  148. package/tsconfig.json +1 -1
  149. package/tsconfig.lint.json +8 -0
package/eslint.config.js CHANGED
@@ -2,18 +2,34 @@ import { defineConfig, globalIgnores } from "eslint/config";
2
2
  import js from "@eslint/js";
3
3
  import globals from "globals";
4
4
  import tseslint from "typescript-eslint";
5
- import eslintConfigPrettier from "eslint-config-prettier/flat";
5
+ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
6
+ import jestPlugin from "eslint-plugin-jest";
6
7
 
7
- const files = ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.test.ts", "eslint.config.js", "jest.config.js"];
8
+ const testFiles = ["tests/**/*.test.ts", "tests/**/*.ts"];
9
+
10
+ const files = [...testFiles, "src/**/*.ts", "scripts/**/*.ts"];
8
11
 
9
12
  export default defineConfig([
10
13
  { files, plugins: { js }, extends: ["js/recommended"] },
11
14
  { files, languageOptions: { globals: globals.node } },
15
+ {
16
+ files: testFiles,
17
+ plugins: {
18
+ jest: jestPlugin,
19
+ },
20
+ languageOptions: {
21
+ globals: {
22
+ ...globals.node,
23
+ ...jestPlugin.environments.globals.globals,
24
+ },
25
+ },
26
+ },
12
27
  tseslint.configs.recommendedTypeChecked,
13
28
  {
29
+ files,
14
30
  languageOptions: {
15
31
  parserOptions: {
16
- projectService: true,
32
+ project: "./tsconfig.lint.json",
17
33
  tsconfigRootDir: import.meta.dirname,
18
34
  },
19
35
  },
@@ -25,11 +41,14 @@ export default defineConfig([
25
41
  "@typescript-eslint/no-non-null-assertion": "error",
26
42
  },
27
43
  },
28
- // Ignore features specific to TypeScript resolved rules
29
- tseslint.config({
30
- // TODO: Configure tests and scripts to work with this.
31
- ignores: ["eslint.config.js", "jest.config.js", "tests/**/*.ts", "scripts/**/*.ts"],
32
- }),
33
- globalIgnores(["node_modules", "dist", "src/common/atlas/openapi.d.ts", "coverage"]),
34
- eslintConfigPrettier,
44
+ globalIgnores([
45
+ "node_modules",
46
+ "dist",
47
+ "src/common/atlas/openapi.d.ts",
48
+ "coverage",
49
+ "global.d.ts",
50
+ "eslint.config.js",
51
+ "jest.config.js",
52
+ ]),
53
+ eslintPluginPrettierRecommended,
35
54
  ]);
package/global.d.ts ADDED
@@ -0,0 +1 @@
1
+ import "jest-extended";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongodb-mcp-server",
3
3
  "description": "MongoDB Model Context Protocol Server",
4
- "version": "0.0.4",
4
+ "version": "0.0.5",
5
5
  "main": "dist/index.js",
6
6
  "author": "MongoDB <info@mongodb.com>",
7
7
  "homepage": "https://github.com/mongodb-js/mongodb-mcp-server",
@@ -41,12 +41,15 @@
41
41
  "@types/simple-oauth2": "^5.0.7",
42
42
  "@types/yargs-parser": "^21.0.3",
43
43
  "eslint": "^9.24.0",
44
- "eslint-config-prettier": "^10.1.1",
44
+ "eslint-config-prettier": "^10.1.2",
45
+ "eslint-plugin-jest": "^28.11.0",
46
+ "eslint-plugin-prettier": "^5.2.6",
45
47
  "globals": "^16.0.0",
46
48
  "jest": "^29.7.0",
47
49
  "jest-environment-node": "^29.7.0",
48
50
  "jest-extended": "^4.0.2",
49
51
  "mongodb-runner": "^5.8.2",
52
+ "native-machine-id": "^0.1.0",
50
53
  "openapi-types": "^12.1.3",
51
54
  "openapi-typescript": "^7.6.1",
52
55
  "prettier": "^3.5.3",
@@ -61,6 +64,7 @@
61
64
  "@mongodb-js/devtools-connect": "^3.7.2",
62
65
  "@mongosh/service-provider-node-driver": "^3.6.0",
63
66
  "bson": "^6.10.3",
67
+ "lru-cache": "^11.1.0",
64
68
  "mongodb": "^6.15.0",
65
69
  "mongodb-log-writer": "^2.4.1",
66
70
  "mongodb-redact": "^1.1.6",
package/scripts/apply.ts CHANGED
@@ -9,13 +9,15 @@ function findObjectFromRef<T>(obj: T | OpenAPIV3_1.ReferenceObject, openapi: Ope
9
9
  }
10
10
  const paramParts = ref.split("/");
11
11
  paramParts.shift(); // Remove the first part which is always '#'
12
- let foundObj: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any
12
+
13
+ let foundObj: Record<string, unknown> = openapi;
13
14
  while (true) {
14
15
  const part = paramParts.shift();
15
16
  if (!part) {
16
17
  break;
17
18
  }
18
- foundObj = foundObj[part];
19
+
20
+ foundObj = foundObj[part] as Record<string, unknown>;
19
21
  }
20
22
  return foundObj as T;
21
23
  }
@@ -28,7 +30,7 @@ async function main() {
28
30
  process.exit(1);
29
31
  }
30
32
 
31
- const specFile = (await fs.readFile(spec, "utf8")) as string;
33
+ const specFile = await fs.readFile(spec as string, "utf8");
32
34
 
33
35
  const operations: {
34
36
  path: string;
@@ -42,7 +44,7 @@ async function main() {
42
44
  const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document;
43
45
  for (const path in openapi.paths) {
44
46
  for (const method in openapi.paths[path]) {
45
- const operation: OpenAPIV3_1.OperationObject = openapi.paths[path][method];
47
+ const operation = openapi.paths[path][method] as OpenAPIV3_1.OperationObject;
46
48
 
47
49
  if (!operation.operationId || !operation.tags?.length) {
48
50
  continue;
@@ -101,9 +103,9 @@ async function main() {
101
103
  })
102
104
  .join("\n");
103
105
 
104
- const templateFile = (await fs.readFile(file, "utf8")) as string;
106
+ const templateFile = await fs.readFile(file as string, "utf8");
105
107
  const templateLines = templateFile.split("\n");
106
- let outputLines: string[] = [];
108
+ const outputLines: string[] = [];
107
109
  let addLines = true;
108
110
  for (const line of templateLines) {
109
111
  if (line.includes("DO NOT EDIT. This is auto-generated code.")) {
@@ -120,7 +122,7 @@ async function main() {
120
122
  }
121
123
  const output = outputLines.join("\n");
122
124
 
123
- await fs.writeFile(file, output, "utf8");
125
+ await fs.writeFile(file as string, output, "utf8");
124
126
  }
125
127
 
126
128
  main().catch((error) => {
package/scripts/filter.ts CHANGED
@@ -8,6 +8,7 @@ async function readStdin() {
8
8
  reject(err);
9
9
  });
10
10
  process.stdin.on("data", (chunk) => {
11
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
11
12
  data += chunk;
12
13
  });
13
14
  process.stdin.on("end", () => {
@@ -42,8 +43,8 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
42
43
  for (const path in openapi.paths) {
43
44
  const filteredMethods = {} as OpenAPIV3_1.PathItemObject;
44
45
  for (const method in openapi.paths[path]) {
45
- if (allowedOperations.includes(openapi.paths[path][method].operationId)) {
46
- filteredMethods[method] = openapi.paths[path][method];
46
+ if (allowedOperations.includes((openapi.paths[path][method] as { operationId: string }).operationId)) {
47
+ filteredMethods[method] = openapi.paths[path][method] as OpenAPIV3_1.OperationObject;
47
48
  }
48
49
  }
49
50
  if (Object.keys(filteredMethods).length > 0) {
@@ -1,16 +1,22 @@
1
- import config from "../../config.js";
2
- import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
1
+ import createClient, { Client, Middleware } from "openapi-fetch";
2
+ import type { FetchOptions } from "openapi-fetch";
3
3
  import { AccessToken, ClientCredentials } from "simple-oauth2";
4
4
  import { ApiClientError } from "./apiClientError.js";
5
5
  import { paths, operations } from "./openapi.js";
6
+ import { BaseEvent } from "../../telemetry/types.js";
7
+ import { mongoLogId } from "mongodb-log-writer";
8
+ import logger from "../../logger.js";
9
+ import { packageInfo } from "../../packageInfo.js";
6
10
 
7
11
  const ATLAS_API_VERSION = "2025-03-12";
8
12
 
13
+ export interface ApiClientCredentials {
14
+ clientId: string;
15
+ clientSecret: string;
16
+ }
17
+
9
18
  export interface ApiClientOptions {
10
- credentials?: {
11
- clientId: string;
12
- clientSecret: string;
13
- };
19
+ credentials?: ApiClientCredentials;
14
20
  baseUrl?: string;
15
21
  userAgent?: string;
16
22
  }
@@ -65,7 +71,7 @@ export class ApiClient {
65
71
  baseUrl: options?.baseUrl || "https://cloud.mongodb.com/",
66
72
  userAgent:
67
73
  options?.userAgent ||
68
- `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
74
+ `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
69
75
  };
70
76
 
71
77
  this.client = createClient<paths>({
@@ -91,6 +97,15 @@ export class ApiClient {
91
97
  this.client.use(this.errorMiddleware);
92
98
  }
93
99
 
100
+ public hasCredentials(): boolean {
101
+ logger.info(
102
+ mongoLogId(1_000_000),
103
+ "api-client",
104
+ `Checking if API client has credentials: ${!!(this.oauth2Client && this.accessToken)}`
105
+ );
106
+ return !!(this.oauth2Client && this.accessToken);
107
+ }
108
+
94
109
  public async getIpInfo(): Promise<{
95
110
  currentIpv4Address: string;
96
111
  }> {
@@ -116,6 +131,32 @@ export class ApiClient {
116
131
  }>;
117
132
  }
118
133
 
134
+ async sendEvents(events: BaseEvent[]): Promise<void> {
135
+ let endpoint = "api/private/unauth/telemetry/events";
136
+ const headers: Record<string, string> = {
137
+ Accept: "application/json",
138
+ "Content-Type": "application/json",
139
+ "User-Agent": this.options.userAgent,
140
+ };
141
+
142
+ const accessToken = await this.getAccessToken();
143
+ if (accessToken) {
144
+ endpoint = "api/private/v1.0/telemetry/events";
145
+ headers["Authorization"] = `Bearer ${accessToken}`;
146
+ }
147
+
148
+ const url = new URL(endpoint, this.options.baseUrl);
149
+ const response = await fetch(url, {
150
+ method: "POST",
151
+ headers,
152
+ body: JSON.stringify(events),
153
+ });
154
+
155
+ if (!response.ok) {
156
+ throw await ApiClientError.fromResponse(response);
157
+ }
158
+ }
159
+
119
160
  // DO NOT EDIT. This is auto-generated code.
120
161
  async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
121
162
  const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
package/src/config.ts CHANGED
@@ -2,15 +2,15 @@ import path from "path";
2
2
  import os from "os";
3
3
  import argv from "yargs-parser";
4
4
 
5
- import packageJson from "../package.json" with { type: "json" };
6
5
  import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb";
7
6
 
8
7
  // If we decide to support non-string config options, we'll need to extend the mechanism for parsing
9
8
  // env variables.
10
- interface UserConfig {
9
+ export interface UserConfig {
11
10
  apiBaseUrl?: string;
12
11
  apiClientId?: string;
13
12
  apiClientSecret?: string;
13
+ telemetry?: "enabled" | "disabled";
14
14
  logPath: string;
15
15
  connectionString?: string;
16
16
  connectOptions: {
@@ -31,21 +31,15 @@ const defaults: UserConfig = {
31
31
  timeoutMS: 30_000,
32
32
  },
33
33
  disabledTools: [],
34
+ telemetry: "disabled",
34
35
  };
35
36
 
36
- const mergedUserConfig = {
37
+ export const config = {
37
38
  ...defaults,
38
39
  ...getEnvConfig(),
39
40
  ...getCliConfig(),
40
41
  };
41
42
 
42
- const config = {
43
- ...mergedUserConfig,
44
- version: packageJson.version,
45
- };
46
-
47
- export default config;
48
-
49
43
  function getLogPath(): string {
50
44
  const localDataPath =
51
45
  process.platform === "win32"
package/src/index.ts CHANGED
@@ -4,20 +4,25 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import logger from "./logger.js";
5
5
  import { mongoLogId } from "mongodb-log-writer";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import config from "./config.js";
7
+ import { config } from "./config.js";
8
8
  import { Session } from "./session.js";
9
9
  import { Server } from "./server.js";
10
+ import { packageInfo } from "./packageInfo.js";
10
11
 
11
12
  try {
12
- const session = new Session();
13
+ const session = new Session({
14
+ apiBaseUrl: config.apiBaseUrl,
15
+ apiClientId: config.apiClientId,
16
+ apiClientSecret: config.apiClientSecret,
17
+ });
13
18
  const mcpServer = new McpServer({
14
- name: "MongoDB Atlas",
15
- version: config.version,
19
+ name: packageInfo.mcpServerName,
20
+ version: packageInfo.version,
16
21
  });
17
-
18
22
  const server = new Server({
19
23
  mcpServer,
20
24
  session,
25
+ userConfig: config,
21
26
  });
22
27
 
23
28
  const transport = new StdioServerTransport();
@@ -25,6 +30,5 @@ try {
25
30
  await server.connect(transport);
26
31
  } catch (error: unknown) {
27
32
  logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error as string}`);
28
-
29
33
  process.exit(1);
30
34
  }
package/src/logger.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import fs from "fs/promises";
2
2
  import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer";
3
- import config from "./config.js";
4
3
  import redact from "mongodb-redact";
5
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
5
  import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";
@@ -9,6 +8,7 @@ export type LogLevel = LoggingMessageNotification["params"]["level"];
9
8
 
10
9
  abstract class LoggerBase {
11
10
  abstract log(level: LogLevel, id: MongoLogId, context: string, message: string): void;
11
+
12
12
  info(id: MongoLogId, context: string, message: string): void {
13
13
  this.log("info", id, context, message);
14
14
  }
@@ -48,22 +48,35 @@ class ConsoleLogger extends LoggerBase {
48
48
  }
49
49
  }
50
50
 
51
- class Logger extends LoggerBase {
52
- constructor(
53
- private logWriter: MongoLogWriter,
54
- private server: McpServer
55
- ) {
51
+ class DiskLogger extends LoggerBase {
52
+ private constructor(private logWriter: MongoLogWriter) {
56
53
  super();
57
54
  }
58
55
 
56
+ static async fromPath(logPath: string): Promise<DiskLogger> {
57
+ await fs.mkdir(logPath, { recursive: true });
58
+
59
+ const manager = new MongoLogManager({
60
+ directory: logPath,
61
+ retentionDays: 30,
62
+ onwarn: console.warn,
63
+ onerror: console.error,
64
+ gzip: false,
65
+ retentionGB: 1,
66
+ });
67
+
68
+ await manager.cleanupOldLogFiles();
69
+
70
+ const logWriter = await manager.createLogWriter();
71
+
72
+ return new DiskLogger(logWriter);
73
+ }
74
+
59
75
  log(level: LogLevel, id: MongoLogId, context: string, message: string): void {
60
76
  message = redact(message);
61
77
  const mongoDBLevel = this.mapToMongoDBLogLevel(level);
78
+
62
79
  this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message);
63
- void this.server.server.sendLoggingMessage({
64
- level,
65
- data: `[${context}]: ${message}`,
66
- });
67
80
  }
68
81
 
69
82
  private mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
@@ -87,31 +100,56 @@ class Logger extends LoggerBase {
87
100
  }
88
101
  }
89
102
 
90
- class ProxyingLogger extends LoggerBase {
91
- private internalLogger: LoggerBase = new ConsoleLogger();
103
+ class McpLogger extends LoggerBase {
104
+ constructor(private server: McpServer) {
105
+ super();
106
+ }
107
+
108
+ log(level: LogLevel, _: MongoLogId, context: string, message: string): void {
109
+ void this.server.server.sendLoggingMessage({
110
+ level,
111
+ data: `[${context}]: ${message}`,
112
+ });
113
+ }
114
+ }
115
+
116
+ class CompositeLogger extends LoggerBase {
117
+ private loggers: LoggerBase[];
118
+
119
+ constructor(...loggers: LoggerBase[]) {
120
+ super();
121
+
122
+ if (loggers.length === 0) {
123
+ // default to ConsoleLogger
124
+ this.loggers = [new ConsoleLogger()];
125
+ return;
126
+ }
127
+
128
+ this.loggers = [...loggers];
129
+ }
130
+
131
+ setLoggers(...loggers: LoggerBase[]): void {
132
+ if (loggers.length === 0) {
133
+ throw new Error("At least one logger must be provided");
134
+ }
135
+ this.loggers = [...loggers];
136
+ }
92
137
 
93
138
  log(level: LogLevel, id: MongoLogId, context: string, message: string): void {
94
- this.internalLogger.log(level, id, context, message);
139
+ for (const logger of this.loggers) {
140
+ logger.log(level, id, context, message);
141
+ }
95
142
  }
96
143
  }
97
144
 
98
- const logger = new ProxyingLogger();
145
+ const logger = new CompositeLogger();
99
146
  export default logger;
100
147
 
101
- export async function initializeLogger(server: McpServer): Promise<void> {
102
- await fs.mkdir(config.logPath, { recursive: true });
103
-
104
- const manager = new MongoLogManager({
105
- directory: config.logPath,
106
- retentionDays: 30,
107
- onwarn: console.warn,
108
- onerror: console.error,
109
- gzip: false,
110
- retentionGB: 1,
111
- });
148
+ export async function initializeLogger(server: McpServer, logPath: string): Promise<LoggerBase> {
149
+ const diskLogger = await DiskLogger.fromPath(logPath);
150
+ const mcpLogger = new McpLogger(server);
112
151
 
113
- await manager.cleanupOldLogFiles();
152
+ logger.setLoggers(mcpLogger, diskLogger);
114
153
 
115
- const logWriter = await manager.createLogWriter();
116
- logger["internalLogger"] = new Logger(logWriter, server);
154
+ return logger;
117
155
  }
@@ -0,0 +1,6 @@
1
+ import packageJson from "../package.json" with { type: "json" };
2
+
3
+ export const packageInfo = {
4
+ version: packageJson.version,
5
+ mcpServerName: "MongoDB MCP Server",
6
+ };
package/src/server.ts CHANGED
@@ -5,28 +5,89 @@ import { AtlasTools } from "./tools/atlas/tools.js";
5
5
  import { MongoDbTools } from "./tools/mongodb/tools.js";
6
6
  import logger, { initializeLogger } from "./logger.js";
7
7
  import { mongoLogId } from "mongodb-log-writer";
8
- import config from "./config.js";
8
+ import { ObjectId } from "mongodb";
9
+ import { Telemetry } from "./telemetry/telemetry.js";
10
+ import { UserConfig } from "./config.js";
11
+ import { type ServerEvent } from "./telemetry/types.js";
12
+ import { type ServerCommand } from "./telemetry/types.js";
13
+ import { CallToolRequestSchema, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
14
+ import assert from "assert";
15
+
16
+ export interface ServerOptions {
17
+ session: Session;
18
+ userConfig: UserConfig;
19
+ mcpServer: McpServer;
20
+ }
9
21
 
10
22
  export class Server {
11
23
  public readonly session: Session;
12
24
  private readonly mcpServer: McpServer;
25
+ private readonly telemetry: Telemetry;
26
+ private readonly userConfig: UserConfig;
27
+ private readonly startTime: number;
13
28
 
14
- constructor({ mcpServer, session }: { mcpServer: McpServer; session: Session }) {
15
- this.mcpServer = mcpServer;
29
+ constructor({ session, mcpServer, userConfig }: ServerOptions) {
30
+ this.startTime = Date.now();
16
31
  this.session = session;
32
+ this.telemetry = new Telemetry(session);
33
+ this.mcpServer = mcpServer;
34
+ this.userConfig = userConfig;
17
35
  }
18
36
 
19
37
  async connect(transport: Transport) {
20
38
  this.mcpServer.server.registerCapabilities({ logging: {} });
21
-
22
39
  this.registerTools();
23
40
  this.registerResources();
24
41
 
25
- await initializeLogger(this.mcpServer);
42
+ // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
43
+ // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
44
+ // the tool accepts any arguments, even if they're all optional.
45
+ //
46
+ // see: https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/server/mcp.ts#L705
47
+ // Since paramsSchema here is not undefined, the server will create a non-optional z.object from it.
48
+ const existingHandler = (
49
+ this.mcpServer.server["_requestHandlers"] as Map<
50
+ string,
51
+ (request: unknown, extra: unknown) => Promise<CallToolResult>
52
+ >
53
+ ).get(CallToolRequestSchema.shape.method.value);
54
+
55
+ assert(existingHandler, "No existing handler found for CallToolRequestSchema");
56
+
57
+ this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
58
+ if (!request.params.arguments) {
59
+ request.params.arguments = {};
60
+ }
61
+
62
+ return existingHandler(request, extra);
63
+ });
64
+
65
+ await initializeLogger(this.mcpServer, this.userConfig.logPath);
26
66
 
27
67
  await this.mcpServer.connect(transport);
28
68
 
29
- logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`);
69
+ this.mcpServer.server.oninitialized = () => {
70
+ this.session.setAgentRunner(this.mcpServer.server.getClientVersion());
71
+ this.session.sessionId = new ObjectId().toString();
72
+
73
+ logger.info(
74
+ mongoLogId(1_000_004),
75
+ "server",
76
+ `Server started with transport ${transport.constructor.name} and agent runner ${this.session.agentRunner?.name}`
77
+ );
78
+
79
+ this.emitServerEvent("start", Date.now() - this.startTime);
80
+ };
81
+
82
+ this.mcpServer.server.onclose = () => {
83
+ const closeTime = Date.now();
84
+ this.emitServerEvent("stop", Date.now() - closeTime);
85
+ };
86
+
87
+ this.mcpServer.server.onerror = (error: Error) => {
88
+ const closeTime = Date.now();
89
+ this.emitServerEvent("stop", Date.now() - closeTime, error);
90
+ };
30
91
  }
31
92
 
32
93
  async close(): Promise<void> {
@@ -34,14 +95,47 @@ export class Server {
34
95
  await this.mcpServer.close();
35
96
  }
36
97
 
98
+ /**
99
+ * Emits a server event
100
+ * @param command - The server command (e.g., "start", "stop", "register", "deregister")
101
+ * @param additionalProperties - Additional properties specific to the event
102
+ */
103
+ emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error) {
104
+ const event: ServerEvent = {
105
+ timestamp: new Date().toISOString(),
106
+ source: "mdbmcp",
107
+ properties: {
108
+ ...this.telemetry.getCommonProperties(),
109
+ result: "success",
110
+ duration_ms: commandDuration,
111
+ component: "server",
112
+ category: "other",
113
+ command: command,
114
+ },
115
+ };
116
+
117
+ if (command === "start") {
118
+ event.properties.startup_time_ms = commandDuration;
119
+ }
120
+ if (command === "stop") {
121
+ event.properties.runtime_duration_ms = Date.now() - this.startTime;
122
+ if (error) {
123
+ event.properties.result = "failure";
124
+ event.properties.reason = error.message;
125
+ }
126
+ }
127
+
128
+ this.telemetry.emitEvents([event]).catch(() => {});
129
+ }
130
+
37
131
  private registerTools() {
38
132
  for (const tool of [...AtlasTools, ...MongoDbTools]) {
39
- new tool(this.session).register(this.mcpServer);
133
+ new tool(this.session, this.userConfig, this.telemetry).register(this.mcpServer);
40
134
  }
41
135
  }
42
136
 
43
137
  private registerResources() {
44
- if (config.connectionString) {
138
+ if (this.userConfig.connectionString) {
45
139
  this.mcpServer.resource(
46
140
  "connection-string",
47
141
  "config://connection-string",
@@ -52,7 +146,7 @@ export class Server {
52
146
  return {
53
147
  contents: [
54
148
  {
55
- text: `Preconfigured connection string: ${config.connectionString}`,
149
+ text: `Preconfigured connection string: ${this.userConfig.connectionString}`,
56
150
  uri: uri.href,
57
151
  },
58
152
  ],