Skip to content

Commit 6df2179

Browse files
Merge branch 'main' into asr/add-health-endpoint
2 parents 51efe4e + 34e748f commit 6df2179

20 files changed

+407
-324
lines changed

.github/workflows/code-health-long-running.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ permissions: {}
1111
jobs:
1212
run-long-running-tests:
1313
name: Run long running tests
14-
if: github.event_name == 'push' || (github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)
1514
runs-on: ubuntu-latest
1615
steps:
1716
- uses: GitHubSecurityLab/actions-permissions/monitor@v1

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/telemetry/types.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type TelemetryEvent<T> = {
1616
duration_ms: number;
1717
result: TelemetryResult;
1818
category: string;
19-
};
19+
} & Record<string, string | number | string[]>;
2020
};
2121

2222
export type BaseEvent = TelemetryEvent<unknown>;
@@ -28,14 +28,12 @@ export type ToolEventProperties = {
2828
command: string;
2929
error_code?: string;
3030
error_type?: string;
31-
project_id?: string;
32-
org_id?: string;
3331
cluster_name?: string;
3432
is_atlas?: boolean;
35-
atlas_local_deployment_id?: string;
36-
};
33+
} & TelemetryToolMetadata;
3734

3835
export type ToolEvent = TelemetryEvent<ToolEventProperties>;
36+
3937
/**
4038
* Interface for server events
4139
*/
@@ -137,3 +135,23 @@ export type CommonProperties = {
137135
*/
138136
hosting_mode?: string;
139137
} & CommonStaticProperties;
138+
139+
/**
140+
* Telemetry metadata that can be provided by tools when emitting telemetry events.
141+
* For MongoDB tools, this is typically empty, while for Atlas tools, this should include
142+
* the project and organization IDs if available.
143+
*/
144+
export type TelemetryToolMetadata = AtlasLocalToolMetadata | AtlasToolMetadata | PerfAdvisorToolMetadata;
145+
146+
export type AtlasLocalToolMetadata = {
147+
atlas_local_deployment_id?: string;
148+
};
149+
150+
export type AtlasToolMetadata = {
151+
project_id?: string;
152+
org_id?: string;
153+
};
154+
155+
export type PerfAdvisorToolMetadata = AtlasToolMetadata & {
156+
operations: string[];
157+
};

src/tools/atlas/atlasTool.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { ToolBase, type ToolArgs, type ToolCategory, type TelemetryToolMetadata } from "../tool.js";
2+
import type { AtlasToolMetadata } from "../../telemetry/types.js";
3+
import { ToolBase, type ToolArgs, type ToolCategory } from "../tool.js";
34
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
45
import { LogId } from "../../common/logger.js";
56
import { z } from "zod";
@@ -84,8 +85,8 @@ For more information on Atlas API access roles, visit: https://www.mongodb.com/d
8485
protected resolveTelemetryMetadata(
8586
result: CallToolResult,
8687
...args: Parameters<ToolCallback<typeof this.argsShape>>
87-
): TelemetryToolMetadata {
88-
const toolMetadata: TelemetryToolMetadata = {};
88+
): AtlasToolMetadata {
89+
const toolMetadata: AtlasToolMetadata = {};
8990
if (!args.length) {
9091
return toolMetadata;
9192
}
@@ -107,12 +108,12 @@ For more information on Atlas API access roles, visit: https://www.mongodb.com/d
107108

108109
// Extract projectId using type guard
109110
if ("projectId" in data && typeof data.projectId === "string" && data.projectId.trim() !== "") {
110-
toolMetadata.projectId = data.projectId;
111+
toolMetadata.project_id = data.projectId;
111112
}
112113

113114
// Extract orgId using type guard
114115
if ("orgId" in data && typeof data.orgId === "string" && data.orgId.trim() !== "") {
115-
toolMetadata.orgId = data.orgId;
116+
toolMetadata.org_id = data.orgId;
116117
}
117118
return toolMetadata;
118119
}

src/tools/atlas/read/getPerformanceAdvisor.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from "zod";
22
import { AtlasToolBase } from "../atlasTool.js";
3-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import type { CallToolResult, ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js";
44
import type { OperationType, ToolArgs } from "../../tool.js";
55
import { formatUntrustedData } from "../../tool.js";
66
import {
@@ -13,6 +13,8 @@ import {
1313
SLOW_QUERY_LOGS_COPY,
1414
} from "../../../common/atlas/performanceAdvisorUtils.js";
1515
import { AtlasArgs } from "../../args.js";
16+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
17+
import type { PerfAdvisorToolMetadata } from "../../../telemetry/types.js";
1618

1719
const PerformanceAdvisorOperationType = z.enum([
1820
"suggestedIndexes",
@@ -130,4 +132,15 @@ export class GetPerformanceAdvisorTool extends AtlasToolBase {
130132
};
131133
}
132134
}
135+
136+
protected override resolveTelemetryMetadata(
137+
result: CallToolResult,
138+
args: ToolArgs<typeof this.argsShape>,
139+
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
140+
): PerfAdvisorToolMetadata {
141+
return {
142+
...super.resolveTelemetryMetadata(result, args, extra),
143+
operations: args.operations,
144+
};
145+
}
133146
}

src/tools/atlas/read/inspectAccessList.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,14 @@ export class InspectAccessListTool extends AtlasToolBase {
3232
};
3333
}
3434

35+
const entries = results.map((entry) => ({
36+
ipAddress: entry.ipAddress,
37+
cidrBlock: entry.cidrBlock,
38+
comment: entry.comment,
39+
}));
40+
3541
return {
36-
content: formatUntrustedData(
37-
`Found ${results.length} access list entries`,
38-
`IP ADDRESS | CIDR | COMMENT
39-
------|------|------
40-
${results
41-
.map((entry) => {
42-
return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`;
43-
})
44-
.join("\n")}`
45-
),
42+
content: formatUntrustedData(`Found ${results.length} access list entries`, JSON.stringify(entries)),
4643
};
4744
}
4845
}

src/tools/atlas/read/inspectCluster.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ export class InspectClusterTool extends AtlasToolBase {
2525
}
2626

2727
private formatOutput(formattedCluster: Cluster): CallToolResult {
28+
const clusterDetails = {
29+
name: formattedCluster.name || "Unknown",
30+
instanceType: formattedCluster.instanceType,
31+
instanceSize: formattedCluster.instanceSize || "N/A",
32+
state: formattedCluster.state || "UNKNOWN",
33+
mongoDBVersion: formattedCluster.mongoDBVersion || "N/A",
34+
connectionStrings: formattedCluster.connectionStrings || {},
35+
};
36+
2837
return {
29-
content: formatUntrustedData(
30-
"Cluster details:",
31-
`Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String
32-
----------------|----------------|----------------|----------------|----------------|----------------
33-
${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionStrings?.standardSrv || formattedCluster.connectionStrings?.standard || "N/A"}`
34-
),
38+
content: formatUntrustedData("Cluster details:", JSON.stringify(clusterDetails)),
3539
};
3640
}
3741
}

src/tools/atlas/read/listAlerts.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,20 @@ export class ListAlertsTool extends AtlasToolBase {
2828
return { content: [{ type: "text", text: "No alerts found in your MongoDB Atlas project." }] };
2929
}
3030

31-
// Format alerts as a table
32-
const output =
33-
`Alert ID | Status | Created | Updated | Type | Comment
34-
----------|---------|----------|----------|------|--------
35-
` +
36-
data.results
37-
.map((alert) => {
38-
const created = alert.created ? new Date(alert.created).toLocaleString() : "N/A";
39-
const updated = alert.updated ? new Date(alert.updated).toLocaleString() : "N/A";
40-
const comment = alert.acknowledgementComment ?? "N/A";
41-
return `${alert.id} | ${alert.status} | ${created} | ${updated} | ${alert.eventTypeName} | ${comment}`;
42-
})
43-
.join("\n");
31+
const alerts = data.results.map((alert) => ({
32+
id: alert.id,
33+
status: alert.status,
34+
created: alert.created ? new Date(alert.created).toISOString() : "N/A",
35+
updated: alert.updated ? new Date(alert.updated).toISOString() : "N/A",
36+
eventTypeName: alert.eventTypeName,
37+
acknowledgementComment: alert.acknowledgementComment ?? "N/A",
38+
}));
4439

4540
return {
46-
content: formatUntrustedData(`Found ${data.results.length} alerts in project ${projectId}`, output),
41+
content: formatUntrustedData(
42+
`Found ${data.results.length} alerts in project ${projectId}`,
43+
JSON.stringify(alerts)
44+
),
4745
};
4846
}
4947
}

src/tools/atlas/read/listClusters.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,22 @@ export class ListClustersTool extends AtlasToolBase {
5959
}
6060
const formattedClusters = clusters.results
6161
.map((result) => {
62-
return (result.clusters || []).map((cluster) => {
63-
return { ...result, ...cluster, clusters: undefined };
64-
});
62+
return (result.clusters || []).map((cluster) => ({
63+
projectName: result.groupName,
64+
projectId: result.groupId,
65+
clusterName: cluster.name,
66+
}));
6567
})
6668
.flat();
6769
if (!formattedClusters.length) {
6870
throw new Error("No clusters found.");
6971
}
70-
const rows = formattedClusters
71-
.map((cluster) => {
72-
return `${cluster.groupName} (${cluster.groupId}) | ${cluster.name}`;
73-
})
74-
.join("\n");
72+
7573
return {
76-
content: [
77-
{
78-
type: "text",
79-
text: `Project | Cluster Name
80-
----------------|----------------
81-
${rows}`,
82-
},
83-
],
74+
content: formatUntrustedData(
75+
`Found ${formattedClusters.length} clusters across all projects`,
76+
JSON.stringify(formattedClusters)
77+
),
8478
};
8579
}
8680

@@ -98,16 +92,11 @@ ${rows}`,
9892
const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || [];
9993
const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || [];
10094
const allClusters = [...formattedClusters, ...formattedFlexClusters];
95+
10196
return {
10297
content: formatUntrustedData(
10398
`Found ${allClusters.length} clusters in project "${project.name}" (${project.id}):`,
104-
`Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String
105-
----------------|----------------|----------------|----------------|----------------|----------------
106-
${allClusters
107-
.map((formattedCluster) => {
108-
return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionStrings?.standardSrv || formattedCluster.connectionStrings?.standard || "N/A"}`;
109-
})
110-
.join("\n")}`
99+
JSON.stringify(allClusters)
111100
),
112101
};
113102
}

src/tools/atlas/read/listDBUsers.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { AtlasToolBase } from "../atlasTool.js";
33
import type { ToolArgs, OperationType } from "../../tool.js";
44
import { formatUntrustedData } from "../../tool.js";
5-
import type { DatabaseUserRole, UserScope } from "../../../common/atlas/openapi.js";
65
import { AtlasArgs } from "../../args.js";
76

87
export const ListDBUsersArgs = {
@@ -32,36 +31,26 @@ export class ListDBUsersTool extends AtlasToolBase {
3231
};
3332
}
3433

35-
const output =
36-
`Username | Roles | Scopes
37-
----------------|----------------|----------------
38-
` +
39-
data.results
40-
.map((user) => {
41-
return `${user.username} | ${formatRoles(user.roles)} | ${formatScopes(user.scopes)}`;
42-
})
43-
.join("\n");
34+
const users = data.results.map((user) => ({
35+
username: user.username,
36+
roles:
37+
user.roles?.map((role) => ({
38+
roleName: role.roleName,
39+
databaseName: role.databaseName,
40+
collectionName: role.collectionName,
41+
})) ?? [],
42+
scopes:
43+
user.scopes?.map((scope) => ({
44+
type: scope.type,
45+
name: scope.name,
46+
})) ?? [],
47+
}));
48+
4449
return {
45-
content: formatUntrustedData(`Found ${data.results.length} database users in project ${projectId}`, output),
50+
content: formatUntrustedData(
51+
`Found ${data.results.length} database users in project ${projectId}`,
52+
JSON.stringify(users)
53+
),
4654
};
4755
}
4856
}
49-
50-
function formatRoles(roles?: DatabaseUserRole[]): string {
51-
if (!roles?.length) {
52-
return "N/A";
53-
}
54-
return roles
55-
.map(
56-
(role) =>
57-
`${role.roleName}${role.databaseName ? `@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}` : ""}`
58-
)
59-
.join(", ");
60-
}
61-
62-
function formatScopes(scopes?: UserScope[]): string {
63-
if (!scopes?.length) {
64-
return "All";
65-
}
66-
return scopes.map((scope) => `${scope.type}:${scope.name}`).join(", ");
67-
}

0 commit comments

Comments
 (0)