Skip to content

Commit 4efb5cf

Browse files
committed
feat: add atlas-create-free-cluster atlas-inspect-cluster tools
1 parent a2237d2 commit 4efb5cf

File tree

15 files changed

+8379
-222
lines changed

15 files changed

+8379
-222
lines changed

package-lock.json

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

package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,32 @@
2727
"check": "npm run check:lint && npm run check:format",
2828
"check:lint": "eslint .",
2929
"check:format": "prettier -c .",
30-
"reformat": "prettier --write ."
30+
"reformat": "prettier --write .",
31+
"generate:download": "curl -Lo ./scripts/spec.json https://github.com/mongodb/openapi/raw/refs/heads/main/openapi/v2/openapi-2025-03-12.json",
32+
"generate:filter": "tsx ./scripts/filter.ts > ./scripts/filteredSpec.json < ./scripts/spec.json",
33+
"generate:bundle": "redocly bundle --ext json --remove-unused-components ./scripts/filteredSpec.json --output ./scripts/bundledSpec.json",
34+
"generate:openapi": "openapi-typescript ./scripts/bundledSpec.json --root-types-no-schema-prefix --root-types --output ./src/common/atlas/openapi.d.ts",
35+
"generate:clear": "rm -rf ./scripts/bundledSpec.json ./scripts/filteredSpec.json ./scripts/spec.json",
36+
"generate": "npm run generate:download && npm run generate:filter && npm run generate:bundle && npm run generate:openapi && npm run generate:clear"
3137
},
3238
"license": "Apache-2.0",
3339
"devDependencies": {
3440
"@eslint/js": "^9.24.0",
3541
"@modelcontextprotocol/inspector": "^0.8.2",
3642
"@modelcontextprotocol/sdk": "^1.8.0",
43+
"@redocly/cli": "^1.34.2",
3744
"@types/node": "^22.14.0",
3845
"@types/simple-oauth2": "^5.0.7",
3946
"eslint": "^9.24.0",
4047
"eslint-config-prettier": "^10.1.1",
4148
"globals": "^16.0.0",
49+
"openapi-types": "^12.1.3",
50+
"openapi-typescript": "^7.6.1",
4251
"prettier": "^3.5.3",
52+
"tsx": "^4.19.3",
4353
"typescript": "^5.8.2",
44-
"typescript-eslint": "^8.29.1"
54+
"typescript-eslint": "^8.29.1",
55+
"yaml": "^2.7.1"
4556
},
4657
"dependencies": {
4758
"@mongodb-js/devtools-connect": "^3.7.2",

scripts/filter.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { OpenAPIV3_1 } from "openapi-types";
2+
3+
async function readStdin() {
4+
return new Promise<string>((resolve, reject) => {
5+
let data = "";
6+
process.stdin.setEncoding("utf8");
7+
process.stdin.on("error", (err) => {
8+
reject(err);
9+
});
10+
process.stdin.on("data", (chunk) => {
11+
data += chunk;
12+
});
13+
process.stdin.on("end", () => {
14+
resolve(data);
15+
});
16+
});
17+
}
18+
19+
function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
20+
const allowedOperations = [
21+
"listProjects",
22+
"getProject",
23+
"createProject",
24+
"listClusters",
25+
"createCluster",
26+
"listClustersForAllProjects"
27+
];
28+
29+
const filteredPaths = {};
30+
31+
for (const path in openapi.paths) {
32+
const filteredMethods = {} as OpenAPIV3_1.PathItemObject;
33+
for (const method in openapi.paths[path]) {
34+
if (allowedOperations.includes(openapi.paths[path][method].operationId)) {
35+
filteredMethods[method] = openapi.paths[path][method];
36+
}
37+
}
38+
if (Object.keys(filteredMethods).length > 0) {
39+
filteredPaths[path] = filteredMethods;
40+
}
41+
}
42+
43+
return {...openapi, paths: filteredPaths};
44+
}
45+
46+
async function main() {
47+
const openapiText = await readStdin();
48+
const openapi = JSON.parse(openapiText) as OpenAPIV3_1.Document;
49+
const filteredOpenapi = filterOpenapi(openapi);
50+
console.log(JSON.stringify(filteredOpenapi));
51+
}
52+
53+
main()
54+
.catch((error) => {
55+
console.error("Error:", error);
56+
process.exit(1);
57+
});

src/common/atlas/auth.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ApiClient } from "./client";
2+
import { State } from "../../state";
3+
4+
export async function ensureAuthenticated(state: State, apiClient: ApiClient): Promise<void> {
5+
if (!(await isAuthenticated(state, apiClient))) {
6+
throw new Error("Not authenticated");
7+
}
8+
}
9+
10+
export async function isAuthenticated(state: State, apiClient: ApiClient): Promise<boolean> {
11+
switch (state.auth.status) {
12+
case "not_auth":
13+
return false;
14+
case "requested":
15+
try {
16+
if (!state.auth.code) {
17+
return false;
18+
}
19+
await apiClient.retrieveToken(state.auth.code.device_code);
20+
return !!state.auth.token;
21+
} catch {
22+
return false;
23+
}
24+
case "issued":
25+
if (!state.auth.token) {
26+
return false;
27+
}
28+
return await apiClient.validateToken();
29+
default:
30+
throw new Error("Unknown authentication status");
31+
}
32+
}

src/client.ts renamed to src/common/atlas/client.ts

Lines changed: 30 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import config from "./config.js";
1+
import { log } from "console";
2+
import config from "../../config.js";
3+
4+
import { Group, PaginatedOrgGroupView, PaginatedAtlasGroupView, ClusterDescription20240805, PaginatedClusterDescription20240805 } from "./openapi.js"
25

36
export interface OAuthToken {
47
access_token: string;
@@ -10,27 +13,6 @@ export interface OAuthToken {
1013
expiry: Date;
1114
}
1215

13-
export interface AtlasProject {
14-
id: string;
15-
name: string;
16-
created?: {
17-
$date: string;
18-
};
19-
}
20-
21-
export interface AtlasCluster {
22-
id?: string;
23-
name: string;
24-
stateName: string;
25-
mongoDBVersion?: string;
26-
providerSettings?: {
27-
regionName?: string;
28-
};
29-
connectionStrings?: {
30-
standard?: string;
31-
};
32-
}
33-
3416
export interface OauthDeviceCode {
3517
user_code: string;
3618
verification_uri: string;
@@ -39,11 +21,6 @@ export interface OauthDeviceCode {
3921
interval: string;
4022
}
4123

42-
export interface AtlasResponse<T> {
43-
results: T[];
44-
totalCount?: number;
45-
}
46-
4724
export type saveTokenFunction = (token: OAuthToken) => void | Promise<void>;
4825

4926
export class ApiClientError extends Error {
@@ -120,10 +97,13 @@ export class ApiClient {
12097
...options?.headers,
12198
},
12299
};
100+
101+
console.error(`Calling Atlas API: ${url.toString()}`);
102+
console.error(`with: ${JSON.stringify(opt)}`);
123103
const response = await fetch(url, opt);
124104

125105
if (!response.ok) {
126-
throw new ApiClientError(`Error calling Atlas API: ${response.statusText}`, response);
106+
throw new ApiClientError(`Error calling Atlas API: ${await response.text()}`, response);
127107
}
128108

129109
return (await response.json()) as T;
@@ -282,31 +262,33 @@ export class ApiClient {
282262
}
283263
}
284264

285-
/**
286-
* Get all projects for the authenticated user
287-
*/
288-
async listProjects(): Promise<AtlasResponse<AtlasProject>> {
289-
return await this.do<AtlasResponse<AtlasProject>>("/groups");
265+
async listProjects(): Promise<PaginatedAtlasGroupView> {
266+
return await this.do<PaginatedAtlasGroupView>("/groups");
290267
}
291268

292-
/**
293-
* Get a specific project by ID
294-
*/
295-
async getProject(projectId: string): Promise<AtlasProject> {
296-
return await this.do<AtlasProject>(`/groups/${projectId}`);
269+
async getProject(groupId: string): Promise<Group> {
270+
return await this.do<Group>(`/groups/${groupId}`);
297271
}
298272

299-
/**
300-
* Get clusters for a specific project
301-
*/
302-
async listProjectClusters(projectId: string): Promise<AtlasResponse<AtlasCluster>> {
303-
return await this.do<AtlasResponse<AtlasCluster>>(`/groups/${projectId}/clusters`);
273+
async listClusters(groupId: string): Promise<PaginatedClusterDescription20240805> {
274+
return await this.do<PaginatedClusterDescription20240805>(`/groups/${groupId}/clusters`);
304275
}
305276

306-
/**
307-
* Get clusters for a specific project
308-
*/
309-
async listAllClusters(): Promise<AtlasResponse<AtlasCluster>> {
310-
return await this.do<AtlasResponse<AtlasCluster>>(`/clusters`);
277+
async listClustersForAllProjects(): Promise<PaginatedOrgGroupView> {
278+
return await this.do<PaginatedOrgGroupView>(`/clusters`);
279+
}
280+
281+
async getCluster(groupId: string, clusterName: string): Promise<ClusterDescription20240805> {
282+
return await this.do<ClusterDescription20240805>(`/groups/${groupId}/clusters/${clusterName}`);
283+
}
284+
285+
async createCluster(groupId: string, cluster: ClusterDescription20240805): Promise<ClusterDescription20240805> {
286+
if (!cluster.groupId) {
287+
throw new Error("Cluster groupId is required");
288+
}
289+
return await this.do<ClusterDescription20240805>(`/groups/${groupId}/clusters`, {
290+
method: "POST",
291+
body: JSON.stringify(cluster),
292+
});
311293
}
312294
}

0 commit comments

Comments
 (0)