Skip to content

Commit 4e7e28a

Browse files
committed
Merge remote-tracking branch 'origin/main' into v2
# Conflicts: # api-extractor/reports/mongodb-mcp-server.public.api.md # api-extractor/reports/tools.public.api.md # api-extractor/reports/web.public.api.md # packages/atlas-telemetry/src/types.ts # src/tools/atlas/tools.ts # src/web.ts
2 parents 4fc2a65 + 42d8167 commit 4e7e28a

12 files changed

Lines changed: 296 additions & 1 deletion

File tree

api-extractor/reports/web.public.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,14 @@ export interface AtlasClusterConnectionInfo {
8383
// (undocumented)
8484
expiryDate: Date;
8585
// (undocumented)
86+
instanceType: "FREE" | "FLEX" | "DEDICATED";
87+
// (undocumented)
8688
projectId: string;
8789
// (undocumented)
90+
provider?: string;
91+
// (undocumented)
92+
region?: string;
93+
// (undocumented)
8894
username: string;
8995
}
9096

knip.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ const config: KnipConfig = {
2525
"packages/types": {
2626
ignoreDependencies: ["@modelcontextprotocol/sdk", "mongodb-redact"],
2727
},
28+
"packages/tools-mongodb": {
29+
ignoreDependencies: [
30+
"@mongodb-js/device-id",
31+
"@mongodb-js/mcp-core",
32+
"@mongodb-js/mcp-logging",
33+
"@mongodb-js/mcp-types",
34+
"@mongosh/arg-parser",
35+
"@mongosh/service-provider-node-driver",
36+
"bson",
37+
"mongodb",
38+
"mongodb-build-info",
39+
"mongodb-connection-string-url",
40+
"mongodb-schema",
41+
"node-machine-id",
42+
"zod",
43+
],
44+
},
2845
"tests/browser": {
2946
entry: ["tests/**/*.ts", "polyfills/**/*.ts", "utils/**/*.ts", "vitest.config.ts", "setup.ts"],
3047
},

packages/atlas-api-client/src/apiClient.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Credentials, AuthProvider } from "./auth/authProvider.js";
1010
import { AuthProviderFactory } from "./auth/authProvider.js";
1111

1212
const ATLAS_API_VERSION = "2025-03-12";
13+
const LEGACY_ATLAS_API_VERSION = "2023-01-01";
1314
const DEFAULT_SEND_TIMEOUT_MS = 5_000;
1415

1516
/**
@@ -860,4 +861,78 @@ export class ApiClient implements IApiClient {
860861
}
861862
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
862863
// DO NOT EDIT. This is auto-generated code.
864+
865+
async upgradeSharedTierCluster(options: {
866+
groupId: string;
867+
body: {
868+
name: string;
869+
providerSettings: {
870+
providerName?: string;
871+
instanceSizeName: "FLEX" | "M10";
872+
backingProviderName?: string;
873+
regionName?: string;
874+
};
875+
};
876+
}): Promise<{ id?: string }> {
877+
const authHeaders = (await this.authProvider?.getAuthHeaders()) ?? {};
878+
const url = new URL(`api/atlas/v2/groups/${options.groupId}/clusters/tenantUpgrade`, this.options.baseUrl);
879+
const response = await this.customFetch(url.toString(), {
880+
method: "POST",
881+
signal: AbortSignal.timeout(DEFAULT_SEND_TIMEOUT_MS),
882+
headers: {
883+
...authHeaders,
884+
"Content-Type": `application/vnd.atlas.${LEGACY_ATLAS_API_VERSION}+json`,
885+
Accept: `application/vnd.atlas.${LEGACY_ATLAS_API_VERSION}+json`,
886+
"User-Agent": this.options.userAgent,
887+
},
888+
body: JSON.stringify(options.body),
889+
});
890+
if (!response.ok) {
891+
throw await ApiClientError.fromResponse(response);
892+
}
893+
return (await response.json()) as { id?: string };
894+
}
895+
896+
async upgradeFlexToDedicated(options: {
897+
groupId: string;
898+
body: {
899+
name: string;
900+
clusterType: "REPLICASET";
901+
replicationSpecs: Array<{
902+
regionConfigs: Array<{
903+
providerName?: string;
904+
regionName?: string;
905+
priority: number;
906+
electableSpecs: { instanceSize: string; nodeCount: number };
907+
}>;
908+
}>;
909+
autoScaling: {
910+
compute: {
911+
enabled: boolean;
912+
scaleDownEnabled: boolean;
913+
minInstanceSize: string;
914+
maxInstanceSize: string;
915+
};
916+
diskGBEnabled: boolean;
917+
};
918+
};
919+
}): Promise<{ id?: string }> {
920+
const authHeaders = (await this.authProvider?.getAuthHeaders()) ?? {};
921+
const url = new URL(`api/atlas/v2/groups/${options.groupId}/flexClusters:tenantUpgrade`, this.options.baseUrl);
922+
const response = await this.customFetch(url.toString(), {
923+
method: "POST",
924+
signal: AbortSignal.timeout(DEFAULT_SEND_TIMEOUT_MS),
925+
headers: {
926+
...authHeaders,
927+
"Content-Type": `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
928+
Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
929+
"User-Agent": this.options.userAgent,
930+
},
931+
body: JSON.stringify(options.body),
932+
});
933+
if (!response.ok) {
934+
throw await ApiClientError.fromResponse(response);
935+
}
936+
return (await response.json()) as { id?: string };
937+
}
863938
}

packages/tools-atlas/src/helpers/cluster.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface Cluster {
2121
name?: string;
2222
instanceType: "FREE" | "DEDICATED" | "FLEX";
2323
instanceSize?: string;
24+
provider?: string;
25+
region?: string;
2426
state?: "IDLE" | "CREATING" | "UPDATING" | "DELETING" | "REPAIRING";
2527
mongoDBVersion?: string;
2628
connectionStrings?: ClusterConnectionStrings;
@@ -32,6 +34,8 @@ export function formatFlexCluster(cluster: FlexClusterDescription20241113): Clus
3234
name: cluster.name,
3335
instanceType: "FLEX",
3436
instanceSize: undefined,
37+
provider: cluster.providerSettings?.backingProviderName,
38+
region: cluster.providerSettings?.regionName,
3539
state: cluster.stateName,
3640
mongoDBVersion: cluster.mongoDBVersion,
3741
connectionStrings: cluster.connectionStrings,
@@ -70,10 +74,19 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster {
7074
const instanceSize = regionConfigs[0]?.instanceSize ?? "UNKNOWN";
7175
const clusterInstanceType = instanceSize === "M0" ? "FREE" : "DEDICATED";
7276

77+
const primaryRegionConfig = cluster.replicationSpecs?.[0]?.regionConfigs?.[0] as
78+
| { backingProviderName?: string; providerName?: string; regionName?: string }
79+
| undefined;
80+
const provider =
81+
clusterInstanceType === "FREE" ? primaryRegionConfig?.backingProviderName : primaryRegionConfig?.providerName;
82+
const region = primaryRegionConfig?.regionName;
83+
7384
return {
7485
name: cluster.name,
7586
instanceType: clusterInstanceType,
7687
instanceSize: clusterInstanceType === "DEDICATED" ? instanceSize : undefined,
88+
provider,
89+
region,
7790
state: cluster.stateName,
7891
mongoDBVersion: cluster.mongoDBVersion,
7992
connectionStrings: cluster.connectionStrings,

packages/tools-atlas/src/tools/connect/connectCluster.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ export class ConnectClusterTool extends AtlasToolBase {
123123
username,
124124
projectId,
125125
clusterName,
126+
instanceType: cluster.instanceType,
127+
provider: cluster.provider,
128+
region: cluster.region,
126129
expiryDate,
127130
};
128131

packages/tools-mongodb/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
},
1313
"main": "./dist/index.js",
1414
"types": "./dist/index.d.ts",
15-
"files": ["dist"],
15+
"files": [
16+
"dist"
17+
],
1618
"scripts": {
1719
"compile": "tsc --build tsconfig.json"
1820
},

src/common/connectionInfo.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export interface AtlasClusterConnectionInfo {
2929
username: string;
3030
projectId: string;
3131
clusterName: string;
32+
instanceType: "FREE" | "FLEX" | "DEDICATED";
33+
provider?: string;
34+
region?: string;
3235
expiryDate: Date;
3336
}
3437

tests/integration/common/connectionManager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ describeWithMongoDB("Connection Manager", (integration) => {
142142
username: "",
143143
projectId: "",
144144
clusterName: "My Atlas Cluster",
145+
instanceType: "FREE" as const,
145146
expiryDate: new Date(),
146147
};
147148

tests/integration/tools/atlas/clusters.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,5 +265,75 @@ describeWithAtlas("clusters", (integration) => {
265265
});
266266
});
267267
});
268+
describe("atlas-upgrade-cluster", () => {
269+
it("should have correct metadata", async () => {
270+
const { tools } = await integration.mcpClient().listTools();
271+
const upgradeCluster = tools.find((tool) => tool.name === "atlas-upgrade-cluster");
272+
273+
expectDefined(upgradeCluster);
274+
expect(upgradeCluster.inputSchema.type).toBe("object");
275+
expectDefined(upgradeCluster.inputSchema.properties);
276+
expect(upgradeCluster.inputSchema.properties).toHaveProperty("projectId");
277+
expect(upgradeCluster.inputSchema.properties).toHaveProperty("clusterName");
278+
expect(upgradeCluster.inputSchema.properties).toHaveProperty("targetTier");
279+
expect(upgradeCluster.inputSchema.properties).toHaveProperty("provider");
280+
expect(upgradeCluster.inputSchema.properties).toHaveProperty("region");
281+
});
282+
283+
withCluster(integration, ({ getProjectId: getUpgradeProjectId, getClusterName: getUpgradeClusterName }) => {
284+
// This withCluster creates a dedicated FREE cluster for the upgrade test.
285+
// The test makes a real upgrade API call; withCluster's cleanup handles teardown regardless of tier.
286+
describe("when not connected to the cluster being upgraded", () => {
287+
it("upgrades FREE cluster to FLEX with explicit projectId and clusterName", async () => {
288+
const response = await integration.mcpClient().callTool({
289+
name: "atlas-upgrade-cluster",
290+
arguments: {
291+
projectId: getUpgradeProjectId(),
292+
clusterName: getUpgradeClusterName(),
293+
},
294+
});
295+
const content = getResponseContent(response.content);
296+
expect(content).toContain(getUpgradeClusterName());
297+
expect(content).toContain("being upgraded");
298+
});
299+
});
300+
});
301+
302+
// Simulates being "connected" to the outer cluster (created by atlas-create-free-cluster above).
303+
// The session state is patched so the tool picks up projectId/clusterName without explicit args,
304+
// then makes a real upgrade API call against that outer cluster.
305+
describe("when connected to the cluster being upgraded", () => {
306+
beforeAll(() => {
307+
const session: Session = integration.mcpServer().session;
308+
(session.connectionManager as unknown as { state: unknown }).state = {
309+
tag: "disconnected",
310+
connectedAtlasCluster: {
311+
username: "testuser",
312+
projectId: getProjectId(),
313+
clusterName,
314+
instanceType: "FREE" as const,
315+
provider: "AWS",
316+
region: "US_EAST_1",
317+
expiryDate: new Date(Date.now() + 3_600_000),
318+
},
319+
};
320+
});
321+
322+
afterAll(() => {
323+
const session: Session = integration.mcpServer().session;
324+
(session.connectionManager as unknown as { state: unknown }).state = { tag: "disconnected" };
325+
});
326+
327+
it("upgrades FREE cluster to FLEX using session state without explicit params", async () => {
328+
const response = await integration.mcpClient().callTool({
329+
name: "atlas-upgrade-cluster",
330+
arguments: {},
331+
});
332+
const content = getResponseContent(response.content);
333+
expect(content).toContain(clusterName);
334+
expect(content).toContain("being upgraded");
335+
});
336+
});
337+
});
268338
});
269339
});

tests/unit/common/apiClient.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,107 @@ describe("ApiClient", () => {
267267
await expect(apiClient.sendEvents({ events: mockEvents })).rejects.toThrow();
268268
});
269269
});
270+
271+
describe("upgradeSharedTierCluster", () => {
272+
const upgradeOptions = {
273+
groupId: "test-group-id",
274+
body: {
275+
name: "MyCluster",
276+
providerSettings: {
277+
providerName: "FLEX",
278+
instanceSizeName: "FLEX" as const,
279+
backingProviderName: "AWS",
280+
regionName: "US_EAST_1",
281+
},
282+
},
283+
};
284+
285+
it("should POST to the tenant upgrade endpoint with legacy API version headers", async () => {
286+
const mockCustomFetch = vi
287+
.spyOn(apiClient as unknown as { customFetch: typeof fetch }, "customFetch")
288+
.mockResolvedValue(new Response(JSON.stringify({ id: "upgraded-cluster-id" }), { status: 200 }));
289+
290+
const result = await apiClient.upgradeSharedTierCluster(upgradeOptions);
291+
292+
expect(mockCustomFetch).toHaveBeenCalledWith(
293+
"https://api.test.com/api/atlas/v2/groups/test-group-id/clusters/tenantUpgrade",
294+
expect.objectContaining({
295+
method: "POST",
296+
headers: {
297+
"Content-Type": "application/vnd.atlas.2023-01-01+json",
298+
Accept: "application/vnd.atlas.2023-01-01+json",
299+
Authorization: "Bearer mockToken",
300+
"User-Agent": "test-user-agent",
301+
},
302+
body: JSON.stringify(upgradeOptions.body),
303+
})
304+
);
305+
expect(result).toEqual({ id: "upgraded-cluster-id" });
306+
});
307+
308+
it("should throw when the response is not ok", async () => {
309+
vi.spyOn(apiClient as unknown as { customFetch: typeof fetch }, "customFetch").mockResolvedValue(
310+
new Response(JSON.stringify({ error: "Bad Request" }), { status: 400 })
311+
);
312+
313+
await expect(apiClient.upgradeSharedTierCluster(upgradeOptions)).rejects.toThrow();
314+
});
315+
});
316+
317+
describe("upgradeFlexToDedicated", () => {
318+
const upgradeOptions = {
319+
groupId: "test-group-id",
320+
body: {
321+
name: "MyCluster",
322+
clusterType: "REPLICASET" as const,
323+
replicationSpecs: [
324+
{
325+
regionConfigs: [
326+
{
327+
providerName: "AWS",
328+
regionName: "US_EAST_1",
329+
priority: 7,
330+
electableSpecs: { instanceSize: "M10", nodeCount: 3 },
331+
},
332+
],
333+
},
334+
],
335+
autoScaling: {
336+
compute: { enabled: true, scaleDownEnabled: true, minInstanceSize: "M10", maxInstanceSize: "M30" },
337+
diskGBEnabled: true,
338+
},
339+
},
340+
};
341+
342+
it("should POST to the flex tenant upgrade endpoint with current API version headers", async () => {
343+
const mockCustomFetch = vi
344+
.spyOn(apiClient as unknown as { customFetch: typeof fetch }, "customFetch")
345+
.mockResolvedValue(new Response(JSON.stringify({ id: "upgraded-cluster-id" }), { status: 200 }));
346+
347+
const result = await apiClient.upgradeFlexToDedicated(upgradeOptions);
348+
349+
expect(mockCustomFetch).toHaveBeenCalledWith(
350+
"https://api.test.com/api/atlas/v2/groups/test-group-id/flexClusters:tenantUpgrade",
351+
expect.objectContaining({
352+
method: "POST",
353+
headers: {
354+
"Content-Type": "application/vnd.atlas.2025-03-12+json",
355+
Accept: "application/vnd.atlas.2025-03-12+json",
356+
Authorization: "Bearer mockToken",
357+
"User-Agent": "test-user-agent",
358+
},
359+
body: JSON.stringify(upgradeOptions.body),
360+
})
361+
);
362+
expect(result).toEqual({ id: "upgraded-cluster-id" });
363+
});
364+
365+
it("should throw when the response is not ok", async () => {
366+
vi.spyOn(apiClient as unknown as { customFetch: typeof fetch }, "customFetch").mockResolvedValue(
367+
new Response(JSON.stringify({ error: "Bad Request" }), { status: 400 })
368+
);
369+
370+
await expect(apiClient.upgradeFlexToDedicated(upgradeOptions)).rejects.toThrow();
371+
});
372+
});
270373
});

0 commit comments

Comments
 (0)