Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
29 changes: 9 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,6 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow

1. Command-line arguments
2. Environment variables
3. Configuration file
4. Default values

### Configuration Options

Expand All @@ -167,6 +165,12 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
| `apiClientId` | Atlas API client ID for authentication |
| `apiClientSecret` | Atlas API client secret for authentication |
| `connectionString` | MongoDB connection string for direct database connections (optional users may choose to inform it on every tool call) |
| `logPath` | Folder to store logs |

**Default Log Path:**

- Windows: `%LOCALAPPDATA%\mongodb\mongodb-mcp\.app-logs`
- macOS/Linux: `~/.mongodb/mongodb-mcp/.app-logs`

### Atlas API Access

Expand Down Expand Up @@ -195,23 +199,6 @@ To use the Atlas API tools, you'll need to create a service account in MongoDB A

### Configuration Methods

#### Configuration File

Create a JSON configuration file at one of these locations:

- Linux/macOS: `/etc/mongodb-mcp.conf`
- Windows: `%LOCALAPPDATA%\mongodb\mongodb-mcp\mongodb-mcp.conf`

Example configuration file:

```json
{
"apiClientId": "your-atlas-client-id",
"apiClientSecret": "your-atlas-client-secret",
"connectionString": "mongodb+srv://username:[email protected]/myDatabase"
}
```

#### Environment Variables

Set environment variables with the prefix `MDB_MCP_` followed by the option name in uppercase with underscores:
Expand All @@ -223,14 +210,16 @@ export MDB_MCP_API_CLIENT_SECRET="your-atlas-client-secret"

# Set a custom MongoDB connection string
export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:[email protected]/myDatabase"

export MDB_MCP_LOG_PATH="/path/to/logs"
```

#### Command-Line Arguments

Pass configuration options as command-line arguments when starting the server:

```shell
node dist/index.js --apiClientId="your-atlas-client-id" --apiClientSecret="your-atlas-client-secret" --connectionString="mongodb+srv://username:[email protected]/myDatabase"
node dist/index.js --apiClientId="your-atlas-client-id" --apiClientSecret="your-atlas-client-secret" --connectionString="mongodb+srv://username:[email protected]/myDatabase" --logPath=/path/to/logs
```

## 🤝 Contributing
Expand Down
75 changes: 50 additions & 25 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import config from "../../config.js";
import createClient, { FetchOptions, Middleware } from "openapi-fetch";
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
import { AccessToken, ClientCredentials } from "simple-oauth2";

import { paths, operations } from "./openapi.js";

const ATLAS_API_VERSION = "2025-03-12";

export class ApiClientError extends Error {
response?: Response;

Expand All @@ -25,38 +27,32 @@ export class ApiClientError extends Error {
}

export interface ApiClientOptions {
credentials: {
credentials?: {
clientId: string;
clientSecret: string;
};
baseUrl?: string;
userAgent?: string;
}

export class ApiClient {
private client = createClient<paths>({
baseUrl: config.apiBaseUrl,
headers: {
"User-Agent": config.userAgent,
Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`,
},
});
private oauth2Client = new ClientCredentials({
client: {
id: this.options.credentials.clientId,
secret: this.options.credentials.clientSecret,
},
auth: {
tokenHost: this.options.baseUrl || config.apiBaseUrl,
tokenPath: "/api/oauth/token",
},
});
private options: {
baseUrl: string;
userAgent: string;
credentials?: {
clientId: string;
clientSecret: string;
};
};
private client: Client<paths>;
private oauth2Client?: ClientCredentials;
private accessToken?: AccessToken;

private getAccessToken = async () => {
if (!this.accessToken || this.accessToken.expired()) {
if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
this.accessToken = await this.oauth2Client.getToken({});
}
return this.accessToken.token.access_token;
return this.accessToken?.token.access_token as string | undefined;
};

private authMiddleware = (apiClient: ApiClient): Middleware => ({
Expand All @@ -82,22 +78,51 @@ export class ApiClient {
},
});

constructor(private options: ApiClientOptions) {
this.client.use(this.authMiddleware(this));
constructor(options?: ApiClientOptions) {
const defaultOptions = {
baseUrl: "https://cloud.mongodb.com/",
userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
};

this.options = {
...defaultOptions,
...options,
};

this.client = createClient<paths>({
baseUrl: this.options.baseUrl,
headers: {
"User-Agent": this.options.userAgent,
Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
},
});
if (this.options.credentials?.clientId && this.options.credentials?.clientSecret) {
this.oauth2Client = new ClientCredentials({
client: {
id: this.options.credentials.clientId,
secret: this.options.credentials.clientSecret,
},
auth: {
tokenHost: this.options.baseUrl,
tokenPath: "/api/oauth/token",
},
});
this.client.use(this.authMiddleware(this));
}
this.client.use(this.errorMiddleware());
}

async getIpInfo() {
const accessToken = await this.getAccessToken();

const endpoint = "api/private/ipinfo";
const url = new URL(endpoint, config.apiBaseUrl);
const url = new URL(endpoint, this.options.baseUrl);
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
"User-Agent": config.userAgent,
"User-Agent": this.options.userAgent,
},
});

Expand Down
51 changes: 10 additions & 41 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import os from "os";
import argv from "yargs-parser";

import packageJson from "../package.json" with { type: "json" };
import fs from "fs";
import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb";
const { localDataPath, configPath } = getLocalDataPath();

// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
// env variables.
interface UserConfig {
apiBaseUrl: string;
apiBaseUrl?: string;
apiClientId?: string;
apiClientSecret?: string;
stateFile: string;
logPath: string;
connectionString?: string;
connectOptions: {
readConcern: ReadConcernLevel;
Expand All @@ -24,8 +22,7 @@ interface UserConfig {
}

const defaults: UserConfig = {
apiBaseUrl: "https://cloud.mongodb.com/",
stateFile: path.join(localDataPath, "state.json"),
logPath: getLogPath(),
connectOptions: {
readConcern: "local",
readPreference: "secondaryPreferred",
Expand All @@ -36,43 +33,26 @@ const defaults: UserConfig = {

const mergedUserConfig = {
...defaults,
...getFileConfig(),
...getEnvConfig(),
...getCliConfig(),
};

const config = {
...mergedUserConfig,
atlasApiVersion: `2025-03-12`,
version: packageJson.version,
userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
localDataPath,
};

export default config;

function getLocalDataPath(): { localDataPath: string; configPath: string } {
let localDataPath: string | undefined;
let configPath: string | undefined;
function getLogPath(): string {
const localDataPath =
process.platform === "win32"
? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
: path.join(os.homedir(), ".mongodb");

if (process.platform === "win32") {
const appData = process.env.APPDATA;
const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA;
if (localAppData && appData) {
localDataPath = path.join(localAppData, "mongodb", "mongodb-mcp");
configPath = path.join(localDataPath, "mongodb-mcp.conf");
}
}

localDataPath ??= path.join(os.homedir(), ".mongodb", "mongodb-mcp");
configPath ??= "/etc/mongodb-mcp.conf";

fs.mkdirSync(localDataPath, { recursive: true });
const logPath = path.join(localDataPath, "mongodb-mcp", ".app-logs");

return {
localDataPath,
configPath,
};
return logPath;
}

// Gets the config supplied by the user as environment variables. The variable names
Expand Down Expand Up @@ -125,17 +105,6 @@ function SNAKE_CASE_toCamelCase(str: string): string {
return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
}

// Gets the config supplied by the user as a JSON file. The file is expected to be located in the local data path
// and named `config.json`.
function getFileConfig(): Partial<UserConfig> {
try {
const config = fs.readFileSync(configPath, "utf8");
return JSON.parse(config);
} catch {
return {};
}
}

// Reads the cli args and parses them into a UserConfig object.
function getCliConfig() {
return argv(process.argv.slice(2)) as unknown as Partial<UserConfig>;
Expand Down
20 changes: 15 additions & 5 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import fs from "fs";
import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer";
import path from "path";
import config from "./config.js";
import redact from "mongodb-redact";
import fs from "fs/promises";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";

Expand Down Expand Up @@ -99,12 +98,23 @@ class ProxyingLogger extends LoggerBase {
const logger = new ProxyingLogger();
export default logger;

async function mkdirPromise(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions) {
return new Promise<string | undefined>((resolve, reject) => {
fs.mkdir(path, options, (err, resultPath) => {
if (err) {
reject(err);
} else {
resolve(resultPath);
}
});
});
}

export async function initializeLogger(server: McpServer): Promise<void> {
const logDir = path.join(config.localDataPath, ".app-logs");
await fs.mkdir(logDir, { recursive: true });
await mkdirPromise(config.logPath, { recursive: true });

const manager = new MongoLogManager({
directory: path.join(config.localDataPath, ".app-logs"),
directory: config.logPath,
retentionDays: 30,
onwarn: console.warn,
onerror: console.error,
Expand Down
36 changes: 6 additions & 30 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ApiClient } from "./common/atlas/apiClient.js";
import defaultState, { State } from "./state.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { registerAtlasTools } from "./tools/atlas/tools.js";
Expand All @@ -10,44 +9,21 @@ import { mongoLogId } from "mongodb-log-writer";

export class Server {
state: State = defaultState;
apiClient?: ApiClient;
initialized: boolean = false;
private server?: McpServer;

private async init() {
if (this.initialized) {
return;
}

await this.state.loadCredentials();

if (config.apiClientId && config.apiClientSecret) {
this.apiClient = new ApiClient({
credentials: {
clientId: config.apiClientId!,
clientSecret: config.apiClientSecret,
},
});
}

this.initialized = true;
}

async connect(transport: Transport) {
await this.init();
const server = new McpServer({
this.server = new McpServer({
name: "MongoDB Atlas",
version: config.version,
});

server.server.registerCapabilities({ logging: {} });
this.server.server.registerCapabilities({ logging: {} });

registerAtlasTools(server, this.state, this.apiClient);
registerMongoDBTools(server, this.state);
registerAtlasTools(this.server, this.state);
registerMongoDBTools(this.server, this.state);

await server.connect(transport);
await initializeLogger(server);
this.server = server;
await initializeLogger(this.server);
await this.server.connect(transport);

logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`);
}
Expand Down
Loading
Loading