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
|
@@ -3,6 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
5
|
import { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
|
|
6
|
+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
|
|
6
7
|
|
|
7
8
|
export class CreateDBUserTool extends AtlasToolBase {
|
|
8
9
|
protected name = "atlas-create-db-user";
|
|
@@ -11,7 +12,16 @@ export class CreateDBUserTool extends AtlasToolBase {
|
|
|
11
12
|
protected argsShape = {
|
|
12
13
|
projectId: z.string().describe("Atlas project ID"),
|
|
13
14
|
username: z.string().describe("Username for the new user"),
|
|
14
|
-
|
|
15
|
+
// Models will generate overly simplistic passwords like SecurePassword123 or
|
|
16
|
+
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
|
|
17
|
+
// the model not to try and generate anything and instead leave the field unset.
|
|
18
|
+
password: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.nullable()
|
|
22
|
+
.describe(
|
|
23
|
+
"Password for the new user. If the user hasn't supplied an explicit password, leave it unset and under no circumstances try to generate a random one. A secure password will be generated by the MCP server if necessary."
|
|
24
|
+
),
|
|
15
25
|
roles: z
|
|
16
26
|
.array(
|
|
17
27
|
z.object({
|
|
@@ -34,6 +44,11 @@ export class CreateDBUserTool extends AtlasToolBase {
|
|
|
34
44
|
roles,
|
|
35
45
|
clusters,
|
|
36
46
|
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
47
|
+
const shouldGeneratePassword = !password;
|
|
48
|
+
if (shouldGeneratePassword) {
|
|
49
|
+
password = await generateSecurePassword();
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
const input = {
|
|
38
53
|
groupId: projectId,
|
|
39
54
|
awsIAMType: "NONE",
|
|
@@ -62,7 +77,12 @@ export class CreateDBUserTool extends AtlasToolBase {
|
|
|
62
77
|
});
|
|
63
78
|
|
|
64
79
|
return {
|
|
65
|
-
content: [
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `User "${username}" created successfully${shouldGeneratePassword ? ` with password: \`${password}\`` : ""}.`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
66
86
|
};
|
|
67
87
|
}
|
|
68
88
|
}
|
|
@@ -28,7 +28,13 @@ export class CreateProjectTool extends AtlasToolBase {
|
|
|
28
28
|
"No organizations were found in your MongoDB Atlas account. Please create an organization first."
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const firstOrg = organizations.results[0];
|
|
32
|
+
if (!firstOrg?.id) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"The first organization found does not have an ID. Please check your Atlas account."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
organizationId = firstOrg.id;
|
|
32
38
|
assumedOrg = true;
|
|
33
39
|
} catch {
|
|
34
40
|
throw new Error(
|
|
@@ -2,24 +2,15 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from "util";
|
|
5
|
+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
|
|
7
6
|
import logger, { LogId } from "../../../logger.js";
|
|
7
|
+
import { inspectCluster } from "../../../common/atlas/cluster.js";
|
|
8
8
|
|
|
9
9
|
const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
|
|
10
10
|
|
|
11
|
-
const randomBytesAsync = promisify(randomBytes);
|
|
12
|
-
|
|
13
|
-
async function generateSecurePassword(): Promise<string> {
|
|
14
|
-
const buf = await randomBytesAsync(16);
|
|
15
|
-
const pass = buf.toString("base64url");
|
|
16
|
-
return pass;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
11
|
function sleep(ms: number): Promise<void> {
|
|
20
12
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
13
|
}
|
|
22
|
-
|
|
23
14
|
export class ConnectClusterTool extends AtlasToolBase {
|
|
24
15
|
protected name = "atlas-connect-cluster";
|
|
25
16
|
protected description = "Connect to MongoDB Atlas cluster";
|
|
@@ -32,22 +23,9 @@ export class ConnectClusterTool extends AtlasToolBase {
|
|
|
32
23
|
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
33
24
|
await this.session.disconnect();
|
|
34
25
|
|
|
35
|
-
const cluster = await this.session.apiClient
|
|
36
|
-
params: {
|
|
37
|
-
path: {
|
|
38
|
-
groupId: projectId,
|
|
39
|
-
clusterName,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!cluster) {
|
|
45
|
-
throw new Error("Cluster not found");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const baseConnectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard;
|
|
26
|
+
const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName);
|
|
49
27
|
|
|
50
|
-
if (!
|
|
28
|
+
if (!cluster.connectionString) {
|
|
51
29
|
throw new Error("Connection string not available");
|
|
52
30
|
}
|
|
53
31
|
|
|
@@ -99,7 +77,7 @@ export class ConnectClusterTool extends AtlasToolBase {
|
|
|
99
77
|
expiryDate,
|
|
100
78
|
};
|
|
101
79
|
|
|
102
|
-
const cn = new URL(
|
|
80
|
+
const cn = new URL(cluster.connectionString);
|
|
103
81
|
cn.username = username;
|
|
104
82
|
cn.password = password;
|
|
105
83
|
cn.searchParams.set("authSource", "admin");
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Cluster, inspectCluster } from "../../../common/atlas/cluster.js";
|
|
6
6
|
|
|
7
7
|
export class InspectClusterTool extends AtlasToolBase {
|
|
8
8
|
protected name = "atlas-inspect-cluster";
|
|
@@ -14,55 +14,19 @@ export class InspectClusterTool extends AtlasToolBase {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
17
|
-
const cluster = await this.session.apiClient
|
|
18
|
-
params: {
|
|
19
|
-
path: {
|
|
20
|
-
groupId: projectId,
|
|
21
|
-
clusterName,
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
});
|
|
17
|
+
const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName);
|
|
25
18
|
|
|
26
19
|
return this.formatOutput(cluster);
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
private formatOutput(
|
|
30
|
-
if (!cluster) {
|
|
31
|
-
throw new Error("Cluster not found");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const regionConfigs = (cluster.replicationSpecs || [])
|
|
35
|
-
.map(
|
|
36
|
-
(replicationSpec) =>
|
|
37
|
-
(replicationSpec.regionConfigs || []) as {
|
|
38
|
-
providerName: string;
|
|
39
|
-
electableSpecs?: {
|
|
40
|
-
instanceSize: string;
|
|
41
|
-
};
|
|
42
|
-
readOnlySpecs?: {
|
|
43
|
-
instanceSize: string;
|
|
44
|
-
};
|
|
45
|
-
}[]
|
|
46
|
-
)
|
|
47
|
-
.flat()
|
|
48
|
-
.map((regionConfig) => {
|
|
49
|
-
return {
|
|
50
|
-
providerName: regionConfig.providerName,
|
|
51
|
-
instanceSize: regionConfig.electableSpecs?.instanceSize || regionConfig.readOnlySpecs?.instanceSize,
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const instanceSize = (regionConfigs.length <= 0 ? undefined : regionConfigs[0].instanceSize) || "UNKNOWN";
|
|
56
|
-
|
|
57
|
-
const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
|
|
58
|
-
|
|
22
|
+
private formatOutput(formattedCluster: Cluster): CallToolResult {
|
|
59
23
|
return {
|
|
60
24
|
content: [
|
|
61
25
|
{
|
|
62
26
|
type: "text",
|
|
63
27
|
text: `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String
|
|
64
28
|
----------------|----------------|----------------|----------------|----------------|----------------
|
|
65
|
-
${
|
|
29
|
+
${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`,
|
|
66
30
|
},
|
|
67
31
|
],
|
|
68
32
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
|
+
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
+
|
|
6
|
+
export class ListAlertsTool extends AtlasToolBase {
|
|
7
|
+
protected name = "atlas-list-alerts";
|
|
8
|
+
protected description = "List MongoDB Atlas alerts";
|
|
9
|
+
protected operationType: OperationType = "read";
|
|
10
|
+
protected argsShape = {
|
|
11
|
+
projectId: z.string().describe("Atlas project ID to list alerts for"),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
15
|
+
const data = await this.session.apiClient.listAlerts({
|
|
16
|
+
params: {
|
|
17
|
+
path: {
|
|
18
|
+
groupId: projectId,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!data?.results?.length) {
|
|
24
|
+
return { content: [{ type: "text", text: "No alerts found in your MongoDB Atlas project." }] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Format alerts as a table
|
|
28
|
+
const output =
|
|
29
|
+
`Alert ID | Status | Created | Updated | Type | Comment
|
|
30
|
+
----------|---------|----------|----------|------|--------
|
|
31
|
+
` +
|
|
32
|
+
data.results
|
|
33
|
+
.map((alert) => {
|
|
34
|
+
const created = alert.created ? new Date(alert.created).toLocaleString() : "N/A";
|
|
35
|
+
const updated = alert.updated ? new Date(alert.updated).toLocaleString() : "N/A";
|
|
36
|
+
const comment = alert.acknowledgementComment ?? "N/A";
|
|
37
|
+
return `${alert.id} | ${alert.status} | ${created} | ${updated} | ${alert.eventTypeName} | ${comment}`;
|
|
38
|
+
})
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: output }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -2,7 +2,13 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PaginatedClusterDescription20240805,
|
|
7
|
+
PaginatedOrgGroupView,
|
|
8
|
+
Group,
|
|
9
|
+
PaginatedFlexClusters20241113,
|
|
10
|
+
} from "../../../common/atlas/openapi.js";
|
|
11
|
+
import { formatCluster, formatFlexCluster } from "../../../common/atlas/cluster.js";
|
|
6
12
|
|
|
7
13
|
export class ListClustersTool extends AtlasToolBase {
|
|
8
14
|
protected name = "atlas-list-clusters";
|
|
@@ -73,43 +79,20 @@ ${rows}`,
|
|
|
73
79
|
};
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
private formatClustersTable(
|
|
77
|
-
|
|
82
|
+
private formatClustersTable(
|
|
83
|
+
project: Group,
|
|
84
|
+
clusters?: PaginatedClusterDescription20240805,
|
|
85
|
+
flexClusters?: PaginatedFlexClusters20241113
|
|
86
|
+
): CallToolResult {
|
|
87
|
+
// Check if both traditional clusters and flex clusters are absent
|
|
88
|
+
if (!clusters?.results?.length && !flexClusters?.results?.length) {
|
|
78
89
|
throw new Error("No clusters found.");
|
|
79
90
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const regionConfigs = (cluster.replicationSpecs || [])
|
|
86
|
-
.map(
|
|
87
|
-
(replicationSpec) =>
|
|
88
|
-
(replicationSpec.regionConfigs || []) as {
|
|
89
|
-
providerName: string;
|
|
90
|
-
electableSpecs?: {
|
|
91
|
-
instanceSize: string;
|
|
92
|
-
};
|
|
93
|
-
readOnlySpecs?: {
|
|
94
|
-
instanceSize: string;
|
|
95
|
-
};
|
|
96
|
-
}[]
|
|
97
|
-
)
|
|
98
|
-
.flat()
|
|
99
|
-
.map((regionConfig) => {
|
|
100
|
-
return {
|
|
101
|
-
providerName: regionConfig.providerName,
|
|
102
|
-
instanceSize:
|
|
103
|
-
regionConfig.electableSpecs?.instanceSize || regionConfig.readOnlySpecs?.instanceSize,
|
|
104
|
-
};
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const instanceSize =
|
|
108
|
-
(regionConfigs.length <= 0 ? undefined : regionConfigs[0].instanceSize) || "UNKNOWN";
|
|
109
|
-
|
|
110
|
-
const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
|
|
111
|
-
|
|
112
|
-
return `${cluster.name} | ${clusterInstanceType} | ${clusterInstanceType == "DEDICATED" ? instanceSize : "N/A"} | ${cluster.stateName} | ${mongoDBVersion} | ${connectionString}`;
|
|
91
|
+
const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || [];
|
|
92
|
+
const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || [];
|
|
93
|
+
const rows = [...formattedClusters, ...formattedFlexClusters]
|
|
94
|
+
.map((formattedCluster) => {
|
|
95
|
+
return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`;
|
|
113
96
|
})
|
|
114
97
|
.join("\n");
|
|
115
98
|
return {
|
|
@@ -21,7 +21,8 @@ export class ListProjectsTool extends AtlasToolBase {
|
|
|
21
21
|
|
|
22
22
|
const orgs: Record<string, string> = orgData.results
|
|
23
23
|
.map((org) => [org.id || "", org.name])
|
|
24
|
-
.
|
|
24
|
+
.filter(([id]) => id)
|
|
25
|
+
.reduce((acc, [id, name]) => ({ ...acc, [id as string]: name }), {});
|
|
25
26
|
|
|
26
27
|
const data = orgId
|
|
27
28
|
? await this.session.apiClient.listOrganizationProjects({
|
|
@@ -41,7 +42,8 @@ export class ListProjectsTool extends AtlasToolBase {
|
|
|
41
42
|
const rows = data.results
|
|
42
43
|
.map((project) => {
|
|
43
44
|
const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A";
|
|
44
|
-
|
|
45
|
+
const orgName = orgs[project.orgId] ?? "N/A";
|
|
46
|
+
return `${project.name} | ${project.id} | ${orgName} | ${project.orgId} | ${createdAt}`;
|
|
45
47
|
})
|
|
46
48
|
.join("\n");
|
|
47
49
|
const formattedProjects = `Project Name | Project ID | Organization Name | Organization ID | Created At
|
package/src/tools/atlas/tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CreateDBUserTool } from "./create/createDBUser.js";
|
|
|
9
9
|
import { CreateProjectTool } from "./create/createProject.js";
|
|
10
10
|
import { ListOrganizationsTool } from "./read/listOrgs.js";
|
|
11
11
|
import { ConnectClusterTool } from "./metadata/connectCluster.js";
|
|
12
|
+
import { ListAlertsTool } from "./read/listAlerts.js";
|
|
12
13
|
|
|
13
14
|
export const AtlasTools = [
|
|
14
15
|
ListClustersTool,
|
|
@@ -22,4 +23,5 @@ export const AtlasTools = [
|
|
|
22
23
|
CreateProjectTool,
|
|
23
24
|
ListOrganizationsTool,
|
|
24
25
|
ConnectClusterTool,
|
|
26
|
+
ListAlertsTool,
|
|
25
27
|
];
|
|
@@ -7,7 +7,6 @@ export class ListDatabasesTool extends MongoDBToolBase {
|
|
|
7
7
|
protected name = "list-databases";
|
|
8
8
|
protected description = "List all databases for a MongoDB connection";
|
|
9
9
|
protected argsShape = {};
|
|
10
|
-
|
|
11
10
|
protected operationType: OperationType = "metadata";
|
|
12
11
|
|
|
13
12
|
protected async execute(): Promise<CallToolResult> {
|
|
@@ -8,13 +8,14 @@ export const CountArgs = {
|
|
|
8
8
|
.record(z.string(), z.unknown())
|
|
9
9
|
.optional()
|
|
10
10
|
.describe(
|
|
11
|
-
"
|
|
11
|
+
"A filter/query parameter. Allows users to filter the documents to count. Matches the syntax of the filter argument of db.collection.count()."
|
|
12
12
|
),
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export class CountTool extends MongoDBToolBase {
|
|
16
16
|
protected name = "count";
|
|
17
|
-
protected description =
|
|
17
|
+
protected description =
|
|
18
|
+
"Gets the number of documents in a MongoDB collection using db.collection.count() and query as an optional filter parameter";
|
|
18
19
|
protected argsShape = {
|
|
19
20
|
...DbOperationArgs,
|
|
20
21
|
...CountArgs,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// import { ConnectTool } from "./metadata/connect.js";
|
|
1
|
+
import { ConnectTool } from "./metadata/connect.js";
|
|
3
2
|
import { ListCollectionsTool } from "./metadata/listCollections.js";
|
|
4
3
|
import { CollectionIndexesTool } from "./read/collectionIndexes.js";
|
|
5
4
|
import { ListDatabasesTool } from "./metadata/listDatabases.js";
|
|
@@ -21,8 +20,7 @@ import { CreateCollectionTool } from "./create/createCollection.js";
|
|
|
21
20
|
import { LogsTool } from "./metadata/logs.js";
|
|
22
21
|
|
|
23
22
|
export const MongoDbTools = [
|
|
24
|
-
|
|
25
|
-
// ConnectTool,
|
|
23
|
+
ConnectTool,
|
|
26
24
|
ListCollectionsTool,
|
|
27
25
|
ListDatabasesTool,
|
|
28
26
|
CollectionIndexesTool,
|
package/src/tools/tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z, type ZodRawShape, type ZodNever, AnyZodObject } from "zod";
|
|
2
2
|
import type { McpServer, RegisteredTool, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { Session } from "../session.js";
|
|
5
5
|
import logger, { LogId } from "../logger.js";
|
|
6
6
|
import { Telemetry } from "../telemetry/telemetry.js";
|
|
@@ -27,6 +27,34 @@ export abstract class ToolBase {
|
|
|
27
27
|
|
|
28
28
|
protected abstract argsShape: ZodRawShape;
|
|
29
29
|
|
|
30
|
+
protected get annotations(): ToolAnnotations {
|
|
31
|
+
const annotations: ToolAnnotations = {
|
|
32
|
+
title: this.name,
|
|
33
|
+
description: this.description,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
switch (this.operationType) {
|
|
37
|
+
case "read":
|
|
38
|
+
case "metadata":
|
|
39
|
+
annotations.readOnlyHint = true;
|
|
40
|
+
annotations.destructiveHint = false;
|
|
41
|
+
break;
|
|
42
|
+
case "delete":
|
|
43
|
+
annotations.readOnlyHint = false;
|
|
44
|
+
annotations.destructiveHint = true;
|
|
45
|
+
break;
|
|
46
|
+
case "create":
|
|
47
|
+
case "update":
|
|
48
|
+
annotations.destructiveHint = false;
|
|
49
|
+
annotations.readOnlyHint = false;
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return annotations;
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
|
|
31
59
|
|
|
32
60
|
constructor(
|
|
@@ -43,20 +71,20 @@ export abstract class ToolBase {
|
|
|
43
71
|
const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
|
|
44
72
|
const startTime = Date.now();
|
|
45
73
|
try {
|
|
46
|
-
logger.debug(LogId.toolExecute, "tool", `Executing ${this.name}
|
|
74
|
+
logger.debug(LogId.toolExecute, "tool", `Executing tool ${this.name}`);
|
|
47
75
|
|
|
48
76
|
const result = await this.execute(...args);
|
|
49
|
-
|
|
77
|
+
this.emitToolEvent(startTime, result, ...args);
|
|
50
78
|
return result;
|
|
51
79
|
} catch (error: unknown) {
|
|
52
80
|
logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
|
|
53
81
|
const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
|
|
54
|
-
|
|
82
|
+
this.emitToolEvent(startTime, toolResult, ...args);
|
|
55
83
|
return toolResult;
|
|
56
84
|
}
|
|
57
85
|
};
|
|
58
86
|
|
|
59
|
-
server.tool(this.name, this.description, this.argsShape, callback);
|
|
87
|
+
server.tool(this.name, this.description, this.argsShape, this.annotations, callback);
|
|
60
88
|
|
|
61
89
|
// This is very similar to RegisteredTool.update, but without the bugs around the name.
|
|
62
90
|
// In the upstream update method, the name is captured in the closure and not updated when
|
|
@@ -66,13 +94,22 @@ export abstract class ToolBase {
|
|
|
66
94
|
const tools = server["_registeredTools"] as { [toolName: string]: RegisteredTool };
|
|
67
95
|
const existingTool = tools[this.name];
|
|
68
96
|
|
|
97
|
+
if (!existingTool) {
|
|
98
|
+
logger.warning(LogId.toolUpdateFailure, "tool", `Tool ${this.name} not found in update`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
existingTool.annotations = this.annotations;
|
|
103
|
+
|
|
69
104
|
if (updates.name && updates.name !== this.name) {
|
|
105
|
+
existingTool.annotations.title = updates.name;
|
|
70
106
|
delete tools[this.name];
|
|
71
107
|
this.name = updates.name;
|
|
72
108
|
tools[this.name] = existingTool;
|
|
73
109
|
}
|
|
74
110
|
|
|
75
111
|
if (updates.description) {
|
|
112
|
+
existingTool.annotations.description = updates.description;
|
|
76
113
|
existingTool.description = updates.description;
|
|
77
114
|
this.description = updates.description;
|
|
78
115
|
}
|
|
@@ -142,11 +179,11 @@ export abstract class ToolBase {
|
|
|
142
179
|
* @param result - Whether the command succeeded or failed
|
|
143
180
|
* @param args - The arguments passed to the tool
|
|
144
181
|
*/
|
|
145
|
-
private
|
|
182
|
+
private emitToolEvent(
|
|
146
183
|
startTime: number,
|
|
147
184
|
result: CallToolResult,
|
|
148
185
|
...args: Parameters<ToolCallback<typeof this.argsShape>>
|
|
149
|
-
):
|
|
186
|
+
): void {
|
|
150
187
|
if (!this.telemetry.isTelemetryEnabled()) {
|
|
151
188
|
return;
|
|
152
189
|
}
|
|
@@ -172,6 +209,6 @@ export abstract class ToolBase {
|
|
|
172
209
|
event.properties.project_id = metadata.projectId;
|
|
173
210
|
}
|
|
174
211
|
|
|
175
|
-
|
|
212
|
+
this.telemetry.emitEvents([event]);
|
|
176
213
|
}
|
|
177
214
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare module "mongodb-connection-string-url" {
|
|
2
|
+
import { URL } from "whatwg-url";
|
|
3
|
+
import { redactConnectionString, ConnectionStringRedactionOptions } from "./redact";
|
|
4
|
+
export { redactConnectionString, ConnectionStringRedactionOptions };
|
|
5
|
+
declare class CaseInsensitiveMap<K extends string = string> extends Map<K, string> {
|
|
6
|
+
delete(name: K): boolean;
|
|
7
|
+
get(name: K): string | undefined;
|
|
8
|
+
has(name: K): boolean;
|
|
9
|
+
set(name: K, value: any): this;
|
|
10
|
+
_normalizeKey(name: any): K;
|
|
11
|
+
}
|
|
12
|
+
declare abstract class URLWithoutHost extends URL {
|
|
13
|
+
abstract get host(): never;
|
|
14
|
+
abstract set host(value: never);
|
|
15
|
+
abstract get hostname(): never;
|
|
16
|
+
abstract set hostname(value: never);
|
|
17
|
+
abstract get port(): never;
|
|
18
|
+
abstract set port(value: never);
|
|
19
|
+
abstract get href(): string;
|
|
20
|
+
abstract set href(value: string);
|
|
21
|
+
}
|
|
22
|
+
export interface ConnectionStringParsingOptions {
|
|
23
|
+
looseValidation?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare class ConnectionString extends URLWithoutHost {
|
|
26
|
+
_hosts: string[];
|
|
27
|
+
constructor(uri: string, options?: ConnectionStringParsingOptions);
|
|
28
|
+
get host(): never;
|
|
29
|
+
set host(_ignored: never);
|
|
30
|
+
get hostname(): never;
|
|
31
|
+
set hostname(_ignored: never);
|
|
32
|
+
get port(): never;
|
|
33
|
+
set port(_ignored: never);
|
|
34
|
+
get href(): string;
|
|
35
|
+
set href(_ignored: string);
|
|
36
|
+
get isSRV(): boolean;
|
|
37
|
+
get hosts(): string[];
|
|
38
|
+
set hosts(list: string[]);
|
|
39
|
+
toString(): string;
|
|
40
|
+
clone(): ConnectionString;
|
|
41
|
+
redact(options?: ConnectionStringRedactionOptions): ConnectionString;
|
|
42
|
+
typedSearchParams<T extends {}>(): {
|
|
43
|
+
append(name: keyof T & string, value: any): void;
|
|
44
|
+
delete(name: keyof T & string): void;
|
|
45
|
+
get(name: keyof T & string): string | null;
|
|
46
|
+
getAll(name: keyof T & string): string[];
|
|
47
|
+
has(name: keyof T & string): boolean;
|
|
48
|
+
set(name: keyof T & string, value: any): void;
|
|
49
|
+
keys(): IterableIterator<keyof T & string>;
|
|
50
|
+
values(): IterableIterator<string>;
|
|
51
|
+
entries(): IterableIterator<[keyof T & string, string]>;
|
|
52
|
+
_normalizeKey(name: keyof T & string): string;
|
|
53
|
+
[Symbol.iterator](): IterableIterator<[keyof T & string, string]>;
|
|
54
|
+
sort(): void;
|
|
55
|
+
forEach<THIS_ARG = void>(
|
|
56
|
+
callback: (this: THIS_ARG, value: string, name: string, searchParams: any) => void,
|
|
57
|
+
thisArg?: THIS_ARG | undefined
|
|
58
|
+
): void;
|
|
59
|
+
readonly [Symbol.toStringTag]: "URLSearchParams";
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export declare class CommaAndColonSeparatedRecord<
|
|
63
|
+
K extends {} = Record<string, unknown>,
|
|
64
|
+
> extends CaseInsensitiveMap<keyof K & string> {
|
|
65
|
+
constructor(from?: string | null);
|
|
66
|
+
toString(): string;
|
|
67
|
+
}
|
|
68
|
+
export default ConnectionString;
|
|
69
|
+
}
|
|
@@ -5,7 +5,9 @@ import { UserConfig } from "../../src/config.js";
|
|
|
5
5
|
import { McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { Session } from "../../src/session.js";
|
|
8
|
+
import { Telemetry } from "../../src/telemetry/telemetry.js";
|
|
8
9
|
import { config } from "../../src/config.js";
|
|
10
|
+
import { jest } from "@jest/globals";
|
|
9
11
|
|
|
10
12
|
interface ParameterInfo {
|
|
11
13
|
name: string;
|
|
@@ -56,14 +58,27 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
|
|
|
56
58
|
apiClientSecret: userConfig.apiClientSecret,
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
// Mock hasValidAccessToken for tests
|
|
62
|
+
if (userConfig.apiClientId && userConfig.apiClientSecret) {
|
|
63
|
+
const mockFn = jest.fn<() => Promise<boolean>>().mockResolvedValue(true);
|
|
64
|
+
// @ts-expect-error accessing private property for testing
|
|
65
|
+
session.apiClient.validateAccessToken = mockFn;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
userConfig.telemetry = "disabled";
|
|
69
|
+
|
|
70
|
+
const telemetry = Telemetry.create(session, userConfig);
|
|
71
|
+
|
|
59
72
|
mcpServer = new Server({
|
|
60
73
|
session,
|
|
61
74
|
userConfig,
|
|
75
|
+
telemetry,
|
|
62
76
|
mcpServer: new McpServer({
|
|
63
77
|
name: "test-server",
|
|
64
78
|
version: "5.2.3",
|
|
65
79
|
}),
|
|
66
80
|
});
|
|
81
|
+
|
|
67
82
|
await mcpServer.connect(serverTransport);
|
|
68
83
|
await mcpClient.connect(clientTransport);
|
|
69
84
|
});
|
|
@@ -117,7 +132,7 @@ export function getResponseElements(content: unknown | { content: unknown }): {
|
|
|
117
132
|
content = (content as { content: unknown }).content;
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
expect(
|
|
135
|
+
expect(content).toBeArray();
|
|
121
136
|
|
|
122
137
|
const response = content as { type: string; text: string }[];
|
|
123
138
|
for (const item of response) {
|
|
@@ -191,6 +206,7 @@ export function validateToolMetadata(
|
|
|
191
206
|
expectDefined(tool);
|
|
192
207
|
expect(tool.description).toBe(description);
|
|
193
208
|
|
|
209
|
+
validateToolAnnotations(tool, name, description);
|
|
194
210
|
const toolParameters = getParameters(tool);
|
|
195
211
|
expect(toolParameters).toHaveLength(parameters.length);
|
|
196
212
|
expect(toolParameters).toIncludeAllMembers(parameters);
|
|
@@ -221,6 +237,29 @@ export function validateThrowsForInvalidArguments(
|
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
/** Expects the argument being defined and asserts it */
|
|
224
|
-
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
|
|
240
|
+
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined | null> {
|
|
225
241
|
expect(arg).toBeDefined();
|
|
242
|
+
expect(arg).not.toBeNull();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateToolAnnotations(tool: ToolInfo, name: string, description: string): void {
|
|
246
|
+
expectDefined(tool.annotations);
|
|
247
|
+
expect(tool.annotations.title).toBe(name);
|
|
248
|
+
expect(tool.annotations.description).toBe(description);
|
|
249
|
+
|
|
250
|
+
switch (tool.operationType) {
|
|
251
|
+
case "read":
|
|
252
|
+
case "metadata":
|
|
253
|
+
expect(tool.annotations.readOnlyHint).toBe(true);
|
|
254
|
+
expect(tool.annotations.destructiveHint).toBe(false);
|
|
255
|
+
break;
|
|
256
|
+
case "delete":
|
|
257
|
+
expect(tool.annotations.readOnlyHint).toBe(false);
|
|
258
|
+
expect(tool.annotations.destructiveHint).toBe(true);
|
|
259
|
+
break;
|
|
260
|
+
case "create":
|
|
261
|
+
case "update":
|
|
262
|
+
expect(tool.annotations.readOnlyHint).toBe(false);
|
|
263
|
+
expect(tool.annotations.destructiveHint).toBe(false);
|
|
264
|
+
}
|
|
226
265
|
}
|