mongodb-mcp-server 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +11 -0
- package/.github/CODEOWNERS +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
- package/.github/workflows/check-pr-title.yml +29 -0
- package/.github/workflows/{lint.yml → check.yml} +22 -1
- package/.github/workflows/code_health.yaml +0 -22
- package/.github/workflows/code_health_fork.yaml +7 -63
- package/.github/workflows/docker.yaml +57 -0
- package/.github/workflows/stale.yml +32 -0
- package/.smithery/Dockerfile +30 -0
- package/.smithery/smithery.yaml +63 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +11 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +10 -0
- package/README.md +173 -35
- package/dist/common/atlas/apiClient.js +151 -35
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/common/atlas/apiClientError.js +38 -5
- package/dist/common/atlas/apiClientError.js.map +1 -1
- package/dist/common/atlas/cluster.js +66 -0
- package/dist/common/atlas/cluster.js.map +1 -0
- package/dist/common/atlas/generatePassword.js +9 -0
- package/dist/common/atlas/generatePassword.js.map +1 -0
- package/dist/helpers/EJsonTransport.js +38 -0
- package/dist/helpers/EJsonTransport.js.map +1 -0
- package/dist/helpers/connectionOptions.js +10 -0
- package/dist/helpers/connectionOptions.js.map +1 -0
- package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
- package/dist/helpers/packageInfo.js.map +1 -0
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.js +7 -0
- package/dist/logger.js.map +1 -1
- package/dist/server.js +16 -12
- package/dist/server.js.map +1 -1
- package/dist/session.js +8 -3
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +1 -3
- package/dist/telemetry/constants.js.map +1 -1
- package/dist/telemetry/eventCache.js.map +1 -1
- package/dist/telemetry/telemetry.js +126 -47
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/atlas/atlasTool.js +38 -0
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/create/createDBUser.js +19 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -1
- package/dist/tools/atlas/create/createProject.js +5 -1
- package/dist/tools/atlas/create/createProject.js.map +1 -1
- package/dist/tools/atlas/metadata/connectCluster.js +5 -22
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
- package/dist/tools/atlas/read/inspectCluster.js +4 -24
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
- package/dist/tools/atlas/read/listAlerts.js +41 -0
- package/dist/tools/atlas/read/listAlerts.js.map +1 -0
- package/dist/tools/atlas/read/listClusters.js +9 -18
- package/dist/tools/atlas/read/listClusters.js.map +1 -1
- package/dist/tools/atlas/read/listProjects.js +3 -1
- package/dist/tools/atlas/read/listProjects.js.map +1 -1
- package/dist/tools/atlas/tools.js +2 -0
- package/dist/tools/atlas/tools.js.map +1 -1
- package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +2 -2
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/mongodb/tools.js +2 -4
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/dist/tools/tool.js +38 -6
- package/dist/tools/tool.js.map +1 -1
- package/eslint.config.js +2 -1
- package/{jest.config.ts → jest.config.cjs} +1 -1
- package/package.json +11 -9
- package/scripts/apply.ts +8 -5
- package/scripts/filter.ts +5 -0
- package/src/common/atlas/apiClient.ts +190 -38
- package/src/common/atlas/apiClientError.ts +58 -7
- package/src/common/atlas/cluster.ts +94 -0
- package/src/common/atlas/generatePassword.ts +10 -0
- package/src/common/atlas/openapi.d.ts +1876 -239
- package/src/helpers/EJsonTransport.ts +47 -0
- package/src/helpers/connectionOptions.ts +20 -0
- package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
- package/src/index.ts +27 -3
- package/src/logger.ts +8 -0
- package/src/server.ts +23 -15
- package/src/session.ts +8 -4
- package/src/telemetry/constants.ts +2 -3
- package/src/telemetry/eventCache.ts +1 -1
- package/src/telemetry/telemetry.ts +182 -64
- package/src/telemetry/types.ts +1 -1
- package/src/tools/atlas/atlasTool.ts +47 -1
- package/src/tools/atlas/create/createDBUser.ts +22 -2
- package/src/tools/atlas/create/createProject.ts +7 -1
- package/src/tools/atlas/metadata/connectCluster.ts +5 -27
- package/src/tools/atlas/read/inspectCluster.ts +4 -40
- package/src/tools/atlas/read/listAlerts.ts +45 -0
- package/src/tools/atlas/read/listClusters.ts +19 -36
- package/src/tools/atlas/read/listProjects.ts +4 -2
- package/src/tools/atlas/tools.ts +2 -0
- package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
- package/src/tools/mongodb/read/count.ts +3 -2
- package/src/tools/mongodb/tools.ts +2 -4
- package/src/tools/tool.ts +45 -8
- package/src/types/mongodb-connection-string-url.d.ts +69 -0
- package/tests/integration/helpers.ts +41 -2
- package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
- package/tests/integration/tools/atlas/alerts.test.ts +42 -0
- package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
- package/tests/integration/tools/atlas/clusters.test.ts +4 -4
- package/tests/integration/tools/atlas/dbUsers.test.ts +58 -33
- package/tests/integration/tools/atlas/orgs.test.ts +2 -2
- package/tests/integration/tools/atlas/projects.test.ts +3 -3
- package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
- package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
- package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
- package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
- package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
- package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
- package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
- package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
- package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
- package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
- package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
- package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
- package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
- package/tests/integration/tools/mongodb/read/find.test.ts +32 -4
- package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
- package/tests/unit/EJsonTransport.test.ts +71 -0
- package/tests/unit/apiClient.test.ts +193 -0
- package/tests/unit/session.test.ts +65 -0
- package/tests/unit/telemetry.test.ts +222 -80
- package/tsconfig.build.json +2 -1
- package/dist/packageInfo.js.map +0 -1
- package/dist/telemetry/device-id.js +0 -20
- package/dist/telemetry/device-id.js.map +0 -1
- package/src/telemetry/device-id.ts +0 -21
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { EJSON } from "bson";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
// This is almost a copy of ReadBuffer from @modelcontextprotocol/sdk
|
|
6
|
+
// but it uses EJSON.parse instead of JSON.parse to handle BSON types
|
|
7
|
+
export class EJsonReadBuffer {
|
|
8
|
+
private _buffer?: Buffer;
|
|
9
|
+
|
|
10
|
+
append(chunk: Buffer): void {
|
|
11
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
readMessage(): JSONRPCMessage | null {
|
|
15
|
+
if (!this._buffer) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const index = this._buffer.indexOf("\n");
|
|
20
|
+
if (index === -1) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
|
|
25
|
+
this._buffer = this._buffer.subarray(index + 1);
|
|
26
|
+
|
|
27
|
+
// This is using EJSON.parse instead of JSON.parse to handle BSON types
|
|
28
|
+
return JSONRPCMessageSchema.parse(EJSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
clear(): void {
|
|
32
|
+
this._buffer = undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// This is a hacky workaround for https://github.com/mongodb-js/mongodb-mcp-server/issues/211
|
|
37
|
+
// The underlying issue is that StdioServerTransport uses JSON.parse to deserialize
|
|
38
|
+
// messages, but that doesn't handle bson types, such as ObjectId when serialized as EJSON.
|
|
39
|
+
//
|
|
40
|
+
// This function creates a StdioServerTransport and replaces the internal readBuffer with EJsonReadBuffer
|
|
41
|
+
// that uses EJson.parse instead.
|
|
42
|
+
export function createEJsonTransport(): StdioServerTransport {
|
|
43
|
+
const server = new StdioServerTransport();
|
|
44
|
+
server["_readBuffer"] = new EJsonReadBuffer();
|
|
45
|
+
|
|
46
|
+
return server;
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MongoClientOptions } from "mongodb";
|
|
2
|
+
import ConnectionString from "mongodb-connection-string-url";
|
|
3
|
+
|
|
4
|
+
export function setAppNameParamIfMissing({
|
|
5
|
+
connectionString,
|
|
6
|
+
defaultAppName,
|
|
7
|
+
}: {
|
|
8
|
+
connectionString: string;
|
|
9
|
+
defaultAppName?: string;
|
|
10
|
+
}): string {
|
|
11
|
+
const connectionStringUrl = new ConnectionString(connectionString);
|
|
12
|
+
|
|
13
|
+
const searchParams = connectionStringUrl.typedSearchParams<MongoClientOptions>();
|
|
14
|
+
|
|
15
|
+
if (!searchParams.has("appName") && defaultAppName !== undefined) {
|
|
16
|
+
searchParams.set("appName", defaultAppName);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return connectionStringUrl.toString();
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
3
|
import logger, { LogId } from "./logger.js";
|
|
5
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
5
|
import { config } from "./config.js";
|
|
7
6
|
import { Session } from "./session.js";
|
|
8
7
|
import { Server } from "./server.js";
|
|
9
|
-
import { packageInfo } from "./packageInfo.js";
|
|
8
|
+
import { packageInfo } from "./helpers/packageInfo.js";
|
|
9
|
+
import { Telemetry } from "./telemetry/telemetry.js";
|
|
10
|
+
import { createEJsonTransport } from "./helpers/EJsonTransport.js";
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
const session = new Session({
|
|
@@ -19,13 +20,36 @@ try {
|
|
|
19
20
|
version: packageInfo.version,
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
const telemetry = Telemetry.create(session, config);
|
|
24
|
+
|
|
22
25
|
const server = new Server({
|
|
23
26
|
mcpServer,
|
|
24
27
|
session,
|
|
28
|
+
telemetry,
|
|
25
29
|
userConfig: config,
|
|
26
30
|
});
|
|
27
31
|
|
|
28
|
-
const transport =
|
|
32
|
+
const transport = createEJsonTransport();
|
|
33
|
+
|
|
34
|
+
const shutdown = () => {
|
|
35
|
+
logger.info(LogId.serverCloseRequested, "server", `Server close requested`);
|
|
36
|
+
|
|
37
|
+
server
|
|
38
|
+
.close()
|
|
39
|
+
.then(() => {
|
|
40
|
+
logger.info(LogId.serverClosed, "server", `Server closed successfully`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
})
|
|
43
|
+
.catch((err: unknown) => {
|
|
44
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
45
|
+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
process.once("SIGINT", shutdown);
|
|
51
|
+
process.once("SIGTERM", shutdown);
|
|
52
|
+
process.once("SIGQUIT", shutdown);
|
|
29
53
|
|
|
30
54
|
await server.connect(transport);
|
|
31
55
|
} catch (error: unknown) {
|
package/src/logger.ts
CHANGED
|
@@ -9,10 +9,14 @@ export type LogLevel = LoggingMessageNotification["params"]["level"];
|
|
|
9
9
|
export const LogId = {
|
|
10
10
|
serverStartFailure: mongoLogId(1_000_001),
|
|
11
11
|
serverInitialized: mongoLogId(1_000_002),
|
|
12
|
+
serverCloseRequested: mongoLogId(1_000_003),
|
|
13
|
+
serverClosed: mongoLogId(1_000_004),
|
|
14
|
+
serverCloseFailure: mongoLogId(1_000_005),
|
|
12
15
|
|
|
13
16
|
atlasCheckCredentials: mongoLogId(1_001_001),
|
|
14
17
|
atlasDeleteDatabaseUserFailure: mongoLogId(1_001_002),
|
|
15
18
|
atlasConnectFailure: mongoLogId(1_001_003),
|
|
19
|
+
atlasInspectFailure: mongoLogId(1_001_004),
|
|
16
20
|
|
|
17
21
|
telemetryDisabled: mongoLogId(1_002_001),
|
|
18
22
|
telemetryEmitFailure: mongoLogId(1_002_002),
|
|
@@ -20,6 +24,8 @@ export const LogId = {
|
|
|
20
24
|
telemetryEmitSuccess: mongoLogId(1_002_004),
|
|
21
25
|
telemetryMetadataError: mongoLogId(1_002_005),
|
|
22
26
|
telemetryDeviceIdFailure: mongoLogId(1_002_006),
|
|
27
|
+
telemetryDeviceIdTimeout: mongoLogId(1_002_007),
|
|
28
|
+
telemetryContainerEnvFailure: mongoLogId(1_002_008),
|
|
23
29
|
|
|
24
30
|
toolExecute: mongoLogId(1_003_001),
|
|
25
31
|
toolExecuteFailure: mongoLogId(1_003_002),
|
|
@@ -27,6 +33,8 @@ export const LogId = {
|
|
|
27
33
|
|
|
28
34
|
mongodbConnectFailure: mongoLogId(1_004_001),
|
|
29
35
|
mongodbDisconnectFailure: mongoLogId(1_004_002),
|
|
36
|
+
|
|
37
|
+
toolUpdateFailure: mongoLogId(1_005_001),
|
|
30
38
|
} as const;
|
|
31
39
|
|
|
32
40
|
abstract class LoggerBase {
|
package/src/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ServerOptions {
|
|
|
16
16
|
session: Session;
|
|
17
17
|
userConfig: UserConfig;
|
|
18
18
|
mcpServer: McpServer;
|
|
19
|
+
telemetry: Telemetry;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export class Server {
|
|
@@ -25,10 +26,10 @@ export class Server {
|
|
|
25
26
|
public readonly userConfig: UserConfig;
|
|
26
27
|
private readonly startTime: number;
|
|
27
28
|
|
|
28
|
-
constructor({ session, mcpServer, userConfig }: ServerOptions) {
|
|
29
|
+
constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) {
|
|
29
30
|
this.startTime = Date.now();
|
|
30
31
|
this.session = session;
|
|
31
|
-
this.telemetry =
|
|
32
|
+
this.telemetry = telemetry;
|
|
32
33
|
this.mcpServer = mcpServer;
|
|
33
34
|
this.userConfig = userConfig;
|
|
34
35
|
}
|
|
@@ -93,6 +94,7 @@ export class Server {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
async close(): Promise<void> {
|
|
97
|
+
await this.telemetry.close();
|
|
96
98
|
await this.session.close();
|
|
97
99
|
await this.mcpServer.close();
|
|
98
100
|
}
|
|
@@ -102,7 +104,7 @@ export class Server {
|
|
|
102
104
|
* @param command - The server command (e.g., "start", "stop", "register", "deregister")
|
|
103
105
|
* @param additionalProperties - Additional properties specific to the event
|
|
104
106
|
*/
|
|
105
|
-
emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error) {
|
|
107
|
+
private emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error) {
|
|
106
108
|
const event: ServerEvent = {
|
|
107
109
|
timestamp: new Date().toISOString(),
|
|
108
110
|
source: "mdbmcp",
|
|
@@ -128,7 +130,7 @@ export class Server {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
this.telemetry.emitEvents([event])
|
|
133
|
+
this.telemetry.emitEvents([event]);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
private registerTools() {
|
|
@@ -172,17 +174,6 @@ export class Server {
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
private async validateConfig(): Promise<void> {
|
|
175
|
-
const isAtlasConfigured = this.userConfig.apiClientId && this.userConfig.apiClientSecret;
|
|
176
|
-
const isMongoDbConfigured = this.userConfig.connectionString;
|
|
177
|
-
if (!isAtlasConfigured && !isMongoDbConfigured) {
|
|
178
|
-
console.error(
|
|
179
|
-
"Either Atlas Client Id or a MongoDB connection string must be configured - you can provide them as environment variables or as startup arguments. \n" +
|
|
180
|
-
"Provide the Atlas credentials as `MDB_MCP_API_CLIENT_ID` and `MDB_MCP_API_CLIENT_SECRET` environment variables or as `--apiClientId` and `--apiClientSecret` startup arguments. \n" +
|
|
181
|
-
"Provide the MongoDB connection string as `MDB_MCP_CONNECTION_STRING` environment variable or as `--connectionString` startup argument."
|
|
182
|
-
);
|
|
183
|
-
throw new Error("Either Atlas Client Id or a MongoDB connection string must be configured");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
177
|
if (this.userConfig.connectionString) {
|
|
187
178
|
try {
|
|
188
179
|
await this.session.connectToMongoDB(this.userConfig.connectionString, this.userConfig.connectOptions);
|
|
@@ -194,5 +185,22 @@ export class Server {
|
|
|
194
185
|
throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
|
|
195
186
|
}
|
|
196
187
|
}
|
|
188
|
+
|
|
189
|
+
if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
|
|
190
|
+
try {
|
|
191
|
+
await this.session.apiClient.validateAccessToken();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (this.userConfig.connectionString === undefined) {
|
|
194
|
+
console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
|
|
195
|
+
|
|
196
|
+
throw new Error(
|
|
197
|
+
"Failed to connect to MongoDB Atlas instance using the credentials from the config"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
console.error(
|
|
201
|
+
"Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
197
205
|
}
|
|
198
206
|
}
|
package/src/session.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Implementation } from "@modelcontextprotocol/sdk/types.js";
|
|
|
4
4
|
import logger, { LogId } from "./logger.js";
|
|
5
5
|
import EventEmitter from "events";
|
|
6
6
|
import { ConnectOptions } from "./config.js";
|
|
7
|
+
import { setAppNameParamIfMissing } from "./helpers/connectionOptions.js";
|
|
8
|
+
import { packageInfo } from "./helpers/packageInfo.js";
|
|
7
9
|
|
|
8
10
|
export interface SessionOptions {
|
|
9
11
|
apiBaseUrl: string;
|
|
@@ -98,8 +100,12 @@ export class Session extends EventEmitter<{
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
async connectToMongoDB(connectionString: string, connectOptions: ConnectOptions): Promise<void> {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
connectionString = setAppNameParamIfMissing({
|
|
104
|
+
connectionString,
|
|
105
|
+
defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
|
|
106
|
+
});
|
|
107
|
+
this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, {
|
|
108
|
+
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
|
|
103
109
|
productName: "MongoDB MCP",
|
|
104
110
|
readConcern: {
|
|
105
111
|
level: connectOptions.readConcern,
|
|
@@ -110,7 +116,5 @@ export class Session extends EventEmitter<{
|
|
|
110
116
|
},
|
|
111
117
|
timeoutMS: connectOptions.timeoutMS,
|
|
112
118
|
});
|
|
113
|
-
|
|
114
|
-
this.serviceProvider = provider;
|
|
115
119
|
}
|
|
116
120
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { packageInfo } from "../packageInfo.js";
|
|
1
|
+
import { packageInfo } from "../helpers/packageInfo.js";
|
|
2
2
|
import { type CommonStaticProperties } from "./types.js";
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
/**
|
|
5
5
|
* Machine-specific metadata formatted for telemetry
|
|
6
6
|
*/
|
|
7
7
|
export const MACHINE_METADATA: CommonStaticProperties = {
|
|
8
|
-
device_id: getDeviceId(),
|
|
9
8
|
mcp_server_version: packageInfo.version,
|
|
10
9
|
mcp_server_name: packageInfo.mcpServerName,
|
|
11
10
|
platform: process.platform,
|
|
@@ -5,55 +5,154 @@ import logger, { LogId } from "../logger.js";
|
|
|
5
5
|
import { ApiClient } from "../common/atlas/apiClient.js";
|
|
6
6
|
import { MACHINE_METADATA } from "./constants.js";
|
|
7
7
|
import { EventCache } from "./eventCache.js";
|
|
8
|
+
import nodeMachineId from "node-machine-id";
|
|
9
|
+
import { getDeviceId } from "@mongodb-js/device-id";
|
|
10
|
+
import fs from "fs/promises";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(filePath, fs.constants.F_OK);
|
|
15
|
+
return true; // File exists
|
|
16
|
+
} catch (e: unknown) {
|
|
17
|
+
if (
|
|
18
|
+
e instanceof Error &&
|
|
19
|
+
(
|
|
20
|
+
e as Error & {
|
|
21
|
+
code: string;
|
|
22
|
+
}
|
|
23
|
+
).code === "ENOENT"
|
|
24
|
+
) {
|
|
25
|
+
return false; // File does not exist
|
|
26
|
+
}
|
|
27
|
+
throw e; // Re-throw unexpected errors
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function isContainerized(): Promise<boolean> {
|
|
32
|
+
if (process.env.container) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const exists = await Promise.all(["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(fileExists));
|
|
37
|
+
|
|
38
|
+
return exists.includes(true);
|
|
39
|
+
}
|
|
13
40
|
|
|
14
41
|
export class Telemetry {
|
|
15
|
-
private
|
|
42
|
+
private deviceIdAbortController = new AbortController();
|
|
43
|
+
private eventCache: EventCache;
|
|
44
|
+
private getRawMachineId: () => Promise<string>;
|
|
45
|
+
private getContainerEnv: () => Promise<boolean>;
|
|
46
|
+
private cachedCommonProperties?: CommonProperties;
|
|
47
|
+
private flushing: boolean = false;
|
|
16
48
|
|
|
17
|
-
constructor(
|
|
49
|
+
private constructor(
|
|
18
50
|
private readonly session: Session,
|
|
19
51
|
private readonly userConfig: UserConfig,
|
|
20
|
-
|
|
52
|
+
{
|
|
53
|
+
eventCache,
|
|
54
|
+
getRawMachineId,
|
|
55
|
+
getContainerEnv,
|
|
56
|
+
}: {
|
|
57
|
+
eventCache: EventCache;
|
|
58
|
+
getRawMachineId: () => Promise<string>;
|
|
59
|
+
getContainerEnv: () => Promise<boolean>;
|
|
60
|
+
}
|
|
21
61
|
) {
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
62
|
+
this.eventCache = eventCache;
|
|
63
|
+
this.getRawMachineId = getRawMachineId;
|
|
64
|
+
this.getContainerEnv = getContainerEnv;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static create(
|
|
68
|
+
session: Session,
|
|
69
|
+
userConfig: UserConfig,
|
|
70
|
+
{
|
|
71
|
+
eventCache = EventCache.getInstance(),
|
|
72
|
+
getRawMachineId = () => nodeMachineId.machineId(true),
|
|
73
|
+
getContainerEnv = isContainerized,
|
|
74
|
+
}: {
|
|
75
|
+
eventCache?: EventCache;
|
|
76
|
+
getRawMachineId?: () => Promise<string>;
|
|
77
|
+
getContainerEnv?: () => Promise<boolean>;
|
|
78
|
+
} = {}
|
|
79
|
+
): Telemetry {
|
|
80
|
+
const instance = new Telemetry(session, userConfig, {
|
|
81
|
+
eventCache,
|
|
82
|
+
getRawMachineId,
|
|
83
|
+
getContainerEnv,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return instance;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async close(): Promise<void> {
|
|
90
|
+
this.deviceIdAbortController.abort();
|
|
91
|
+
await this.flush();
|
|
25
92
|
}
|
|
26
93
|
|
|
27
94
|
/**
|
|
28
95
|
* Emits events through the telemetry pipeline
|
|
29
96
|
* @param events - The events to emit
|
|
30
97
|
*/
|
|
31
|
-
public
|
|
32
|
-
|
|
33
|
-
if (!this.isTelemetryEnabled()) {
|
|
34
|
-
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await this.emit(events);
|
|
39
|
-
} catch {
|
|
40
|
-
logger.debug(LogId.telemetryEmitFailure, "telemetry", `Error emitting telemetry events.`);
|
|
41
|
-
}
|
|
98
|
+
public emitEvents(events: BaseEvent[]): void {
|
|
99
|
+
void this.flush(events);
|
|
42
100
|
}
|
|
43
101
|
|
|
44
102
|
/**
|
|
45
103
|
* Gets the common properties for events
|
|
46
104
|
* @returns Object containing common properties for all events
|
|
47
105
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
106
|
+
private async getCommonProperties(): Promise<CommonProperties> {
|
|
107
|
+
if (!this.cachedCommonProperties) {
|
|
108
|
+
let deviceId: string | undefined;
|
|
109
|
+
let containerEnv: boolean | undefined;
|
|
110
|
+
try {
|
|
111
|
+
await Promise.all([
|
|
112
|
+
getDeviceId({
|
|
113
|
+
getMachineId: () => this.getRawMachineId(),
|
|
114
|
+
onError: (reason, error) => {
|
|
115
|
+
switch (reason) {
|
|
116
|
+
case "resolutionError":
|
|
117
|
+
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
|
|
118
|
+
break;
|
|
119
|
+
case "timeout":
|
|
120
|
+
logger.debug(
|
|
121
|
+
LogId.telemetryDeviceIdTimeout,
|
|
122
|
+
"telemetry",
|
|
123
|
+
"Device ID retrieval timed out"
|
|
124
|
+
);
|
|
125
|
+
break;
|
|
126
|
+
case "abort":
|
|
127
|
+
// No need to log in the case of aborts
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
abortSignal: this.deviceIdAbortController.signal,
|
|
132
|
+
}).then((id) => {
|
|
133
|
+
deviceId = id;
|
|
134
|
+
}),
|
|
135
|
+
this.getContainerEnv().then((env) => {
|
|
136
|
+
containerEnv = env;
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
} catch (error: unknown) {
|
|
140
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
141
|
+
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", err.message);
|
|
142
|
+
}
|
|
143
|
+
this.cachedCommonProperties = {
|
|
144
|
+
...MACHINE_METADATA,
|
|
145
|
+
mcp_client_version: this.session.agentRunner?.version,
|
|
146
|
+
mcp_client_name: this.session.agentRunner?.name,
|
|
147
|
+
session_id: this.session.sessionId,
|
|
148
|
+
config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
|
|
149
|
+
config_connection_string: this.userConfig.connectionString ? "true" : "false",
|
|
150
|
+
is_container_env: containerEnv ? "true" : "false",
|
|
151
|
+
device_id: deviceId,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.cachedCommonProperties;
|
|
57
156
|
}
|
|
58
157
|
|
|
59
158
|
/**
|
|
@@ -74,55 +173,74 @@ export class Telemetry {
|
|
|
74
173
|
}
|
|
75
174
|
|
|
76
175
|
/**
|
|
77
|
-
* Attempts to
|
|
176
|
+
* Attempts to flush events through authenticated and unauthenticated clients
|
|
78
177
|
* Falls back to caching if both attempts fail
|
|
79
178
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
179
|
+
public async flush(events?: BaseEvent[]): Promise<void> {
|
|
180
|
+
if (!this.isTelemetryEnabled()) {
|
|
181
|
+
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (this.flushing) {
|
|
186
|
+
this.eventCache.appendEvents(events ?? []);
|
|
187
|
+
process.nextTick(async () => {
|
|
188
|
+
// try again if in the middle of a flush
|
|
189
|
+
await this.flush();
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.flushing = true;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const cachedEvents = this.eventCache.getEvents();
|
|
198
|
+
const allEvents = [...cachedEvents, ...(events ?? [])];
|
|
199
|
+
if (allEvents.length <= 0) {
|
|
200
|
+
this.flushing = false;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.debug(
|
|
205
|
+
LogId.telemetryEmitStart,
|
|
206
|
+
"telemetry",
|
|
207
|
+
`Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
|
|
208
|
+
);
|
|
89
209
|
|
|
90
|
-
|
|
91
|
-
if (result.success) {
|
|
210
|
+
await this.sendEvents(this.session.apiClient, allEvents);
|
|
92
211
|
this.eventCache.clearEvents();
|
|
93
212
|
logger.debug(
|
|
94
213
|
LogId.telemetryEmitSuccess,
|
|
95
214
|
"telemetry",
|
|
96
215
|
`Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
|
|
97
216
|
);
|
|
98
|
-
|
|
217
|
+
} catch (error: unknown) {
|
|
218
|
+
logger.debug(
|
|
219
|
+
LogId.telemetryEmitFailure,
|
|
220
|
+
"telemetry",
|
|
221
|
+
`Error sending event to client: ${error instanceof Error ? error.message : String(error)}`
|
|
222
|
+
);
|
|
223
|
+
this.eventCache.appendEvents(events ?? []);
|
|
224
|
+
process.nextTick(async () => {
|
|
225
|
+
// try again
|
|
226
|
+
await this.flush();
|
|
227
|
+
});
|
|
99
228
|
}
|
|
100
229
|
|
|
101
|
-
|
|
102
|
-
LogId.telemetryEmitFailure,
|
|
103
|
-
"telemetry",
|
|
104
|
-
`Error sending event to client: ${result.error instanceof Error ? result.error.message : String(result.error)}`
|
|
105
|
-
);
|
|
106
|
-
this.eventCache.appendEvents(events);
|
|
230
|
+
this.flushing = false;
|
|
107
231
|
}
|
|
108
232
|
|
|
109
233
|
/**
|
|
110
234
|
* Attempts to send events through the provided API client
|
|
111
235
|
*/
|
|
112
|
-
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
} catch (error) {
|
|
122
|
-
return {
|
|
123
|
-
success: false,
|
|
124
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
236
|
+
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<void> {
|
|
237
|
+
const commonProperties = await this.getCommonProperties();
|
|
238
|
+
|
|
239
|
+
await client.sendEvents(
|
|
240
|
+
events.map((event) => ({
|
|
241
|
+
...event,
|
|
242
|
+
properties: { ...commonProperties, ...event.properties },
|
|
243
|
+
}))
|
|
244
|
+
);
|
|
127
245
|
}
|
|
128
246
|
}
|
package/src/telemetry/types.ts
CHANGED
|
@@ -53,7 +53,6 @@ export type ServerEvent = TelemetryEvent<ServerEventProperties>;
|
|
|
53
53
|
* Interface for static properties, they can be fetched once and reused.
|
|
54
54
|
*/
|
|
55
55
|
export type CommonStaticProperties = {
|
|
56
|
-
device_id?: string;
|
|
57
56
|
mcp_server_version: string;
|
|
58
57
|
mcp_server_name: string;
|
|
59
58
|
platform: string;
|
|
@@ -72,4 +71,5 @@ export type CommonProperties = {
|
|
|
72
71
|
config_atlas_auth?: TelemetryBoolSet;
|
|
73
72
|
config_connection_string?: TelemetryBoolSet;
|
|
74
73
|
session_id?: string;
|
|
74
|
+
is_container_env?: TelemetryBoolSet;
|
|
75
75
|
} & CommonStaticProperties;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ToolBase, ToolCategory, TelemetryToolMetadata } from "../tool.js";
|
|
1
|
+
import { ToolBase, ToolCategory, TelemetryToolMetadata, ToolArgs } from "../tool.js";
|
|
2
2
|
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import logger, { LogId } from "../../logger.js";
|
|
4
5
|
import { z } from "zod";
|
|
6
|
+
import { ApiClientError } from "../../common/atlas/apiClientError.js";
|
|
5
7
|
|
|
6
8
|
export abstract class AtlasToolBase extends ToolBase {
|
|
7
9
|
protected category: ToolCategory = "atlas";
|
|
@@ -13,6 +15,50 @@ export abstract class AtlasToolBase extends ToolBase {
|
|
|
13
15
|
return super.verifyAllowed();
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
protected handleError(
|
|
19
|
+
error: unknown,
|
|
20
|
+
args: ToolArgs<typeof this.argsShape>
|
|
21
|
+
): Promise<CallToolResult> | CallToolResult {
|
|
22
|
+
if (error instanceof ApiClientError) {
|
|
23
|
+
const statusCode = error.response.status;
|
|
24
|
+
|
|
25
|
+
if (statusCode === 401) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: `Unable to authenticate with MongoDB Atlas, API error: ${error.message}
|
|
31
|
+
|
|
32
|
+
Hint: Your API credentials may be invalid, expired or lack permissions.
|
|
33
|
+
Please check your Atlas API credentials and ensure they have the appropriate permissions.
|
|
34
|
+
For more information on setting up API keys, visit: https://www.mongodb.com/docs/atlas/configure-api-access/`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (statusCode === 403) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: `Received a Forbidden API Error: ${error.message}
|
|
47
|
+
|
|
48
|
+
You don't have sufficient permissions to perform this action in MongoDB Atlas
|
|
49
|
+
Please ensure your API key has the necessary roles assigned.
|
|
50
|
+
For more information on Atlas API access roles, visit: https://www.mongodb.com/docs/atlas/api/service-accounts-overview/`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For other types of errors, use the default error handling from the base class
|
|
59
|
+
return super.handleError(error, args);
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
/**
|
|
17
63
|
*
|
|
18
64
|
* Resolves the tool metadata from the arguments passed to the tool
|