Skip to content

feat: atlas mcp #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.vscode/mcp.json
.github/prompts/*

# Environment variables
.env

# Sensitive
state.json
10 changes: 10 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Features

- [x] Login via OAuth authentication (device code)
- [ ] Register via OAuth authentication (device code)
- [x] List clusters
- [ ] Create M0 cluster
- [ ] Create a DBUser
- [ ] Delete a DBUser
- [ ] Connect to a cluster
- [ ] Emit telemetry events on MCP usage
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,147 @@
<<<<<<< HEAD
# atlas-mcp-server

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

## 📚 Table of Contents
- [🚀 Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Running the MCP Server](#running-the-mcp-server)
- [🔧 Troubleshooting](#troubleshooting)
- [Restart Server](#restart-server)
- [View Logs](#view-logs)
- [Debugging](#debugging)
- [🛠️ Supported Tools](#supported-tools)
- [Tool List](#tool-list)
- [👩‍💻 Client Integration](#client-integration)
- [VSCode](#vscode)
- [Claude](#claude)

## 🚀 Getting Started

### Prerequisites
- Node.js installed
- MongoDB Atlas account

### Installation

```shell
npm install
```

### Running the MCP Server

```shell
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
- Select the MCP server you want to see logs for
- Select the option to view logs in the output panel

### Debugging

We can use @modelcontextprotocol/inspector to debug the server - https://github.com/modelcontextprotocol/inspector

From the root of this repository, run:
```shell
npx @modelcontextprotocol/inspector -- node dist/index.js
```

Or use the npm script:
```shell
npm run inspect
```

## 🛠️ Supported Tools

### Tool List
- `auth` - Authenticate to MongoDB Atlas
- `list-clusters` - Lists MongoDB Atlas clusters
- `list-projects` - Lists MongoDB Atlas projects

## 👩‍💻 Client Integration (Use the server!)

### VSCode

Prerequisites:
- Use VSCode Insiders (https://code.visualstudio.com/insiders/)
- Setup copilot in VSCode Insiders

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.
- Add the path to dist/index.js in the prompt

Step 2: Verify the created mcp file

It should look like this
```shell
{
"servers": {
"demo-atlas-server": {
"type": "stdio",
"command": "/Users/<user>/workplace/atlas-mcp-server/dist/index.js",
"args": []
}
}
}
```

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
```
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
```

Paste the mcp server configuration into the file
```
{
"mcpServers": {
"Demo": {
"command": "path/to/this/repo/atlas-mc-server/dist/index.js"
}
}
}
```

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)
224 changes: 224 additions & 0 deletions dist/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import config from "./config.js";
;
export class ApiClientError extends Error {
constructor(message, response = undefined) {
super(message);
this.name = "ApiClientError";
this.response = response;
}
}
export class ApiClient {
constructor(options) {
const { token, saveToken } = options;
this.token = token;
this.saveToken = saveToken;
}
defaultOptions() {
const authHeaders = (!this.token?.access_token) ? null : {
"Authorization": `Bearer ${this.token.access_token}`
};
return {
method: "GET",
credentials: (!this.token?.access_token) ? undefined : "include",
headers: {
"Content-Type": "application/json",
"Accept": "application/vnd.atlas.2025-04-07+json",
"User-Agent": `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
...authHeaders
}
};
}
async storeToken(token) {
this.token = token;
if (this.saveToken) {
await this.saveToken(token);
}
return token;
}
async do(endpoint, options) {
if (!this.token || !this.token.access_token) {
throw new Error("Not authenticated. Please run the auth tool first.");
}
const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseURL}`);
if (!this.checkTokenExpiry()) {
await this.refreshToken();
}
const defaultOpt = this.defaultOptions();
const opt = {
...defaultOpt,
...options,
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();
}
async authenticate() {
const endpoint = "api/private/unauth/account/device/authorize";
const authUrl = new URL(endpoint, config.apiBaseURL);
const response = await fetch(authUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
},
body: new URLSearchParams({
client_id: config.clientID,
scope: "openid profile offline_access",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}).toString(),
});
if (!response.ok) {
throw new ApiClientError(`Failed to initiate authentication: ${response.statusText}`, response);
}
return await response.json();
}
async retrieveToken(device_code) {
const endpoint = "api/private/unauth/account/device/token";
const url = new URL(endpoint, config.apiBaseURL);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: config.clientID,
device_code: device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}).toString(),
});
if (response.ok) {
const tokenData = await response.json();
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 });
}
try {
const errorResponse = await response.json();
if (errorResponse.errorCode === "DEVICE_AUTHORIZATION_PENDING") {
throw new ApiClientError("Authentication pending. Try again later.", response);
}
else if (errorResponse.error === "expired_token") {
throw new ApiClientError("Device code expired. Please restart the authentication process.", response);
}
else {
throw new ApiClientError("Device code expired. Please restart the authentication process.", response);
}
}
catch (error) {
throw new ApiClientError("Failed to retrieve token. Please check your device code.", response);
}
}
async refreshToken(token) {
const endpoint = "api/private/unauth/account/device/token";
const url = new URL(endpoint, config.apiBaseURL);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
},
body: new URLSearchParams({
client_id: config.clientID,
refresh_token: (token || this.token)?.refresh_token || "",
grant_type: "refresh_token",
scope: "openid profile offline_access",
}).toString(),
});
if (!response.ok) {
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 jwt = JSON.parse(buf);
const expiry = new Date(jwt.exp * 1000);
const tokenToStore = {
...data,
expiry,
};
return await this.storeToken(tokenToStore);
}
async revokeToken(token) {
const endpoint = "api/private/unauth/account/device/token";
const url = new URL(endpoint, config.apiBaseURL);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"User-Agent": `AtlasMCP/${process.env.VERSION} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
},
body: new URLSearchParams({
client_id: config.clientID,
token: (token || this.token)?.access_token || "",
token_type_hint: "refresh_token",
}).toString(),
});
if (!response.ok) {
throw new ApiClientError(`Failed to revoke token: ${response.statusText}`, response);
}
if (!token && this.token) {
this.token = undefined;
}
return;
}
checkTokenExpiry(token) {
try {
token = token || this.token;
if (!token || !token.access_token) {
return false;
}
if (!token.expiry) {
return false;
}
const expiryDelta = 10 * 1000; // 10 seconds in milliseconds
const expiryWithDelta = new Date(token.expiry.getTime() - expiryDelta);
return expiryWithDelta.getTime() > Date.now();
}
catch (error) {
return false;
}
}
async validateToken(token) {
if (this.checkTokenExpiry(token)) {
return true;
}
try {
await this.refreshToken(token);
return true;
}
catch (error) {
return false;
}
}
/**
* Get all projects for the authenticated user
*/
async listProjects() {
return await this.do('/groups');
}
/**
* Get a specific project by ID
*/
async getProject(projectId) {
return await this.do(`/groups/${projectId}`);
}
/**
* Get clusters for a specific project
*/
async listProjectClusters(projectId) {
return await this.do(`/groups/${projectId}/clusters`);
}
/**
* Get clusters for a specific project
*/
async listAllClusters() {
return await this.do(`/clusters`);
}
}
Loading