diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..9eb4595e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +coverage +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..b52f3251 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,23 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": true, + "singleQuote": false, + "printWidth": 120, + "overrides": [ + { + "files": "*.json", + "options": { + "tabWidth": 2, + "printWidth": 80 + } + }, + { + "files": "*.md", + "options": { + "tabWidth": 2, + "printWidth": 80 + } + } + ] +} diff --git a/FEATURES.md b/FEATURES.md index 6a77ba1e..83ac6a89 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,10 +1,10 @@ ## Features -- [x] Login via OAuth authentication (device code) +- [x] Login via OAuth authentication (device code) - [ ] Register via OAuth authentication (device code) -- [x] List clusters -- [ ] Create M0 cluster +- [x] List clusters +- [ ] Create M0 cluster - [ ] Create a DBUser - [ ] Delete a DBUser - [ ] Connect to a cluster -- [ ] Emit telemetry events on MCP usage \ No newline at end of file +- [ ] Emit telemetry events on MCP usage diff --git a/README.md b/README.md index 1c6e28bf..72c4d71b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ <<<<<<< HEAD + # atlas-mcp-server -TBD -======= +# TBD + # Atlas MCP Server PoC A Model Context Protocol server for interacting with MongoDB Atlas. -Developed using the official MCP SDK https://github.com/modelcontextprotocol/typescript-sdk +Developed using the official MCP SDK https://github.com/modelcontextprotocol/typescript-sdk ## 📚 Table of Contents + - [🚀 Getting Started](#getting-started) - [Prerequisites](#prerequisites) - [Installation](#installation) @@ -27,6 +29,7 @@ Developed using the official MCP SDK https://github.com/modelcontextprotocol/typ ## 🚀 Getting Started ### Prerequisites + - Node.js installed - MongoDB Atlas account @@ -45,12 +48,14 @@ npm run build ## 🔧 Troubleshooting ### Restart Server + - Run `npm run build` to re-build the server if you made changes to the code - Press `Cmd + Shift + P` and type List MCP Servers - Select the MCP server you want to restart - Select the option to restart the server ### View Logs + To see MCP logs, check https://code.visualstudio.com/docs/copilot/chat/mcp-servers. - Press `Cmd + Shift + P` and type List MCP Servers @@ -61,12 +66,14 @@ To see MCP logs, check https://code.visualstudio.com/docs/copilot/chat/mcp-serve We can use @modelcontextprotocol/inspector to debug the server - https://github.com/modelcontextprotocol/inspector -From the root of this repository, run: +From the root of this repository, run: + ```shell npx @modelcontextprotocol/inspector -- node dist/index.js ``` Or use the npm script: + ```shell npm run inspect ``` @@ -74,6 +81,7 @@ npm run inspect ## 🛠️ Supported Tools ### Tool List + - `auth` - Authenticate to MongoDB Atlas - `list-clusters` - Lists MongoDB Atlas clusters - `list-projects` - Lists MongoDB Atlas projects @@ -82,11 +90,12 @@ npm run inspect ### VSCode -Prerequisites: +Prerequisites: + - Use VSCode Insiders (https://code.visualstudio.com/insiders/) - Setup copilot in VSCode Insiders -Step 1: Add the mcp server to VSCode configuration +Step 1: Add the mcp server to VSCode configuration - Press `Cmd + Shift + P` and type `MCP: Add MCP Server` and select it. - Select the first option for a local MCP server. @@ -94,8 +103,9 @@ Step 1: Add the mcp server to VSCode configuration Step 2: Verify the created mcp file -It should look like this -```shell +It should look like this + +```json { "servers": { "demo-atlas-server": { @@ -107,29 +117,31 @@ It should look like this } ``` -Step 3: Open the copilot chat and check that the toolbox icon is visible and has the mcp server listed. +Step 3: Open the copilot chat and check that the toolbox icon is visible and has the mcp server listed. Step 4: Try running a command - Can you list my clusters? - ### Claude Step 1: Install claude and login + ```shell brew install claude ``` Step 2: Create a configuration file for your MCP server - + Open the file -``` + +```shell code ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` Paste the mcp server configuration into the file -``` + +```json { "mcpServers": { "Demo": { @@ -139,9 +151,8 @@ Paste the mcp server configuration into the file } ``` -Step 3: Launch Claude Desktop and click on the hammer icon, the Demo MCP server should be detected. Type in the chat "show me a demo of MCP" and allow the tool to get access. -- Detailed instructions with screenshots can be found in this [document](https://docs.google.com/document/d/1_C8QBMZ5rwImV_9v4G96661OqcBk1n1SfEgKyNalv9c/edit?tab=t.2hhewstzj7ck#bookmark=id.nktw0lg0fn7t). +Step 3: Launch Claude Desktop and click on the hammer icon, the Demo MCP server should be detected. Type in the chat "show me a demo of MCP" and allow the tool to get access. +- Detailed instructions with screenshots can be found in this [document](https://docs.google.com/document/d/1_C8QBMZ5rwImV_9v4G96661OqcBk1n1SfEgKyNalv9c/edit?tab=t.2hhewstzj7ck#bookmark=id.nktw0lg0fn7t). Note: If you make changes to your MCP server code, rebuild the project with `npm run build` and restart the server and Claude Desktop. ->>>>>>> 1599834 (chore: adds docs written by filipe into readme) diff --git a/dist/client.js b/dist/client.js index 51627fbb..6b589aa6 100644 --- a/dist/client.js +++ b/dist/client.js @@ -1,5 +1,4 @@ import config from "./config.js"; -; export class ApiClientError extends Error { constructor(message, response = undefined) { super(message); @@ -14,18 +13,20 @@ export class ApiClient { this.saveToken = saveToken; } defaultOptions() { - const authHeaders = (!this.token?.access_token) ? null : { - "Authorization": `Bearer ${this.token.access_token}` - }; + const authHeaders = !this.token?.access_token + ? null + : { + Authorization: `Bearer ${this.token.access_token}`, + }; return { method: "GET", - credentials: (!this.token?.access_token) ? undefined : "include", + credentials: !this.token?.access_token ? undefined : "include", headers: { "Content-Type": "application/json", - "Accept": "application/vnd.atlas.2025-04-07+json", + Accept: "application/vnd.atlas.2025-04-07+json", "User-Agent": `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, - ...authHeaders - } + ...authHeaders, + }, }; } async storeToken(token) { @@ -50,13 +51,13 @@ export class ApiClient { headers: { ...defaultOpt.headers, ...options?.headers, - } + }, }; const response = await fetch(url, opt); if (!response.ok) { throw new ApiClientError(`Error calling Atlas API: ${response.statusText}`, response); } - return await response.json(); + return (await response.json()); } async authenticate() { const endpoint = "api/private/unauth/account/device/authorize"; @@ -65,7 +66,7 @@ export class ApiClient { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + Accept: "application/json", }, body: new URLSearchParams({ client_id: config.clientID, @@ -76,7 +77,7 @@ export class ApiClient { if (!response.ok) { throw new ApiClientError(`Failed to initiate authentication: ${response.statusText}`, response); } - return await response.json(); + return (await response.json()); } async retrieveToken(device_code) { const endpoint = "api/private/unauth/account/device/token"; @@ -94,7 +95,7 @@ export class ApiClient { }); if (response.ok) { const tokenData = await response.json(); - const buf = Buffer.from(tokenData.access_token.split('.')[1], 'base64').toString(); + const buf = Buffer.from(tokenData.access_token.split(".")[1], "base64").toString(); const jwt = JSON.parse(buf); const expiry = new Date(jwt.exp * 1000); return await this.storeToken({ ...tokenData, expiry }); @@ -122,7 +123,7 @@ export class ApiClient { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + Accept: "application/json", }, body: new URLSearchParams({ client_id: config.clientID, @@ -135,7 +136,7 @@ export class ApiClient { throw new ApiClientError(`Failed to refresh token: ${response.statusText}`, response); } const data = await response.json(); - const buf = Buffer.from(data.access_token.split('.')[1], 'base64').toString(); + const buf = Buffer.from(data.access_token.split(".")[1], "base64").toString(); const jwt = JSON.parse(buf); const expiry = new Date(jwt.exp * 1000); const tokenToStore = { @@ -151,7 +152,7 @@ export class ApiClient { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + Accept: "application/json", "User-Agent": `AtlasMCP/${process.env.VERSION} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, }, body: new URLSearchParams({ @@ -201,7 +202,7 @@ export class ApiClient { * Get all projects for the authenticated user */ async listProjects() { - return await this.do('/groups'); + return await this.do("/groups"); } /** * Get a specific project by ID diff --git a/dist/server.js b/dist/server.js index fb661ece..8963ca2a 100644 --- a/dist/server.js +++ b/dist/server.js @@ -27,7 +27,7 @@ export class Server { this.state.auth.token = token; this.state.auth.status = "issued"; saveState(this.state); - } + }, }); this.initiated = true; } @@ -71,7 +71,10 @@ export class Server { await saveState(this.state); return { content: [ - { type: "text", text: `Please authenticate by visiting ${code.verification_uri} and entering the code ${code.user_code}` }, + { + type: "text", + text: `Please authenticate by visiting ${code.verification_uri} and entering the code ${code.user_code}`, + }, ], }; } @@ -118,17 +121,19 @@ export class Server { } if (clusters.length === 0) { return { - content: [{ + content: [ + { type: "text", - text: "No clusters found. You may need to create a cluster in your MongoDB Atlas account." - }], + text: "No clusters found. You may need to create a cluster in your MongoDB Atlas account.", + }, + ], }; } const formattedClusters = formatClustersTable(clusters); return { content: [ { type: "text", text: introText }, - { type: "text", text: formattedClusters } + { type: "text", text: formattedClusters }, ], }; } @@ -139,15 +144,17 @@ export class Server { return { content: [ { type: "text", text: "You need to authenticate before listing clusters." }, - { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." } + { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." }, ], }; } return { - content: [{ + content: [ + { type: "text", - text: `Error listing clusters: ${error instanceof Error ? error.message : String(error)}` - }], + text: `Error listing clusters: ${error instanceof Error ? error.message : String(error)}`, + }, + ], }; } } @@ -170,15 +177,17 @@ export class Server { // Format projects as a table const header = `Project Name | Project ID | Created At ----------------|----------------|----------------`; - const rows = projects.map((project) => { - const createdAt = project.created ? new Date(project.created.$date).toLocaleString() : 'N/A'; + const rows = projects + .map((project) => { + const createdAt = project.created ? new Date(project.created.$date).toLocaleString() : "N/A"; return `${project.name} | ${project.id} | ${createdAt}`; - }).join("\n"); + }) + .join("\n"); const formattedProjects = `${header}\n${rows}`; return { content: [ { type: "text", text: "Here are your MongoDB Atlas projects:" }, - { type: "text", text: formattedProjects } + { type: "text", text: formattedProjects }, ], }; } @@ -189,15 +198,17 @@ export class Server { return { content: [ { type: "text", text: "You need to authenticate before listing projects." }, - { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." } + { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." }, ], }; } return { - content: [{ + content: [ + { type: "text", - text: `Error listing projects: ${error instanceof Error ? error.message : String(error)}` - }], + text: `Error listing projects: ${error instanceof Error ? error.message : String(error)}`, + }, + ], }; } } @@ -223,18 +234,19 @@ export class Server { await server.connect(transport); } } -; function formatClustersTable(clusters) { if (clusters.length === 0) { return "No clusters found."; } const header = `Cluster Name | State | MongoDB Version | Region | Connection String ----------------|----------------|----------------|----------------|----------------|----------------`; - const rows = clusters.map(cluster => { - const region = cluster.providerSettings?.regionName || 'N/A'; - const connectionString = cluster.connectionStrings?.standard || 'N/A'; - const mongoDBVersion = cluster.mongoDBVersion || 'N/A'; + const rows = clusters + .map((cluster) => { + const region = cluster.providerSettings?.regionName || "N/A"; + const connectionString = cluster.connectionStrings?.standard || "N/A"; + const mongoDBVersion = cluster.mongoDBVersion || "N/A"; return `${cluster.name} | ${cluster.stateName} | ${mongoDBVersion} | ${region} | ${connectionString}`; - }).join("\n"); + }) + .join("\n"); return `${header}\n${rows}`; } diff --git a/package-lock.json b/package-lock.json index fe9312df..5af6bf8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,12 @@ "@modelcontextprotocol/sdk": "^1.8.0", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", + "prettier": "^3.5.3", "typescript": "^5.8.2", "zod": "^3.24.2" + }, + "engines": { + "node": ">=23.0.0" } }, "node_modules/@modelcontextprotocol/sdk": { @@ -830,6 +834,22 @@ "node": ">=16.20.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index f4ab3cf1..a92d15c6 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,16 @@ "build:addshebang": "echo '#!/usr/bin/env node' > dist/index2.js && cat dist/index.js >> dist/index2.js && mv dist/index2.js dist/index.js", "build:chmod": "chmod +x dist/index.js", "build": "npm run build:clean && npm run build:compile && npm run build:addshebang && npm run build:chmod", - "inspect": "npm run build && npx @modelcontextprotocol/inspector -- dist/index.js" + "inspect": "npm run build && npx @modelcontextprotocol/inspector -- dist/index.js", + "prettier": "prettier", + "reformat": "npm run prettier -- --write ." }, "license": "MIT", "devDependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", + "prettier": "^3.5.3", "typescript": "^5.8.2", "zod": "^3.24.2" }, diff --git a/src/client.ts b/src/client.ts index cfde0841..57271f4a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -34,12 +34,12 @@ export interface AtlasCluster { } export interface OauthDeviceCode { - user_code: string; - verification_uri: string; - device_code: string; - expires_in: string; - interval: string; -}; + user_code: string; + verification_uri: string; + device_code: string; + expires_in: string; + interval: string; +} export interface AtlasResponse { results: T[]; @@ -75,19 +75,21 @@ export class ApiClient { } private defaultOptions(): RequestInit { - const authHeaders = (!this.token?.access_token) ? null : { - "Authorization": `Bearer ${this.token.access_token}` - }; + const authHeaders = !this.token?.access_token + ? null + : { + Authorization: `Bearer ${this.token.access_token}`, + }; return { method: "GET", - credentials: (!this.token?.access_token) ? undefined : "include", + credentials: !this.token?.access_token ? undefined : "include", headers: { "Content-Type": "application/json", - "Accept": "application/vnd.atlas.2025-04-07+json", + Accept: "application/vnd.atlas.2025-04-07+json", "User-Agent": `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, - ...authHeaders - } + ...authHeaders, + }, }; } @@ -119,7 +121,7 @@ export class ApiClient { headers: { ...defaultOpt.headers, ...options?.headers, - } + }, }; const response = await fetch(url, opt); @@ -127,7 +129,7 @@ export class ApiClient { throw new ApiClientError(`Error calling Atlas API: ${response.statusText}`, response); } - return await response.json() as T; + return (await response.json()) as T; } async authenticate(): Promise { @@ -139,7 +141,7 @@ export class ApiClient { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + Accept: "application/json", }, body: new URLSearchParams({ client_id: config.clientID, @@ -152,7 +154,7 @@ export class ApiClient { throw new ApiClientError(`Failed to initiate authentication: ${response.statusText}`, response); } - return await response.json() as OauthDeviceCode; + return (await response.json()) as OauthDeviceCode; } async retrieveToken(device_code: string): Promise { @@ -172,10 +174,10 @@ export class ApiClient { if (response.ok) { const tokenData = await response.json(); - const buf = Buffer.from(tokenData.access_token.split('.')[1], 'base64').toString() + const buf = Buffer.from(tokenData.access_token.split(".")[1], "base64").toString(); const jwt = JSON.parse(buf); const expiry = new Date(jwt.exp * 1000); - return await this.storeToken({...tokenData, expiry}); + return await this.storeToken({ ...tokenData, expiry }); } try { const errorResponse = await response.json(); @@ -198,7 +200,7 @@ export class ApiClient { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + Accept: "application/json", }, body: new URLSearchParams({ client_id: config.clientID, @@ -213,7 +215,7 @@ export class ApiClient { } const data = await response.json(); - const buf = Buffer.from(data.access_token.split('.')[1], 'base64').toString() + const buf = Buffer.from(data.access_token.split(".")[1], "base64").toString(); const jwt = JSON.parse(buf); const expiry = new Date(jwt.exp * 1000); @@ -232,7 +234,7 @@ export class ApiClient { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + Accept: "application/json", "User-Agent": `AtlasMCP/${process.env.VERSION} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, }, body: new URLSearchParams({ @@ -263,16 +265,13 @@ export class ApiClient { return false; } const expiryDelta = 10 * 1000; // 10 seconds in milliseconds - const expiryWithDelta = new Date( - token.expiry.getTime() - expiryDelta - ); + const expiryWithDelta = new Date(token.expiry.getTime() - expiryDelta); return expiryWithDelta.getTime() > Date.now(); } catch (error) { return false; } } - async validateToken(token?: OAuthToken): Promise { if (this.checkTokenExpiry(token)) { return true; @@ -290,16 +289,16 @@ export class ApiClient { * Get all projects for the authenticated user */ async listProjects(): Promise> { - return await this.do>('/groups'); + return await this.do>("/groups"); } - + /** * Get a specific project by ID */ async getProject(projectId: string): Promise { return await this.do(`/groups/${projectId}`); } - + /** * Get clusters for a specific project */ diff --git a/src/index.ts b/src/index.ts index e19aee5a..5c80237b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,5 +12,3 @@ runServer().catch((error) => { console.error(`Fatal error running server:`, error); process.exit(1); }); - - diff --git a/src/server.ts b/src/server.ts index 81da3745..d7dcd65a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -30,12 +30,12 @@ export class Server { this.state.auth.token = token; this.state.auth.status = "issued"; saveState(this.state); - } + }, }); this.initiated = true; } - + private async ensureAuthenticated() { switch (this.state!.auth.status) { case "not_auth": @@ -80,7 +80,10 @@ export class Server { return { content: [ - { type: "text", text: `Please authenticate by visiting ${code.verification_uri} and entering the code ${code.user_code}` }, + { + type: "text", + text: `Please authenticate by visiting ${code.verification_uri} and entering the code ${code.user_code}`, + }, ], }; } catch (error: unknown) { @@ -106,10 +109,10 @@ export class Server { content: [{ type: "text", text: "You need to be authenticated first" }], }; } - + let clusters: AtlasCluster[] | undefined = undefined; let introText = "Here are your MongoDB Atlas clusters:"; - + const selectedProjectId = projectId || config.projectID; if (!selectedProjectId) { return { @@ -126,42 +129,46 @@ export class Server { } catch (e) { log("error", `Error fetching project details: ${e}`); } - + if (clusters.length === 0) { return { - content: [{ - type: "text", - text: "No clusters found. You may need to create a cluster in your MongoDB Atlas account." - }], + content: [ + { + type: "text", + text: "No clusters found. You may need to create a cluster in your MongoDB Atlas account.", + }, + ], }; } - + const formattedClusters = formatClustersTable(clusters); - + return { content: [ { type: "text", text: introText }, - { type: "text", text: formattedClusters } + { type: "text", text: formattedClusters }, ], }; } catch (error) { log("error", `Error listing clusters: ${error}`); - + // If the error is authentication related, suggest using auth tool if (error instanceof Error && error.message.includes("Not authenticated")) { return { content: [ { type: "text", text: "You need to authenticate before listing clusters." }, - { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." } + { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." }, ], }; } - + return { - content: [{ - type: "text", - text: `Error listing clusters: ${error instanceof Error ? error.message : String(error)}` - }], + content: [ + { + type: "text", + text: `Error listing clusters: ${error instanceof Error ? error.message : String(error)}`, + }, + ], }; } } @@ -174,49 +181,52 @@ export class Server { content: [{ type: "text", text: "You need to be authenticated first" }], }; } - + const projectsData = await this.apiClient!.listProjects(); const projects = projectsData.results || []; - + if (!projects || projects.length === 0) { return { content: [{ type: "text", text: "No projects found in your MongoDB Atlas account." }], }; } - + // Format projects as a table const header = `Project Name | Project ID | Created At ----------------|----------------|----------------`; - const rows = projects.map((project: any) => { - const createdAt = project.created ? new Date(project.created.$date).toLocaleString() : 'N/A'; - return `${project.name} | ${project.id} | ${createdAt}`; - } - ).join("\n"); + const rows = projects + .map((project: any) => { + const createdAt = project.created ? new Date(project.created.$date).toLocaleString() : "N/A"; + return `${project.name} | ${project.id} | ${createdAt}`; + }) + .join("\n"); const formattedProjects = `${header}\n${rows}`; return { content: [ { type: "text", text: "Here are your MongoDB Atlas projects:" }, - { type: "text", text: formattedProjects } + { type: "text", text: formattedProjects }, ], - } + }; } catch (error) { log("error", `Error listing projects: ${error}`); - + // If the error is authentication related, suggest using auth tool if (error instanceof Error && error.message.includes("Not authenticated")) { return { content: [ { type: "text", text: "You need to authenticate before listing projects." }, - { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." } + { type: "text", text: "Please use the 'auth' tool to log in to your MongoDB Atlas account." }, ], }; } - + return { - content: [{ - type: "text", - text: `Error listing projects: ${error instanceof Error ? error.message : String(error)}` - }], + content: [ + { + type: "text", + text: `Error listing projects: ${error instanceof Error ? error.message : String(error)}`, + }, + ], }; } } @@ -228,15 +238,20 @@ export class Server { }); server.tool("auth", "Authenticate to Atlas", async ({}) => this.authTool()); - + let projectIdFilter: any = z.string().describe("Optional Atlas project ID to filter clusters"); if (config.projectID) { projectIdFilter = projectIdFilter.optional(); } - - server.tool("list-clusters", "Lists MongoDB Atlas clusters", { - projectId: projectIdFilter, - }, async ({ projectId }: { projectId?: string }) => this.listClustersTool(projectId)); + + server.tool( + "list-clusters", + "Lists MongoDB Atlas clusters", + { + projectId: projectIdFilter, + }, + async ({ projectId }: { projectId?: string }) => this.listClustersTool(projectId) + ); server.tool("list-projects", "Lists MongoDB Atlas projects", async () => this.listProjectsTool()); return server; @@ -247,8 +262,7 @@ export class Server { const server = this.mcpServer(); await server.connect(transport); } - -}; +} function formatClustersTable(clusters: AtlasCluster[]): string { if (clusters.length === 0) { @@ -256,11 +270,13 @@ function formatClustersTable(clusters: AtlasCluster[]): string { } const header = `Cluster Name | State | MongoDB Version | Region | Connection String ----------------|----------------|----------------|----------------|----------------|----------------`; - const rows = clusters.map(cluster => { - const region = cluster.providerSettings?.regionName || 'N/A'; - const connectionString = cluster.connectionStrings?.standard || 'N/A'; - const mongoDBVersion = cluster.mongoDBVersion || 'N/A'; - return `${cluster.name} | ${cluster.stateName} | ${mongoDBVersion} | ${region} | ${connectionString}`; - }).join("\n"); + const rows = clusters + .map((cluster) => { + const region = cluster.providerSettings?.regionName || "N/A"; + const connectionString = cluster.connectionStrings?.standard || "N/A"; + const mongoDBVersion = cluster.mongoDBVersion || "N/A"; + return `${cluster.name} | ${cluster.stateName} | ${mongoDBVersion} | ${region} | ${connectionString}`; + }) + .join("\n"); return `${header}\n${rows}`; } diff --git a/src/state.ts b/src/state.ts index 2c6414b5..11be5496 100644 --- a/src/state.ts +++ b/src/state.ts @@ -7,17 +7,16 @@ export interface State { status: "not_auth" | "requested" | "issued"; code?: OauthDeviceCode; token?: OAuthToken; - } + }; } - export async function saveState(state: State): Promise { return new Promise((resolve, reject) => { fs.writeFile(config.stateFile, JSON.stringify(state), function (err) { if (err) { return reject(err); } - + return resolve(); }); });