Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bd69450
Add base atlas performance advisor MCP server tool
kylelai1 Sep 4, 2025
f331bac
Cleanup comments
kylelai1 Sep 4, 2025
78f9888
Merge main
kylelai1 Sep 4, 2025
2f023eb
Fix API return type
kylelai1 Sep 5, 2025
a4cbc8b
Clean up getting slow query logs from atlas admin api
kylelai1 Sep 8, 2025
2ef0ded
Fix types for performance advisor api response
kylelai1 Sep 8, 2025
923263e
Address comments
kylelai1 Sep 13, 2025
8272719
Move utils to util file
kylelai1 Sep 13, 2025
5871dc0
Cleanup naming
kylelai1 Sep 13, 2025
ce0466b
Remove processId arg
kylelai1 Sep 16, 2025
f0b24bb
Address comments
kylelai1 Sep 17, 2025
3217e95
Typing
kylelai1 Sep 18, 2025
016af0e
Clean up PA retrieval code
kylelai1 Sep 18, 2025
a2fbe34
Merge branch 'main' into atlas-list-performance-advisor-base-tool
kylelai1 Sep 19, 2025
4645d07
Clean up nits
kylelai1 Sep 19, 2025
d6b7db8
Merge branch 'atlas-list-performance-advisor-tool' into atlas-list-pe…
kylelai1 Sep 19, 2025
9dbdca1
Use promise.resolve for an undefined value in a promise.all
kylelai1 Sep 19, 2025
5da6e8d
Add tests
kylelai1 Sep 23, 2025
63a94e1
merge base branch
kylelai1 Sep 23, 2025
93d9757
Retrieve process ID from standard connection string
kylelai1 Sep 24, 2025
4869b78
Cleanup
kylelai1 Sep 24, 2025
53c262b
fix type error
kylelai1 Sep 24, 2025
095256c
Type fixes
kylelai1 Sep 24, 2025
9d56da0
Address comments
kylelai1 Sep 25, 2025
400bff1
handle 402 error for payment
kylelai1 Sep 25, 2025
35b695d
Cleanup
kylelai1 Sep 26, 2025
3e73b21
Limit max num of slow queries returned by the tool
kylelai1 Sep 29, 2025
3efe310
Use promise.allSettled for slow query promises
kylelai1 Sep 30, 2025
3b7112d
Set sampled slow query logs limit to 50
kylelai1 Sep 30, 2025
f5c1d8d
Product feedback
kylelai1 Oct 1, 2025
7b4922f
Fix since param
kylelai1 Oct 1, 2025
8352d18
Use promise.allSettled
kylelai1 Oct 1, 2025
26ccdfc
Address comments
kylelai1 Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions src/common/atlas/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,45 @@ import type { ApiClient } from "./apiClient.js";
import { LogId } from "../logger.js";

const DEFAULT_PORT = "27017";

function extractProcessIds(connectionString: string): Array<string> {
if (!connectionString || connectionString === "") return [];

// Extract host:port pairs from connection string
const matches = connectionString.match(/^mongodb:\/\/([^/]+)/);
if (!matches) {
return [];
}

// matches[1] gives us the host:port pairs
const hostsString = matches[1];
const hosts = hostsString?.split(",") ?? [];

return hosts?.map((host) => {
const [hostname, port] = host.split(":");
return `${hostname}:${port || DEFAULT_PORT}`;
});
}
export interface Cluster {
name?: string;
instanceType: "FREE" | "DEDICATED" | "FLEX";
instanceSize?: string;
state?: "IDLE" | "CREATING" | "UPDATING" | "DELETING" | "REPAIRING";
mongoDBVersion?: string;
connectionString?: string;
processIds?: Array<string>;
}

export function formatFlexCluster(cluster: FlexClusterDescription20241113): Cluster {
const connectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard;
return {
name: cluster.name,
instanceType: "FLEX",
instanceSize: undefined,
state: cluster.stateName,
mongoDBVersion: cluster.mongoDBVersion,
connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard,
connectionString,
processIds: extractProcessIds(cluster.connectionStrings?.standard ?? ""),
};
}

Expand Down Expand Up @@ -53,14 +75,16 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster {

const instanceSize = regionConfigs[0]?.instanceSize ?? "UNKNOWN";
const clusterInstanceType = instanceSize === "M0" ? "FREE" : "DEDICATED";
const connectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard;

return {
name: cluster.name,
instanceType: clusterInstanceType,
instanceSize: clusterInstanceType === "DEDICATED" ? instanceSize : undefined,
state: cluster.stateName,
mongoDBVersion: cluster.mongoDBVersion,
connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard,
connectionString,
processIds: extractProcessIds(cluster.connectionStrings?.standard ?? ""),
};
}

Expand Down Expand Up @@ -98,21 +122,17 @@ export async function inspectCluster(apiClient: ApiClient, projectId: string, cl
}
}

export async function getProcessIdFromCluster(
export async function getProcessIdsFromCluster(
apiClient: ApiClient,
projectId: string,
clusterName: string
): Promise<string> {
): Promise<Array<string>> {
try {
const cluster = await inspectCluster(apiClient, projectId, clusterName);
if (!cluster.connectionString) {
throw new Error("No connection string available for cluster");
}
const url = new URL(cluster.connectionString);
return `${url.hostname}:${url.port || DEFAULT_PORT}`;
return cluster.processIds || [];
} catch (error) {
throw new Error(
`Failed to get processId from cluster: ${error instanceof Error ? error.message : String(error)}`
`Failed to get processIds from cluster: ${error instanceof Error ? error.message : String(error)}`
);
}
}
40 changes: 26 additions & 14 deletions src/common/atlas/performanceAdvisorUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LogId } from "../logger.js";
import type { ApiClient } from "./apiClient.js";
import { getProcessIdFromCluster } from "./cluster.js";
import { getProcessIdsFromCluster } from "./cluster.js";
import type { components } from "./openapi.js";

export type SuggestedIndex = components["schemas"]["PerformanceAdvisorIndex"];
Expand Down Expand Up @@ -112,22 +112,34 @@ export async function getSlowQueries(
namespaces?: Array<string>
): Promise<{ slowQueryLogs: Array<SlowQueryLog> }> {
try {
const processId = await getProcessIdFromCluster(apiClient, projectId, clusterName);
const processIds = await getProcessIdsFromCluster(apiClient, projectId, clusterName);

const response = await apiClient.listSlowQueries({
params: {
path: {
groupId: projectId,
processId,
},
query: {
...(since && { since: since.getTime() }),
...(namespaces && { namespaces: namespaces }),
if (processIds.length === 0) {
return { slowQueryLogs: [] };
}

const slowQueryPromises = processIds.map((processId) =>
apiClient.listSlowQueries({
params: {
path: {
groupId: projectId,
processId,
},
query: {
...(since && { since: since.getTime() }),
...(namespaces && { namespaces: namespaces }),
},
},
},
});
})
);

const responses = await Promise.all(slowQueryPromises);

const allSlowQueryLogs = responses.reduce((acc, response) => {
return acc.concat(response.slowQueries ?? []);
}, [] as Array<SlowQueryLog>);

return { slowQueryLogs: response.slowQueries ?? [] };
return { slowQueryLogs: allSlowQueryLogs };
} catch (err) {
apiClient.logger.debug({
id: LogId.atlasPaSlowQueryLogsFailure,
Expand Down
27 changes: 15 additions & 12 deletions src/tools/atlas/read/getPerformanceAdvisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,22 @@ export class GetPerformanceAdvisorTool extends AtlasToolBase {
: Promise.resolve(undefined),
]);

const hasSuggestedIndexes = suggestedIndexesResult && suggestedIndexesResult?.suggestedIndexes?.length > 0;
const hasDropIndexSuggestions =
dropIndexSuggestionsResult &&
dropIndexSuggestionsResult?.hiddenIndexes?.length > 0 &&
dropIndexSuggestionsResult?.redundantIndexes?.length > 0 &&
dropIndexSuggestionsResult?.unusedIndexes?.length > 0;
const hasSlowQueryLogs = slowQueryLogsResult && slowQueryLogsResult?.slowQueryLogs?.length > 0;
const hasSchemaSuggestions =
schemaSuggestionsResult && schemaSuggestionsResult?.recommendations?.length > 0;

// Inserts the performance advisor data with the relevant section header if it exists
const performanceAdvisorData = [
suggestedIndexesResult && suggestedIndexesResult?.suggestedIndexes?.length > 0
? `## Suggested Indexes\n${JSON.stringify(suggestedIndexesResult.suggestedIndexes)}`
: "No suggested indexes found.",
dropIndexSuggestionsResult
? `## Drop Index Suggestions\n${JSON.stringify(dropIndexSuggestionsResult)}`
: "No drop index suggestions found.",
slowQueryLogsResult && slowQueryLogsResult?.slowQueryLogs?.length > 0
? `## Slow Query Logs\n${JSON.stringify(slowQueryLogsResult.slowQueryLogs)}`
: "No slow query logs found.",
schemaSuggestionsResult && schemaSuggestionsResult?.recommendations?.length > 0
? `## Schema Suggestions\n${JSON.stringify(schemaSuggestionsResult.recommendations)}`
: "No schema suggestions found.",
`## Suggested Indexes\n${hasSuggestedIndexes ? JSON.stringify(suggestedIndexesResult.suggestedIndexes) : "No suggested indexes found."}`,
`## Drop Index Suggestions\n${hasDropIndexSuggestions ? JSON.stringify(dropIndexSuggestionsResult) : "No drop index suggestions found."}`,
`## Slow Query Logs\n${hasSlowQueryLogs ? JSON.stringify(slowQueryLogsResult.slowQueryLogs) : "No slow query logs found."}`,
`## Schema Suggestions\n${hasSchemaSuggestions ? JSON.stringify(schemaSuggestionsResult.recommendations) : "No schema suggestions found."}`,
];

if (performanceAdvisorData.length === 0) {
Expand Down
61 changes: 60 additions & 1 deletion tests/integration/tools/atlas/atlasHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ObjectId } from "mongodb";
import type { Group } from "../../../../src/common/atlas/openapi.js";
import type { ClusterDescription20240805, Group } from "../../../../src/common/atlas/openapi.js";
import type { ApiClient } from "../../../../src/common/atlas/apiClient.js";
import type { IntegrationTest } from "../../helpers.js";
import { setupIntegrationTest, defaultTestConfig, defaultDriverOptions } from "../../helpers.js";
import type { SuiteCollector } from "vitest";
import { afterAll, beforeAll, describe } from "vitest";
import type { Session } from "../../../../src/common/session.js";

export type IntegrationTestFunction = (integration: IntegrationTest) => void;

Expand Down Expand Up @@ -143,3 +144,61 @@ async function createProject(apiClient: ApiClient): Promise<Group & Required<Pic

return group as Group & Required<Pick<Group, "id">>;
}

export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function deleteAndWaitCluster(
session: Session,
projectId: string,
clusterName: string,
pollingInterval: number = 1000
): Promise<void> {
await session.apiClient.deleteCluster({
params: {
path: {
groupId: projectId,
clusterName,
},
},
});
while (true) {
try {
await session.apiClient.getCluster({
params: {
path: {
groupId: projectId,
clusterName,
},
},
});
await sleep(pollingInterval);
} catch {
break;
}
}
}

export async function waitCluster(
session: Session,
projectId: string,
clusterName: string,
check: (cluster: ClusterDescription20240805) => boolean | Promise<boolean>,
pollingInterval: number = 1000
): Promise<void> {
while (true) {
const cluster = await session.apiClient.getCluster({
params: {
path: {
groupId: projectId,
clusterName,
},
},
});
if (await check(cluster)) {
return;
}
await sleep(pollingInterval);
}
}
63 changes: 9 additions & 54 deletions tests/integration/tools/atlas/clusters.test.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,16 @@
import type { Session } from "../../../../src/common/session.js";
import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js";
import { describeWithAtlas, withProject, randomId, parseTable } from "./atlasHelpers.js";
import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js";
import {
describeWithAtlas,
withProject,
randomId,
parseTable,
deleteAndWaitCluster,
waitCluster,
sleep,
} from "./atlasHelpers.js";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string): Promise<void> {
await session.apiClient.deleteCluster({
params: {
path: {
groupId: projectId,
clusterName,
},
},
});
while (true) {
try {
await session.apiClient.getCluster({
params: {
path: {
groupId: projectId,
clusterName,
},
},
});
await sleep(1000);
} catch {
break;
}
}
}

async function waitCluster(
session: Session,
projectId: string,
clusterName: string,
check: (cluster: ClusterDescription20240805) => boolean | Promise<boolean>
): Promise<void> {
while (true) {
const cluster = await session.apiClient.getCluster({
params: {
path: {
groupId: projectId,
clusterName,
},
},
});
if (await check(cluster)) {
return;
}
await sleep(1000);
}
}

describeWithAtlas("clusters", (integration) => {
withProject(integration, ({ getProjectId, getIpAddress }) => {
const clusterName = "ClusterTest-" + randomId;
Expand Down
Loading
Loading