Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
44 changes: 8 additions & 36 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ 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;
apiClientId?: string;
apiClientSecret?: string;
stateFile: string;
logPath: string;
connectionString?: string;
connectOptions: {
readConcern: ReadConcernLevel;
Expand All @@ -25,7 +24,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,7 +35,6 @@ const defaults: UserConfig = {

const mergedUserConfig = {
...defaults,
...getFileConfig(),
...getEnvConfig(),
...getCliConfig(),
};
Expand All @@ -46,33 +44,18 @@ const config = {
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 +108,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
24 changes: 6 additions & 18 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { mongoLogId } from "mongodb-log-writer";
export class Server {
state: State = defaultState;
apiClient?: ApiClient;
initialized: boolean = false;

private async init() {
if (this.initialized) {
return;
}
private createMcpServer(): McpServer {
const server = new McpServer({
name: "MongoDB Atlas",
version: config.version,
});

await this.state.loadCredentials();
server.server.registerCapabilities({ logging: {} });

if (config.apiClientId && config.apiClientSecret) {
this.apiClient = new ApiClient({
Expand All @@ -29,25 +29,13 @@ export class Server {
});
}

this.initialized = true;
}

private createMcpServer(): McpServer {
const server = new McpServer({
name: "MongoDB Atlas",
version: config.version,
});

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

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

return server;
}

async connect(transport: Transport) {
await this.init();
const server = this.createMcpServer();
await server.connect(transport);
await initializeLogger(server);
Expand Down
27 changes: 0 additions & 27 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,7 @@
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { AsyncEntry } from "@napi-rs/keyring";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this package dependency if we're not going to use it.

import logger from "./logger.js";
import { mongoLogId } from "mongodb-log-writer";

interface Credentials {
connectionString?: string;
}

export class State {
private entry = new AsyncEntry("mongodb-mcp", "credentials");
credentials: Credentials = {};
serviceProvider?: NodeDriverServiceProvider;

public async persistCredentials(): Promise<void> {
await this.entry.setPassword(JSON.stringify(this.credentials));
}

public async loadCredentials(): Promise<boolean> {
try {
const data = await this.entry.getPassword();
if (data) {
this.credentials = JSON.parse(data);
}

return true;
} catch (err: unknown) {
logger.error(mongoLogId(1_000_007), "state", `Failed to load state: ${err}`);
return false;
}
}
}

const defaultState = new State();
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/atlasTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export abstract class AtlasToolBase extends ToolBase {
super(state);
}

protected ensureAuthenticated(): void {
protected ensureAuthenticated(): asserts this is { apiClient: ApiClient } {
if (!this.apiClient) {
throw new Error(
"Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables."
Expand Down
4 changes: 2 additions & 2 deletions src/tools/atlas/createAccessList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class CreateAccessListTool extends AtlasToolBase {
}));

if (currentIpAddress) {
const currentIp = await this.apiClient!.getIpInfo();
const currentIp = await this.apiClient.getIpInfo();
const input = {
groupId: projectId,
ipAddress: currentIp.currentIpv4Address,
Expand All @@ -56,7 +56,7 @@ export class CreateAccessListTool extends AtlasToolBase {

const inputs = [...ipInputs, ...cidrInputs];

await this.apiClient!.createProjectIpAccessList({
await this.apiClient.createProjectIpAccessList({
params: {
path: {
groupId: projectId,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/createDBUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class CreateDBUserTool extends AtlasToolBase {
: undefined,
} as CloudDatabaseUser;

await this.apiClient!.createDatabaseUser({
await this.apiClient.createDatabaseUser({
params: {
path: {
groupId: projectId,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/createFreeCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class CreateFreeClusterTool extends AtlasToolBase {
terminationProtectionEnabled: false,
} as unknown as ClusterDescription20240805;

await this.apiClient!.createCluster({
await this.apiClient.createCluster({
params: {
path: {
groupId: projectId,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/inspectAccessList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class InspectAccessListTool extends AtlasToolBase {
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
this.ensureAuthenticated();

const accessList = await this.apiClient!.listProjectIpAccessLists({
const accessList = await this.apiClient.listProjectIpAccessLists({
params: {
path: {
groupId: projectId,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/inspectCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class InspectClusterTool extends AtlasToolBase {
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
this.ensureAuthenticated();

const cluster = await this.apiClient!.getCluster({
const cluster = await this.apiClient.getCluster({
params: {
path: {
groupId: projectId,
Expand Down
6 changes: 3 additions & 3 deletions src/tools/atlas/listClusters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export class ListClustersTool extends AtlasToolBase {
this.ensureAuthenticated();

if (!projectId) {
const data = await this.apiClient!.listClustersForAllProjects();
const data = await this.apiClient.listClustersForAllProjects();

return this.formatAllClustersTable(data);
} else {
const project = await this.apiClient!.getProject({
const project = await this.apiClient.getProject({
params: {
path: {
groupId: projectId,
Expand All @@ -31,7 +31,7 @@ export class ListClustersTool extends AtlasToolBase {
throw new Error(`Project with ID "${projectId}" not found.`);
}

const data = await this.apiClient!.listClusters({
const data = await this.apiClient.listClusters({
params: {
path: {
groupId: project.id || "",
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/listDBUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class ListDBUsersTool extends AtlasToolBase {
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
this.ensureAuthenticated();

const data = await this.apiClient!.listDatabaseUsers({
const data = await this.apiClient.listDatabaseUsers({
params: {
path: {
groupId: projectId,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/listProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class ListProjectsTool extends AtlasToolBase {
protected async execute(): Promise<CallToolResult> {
this.ensureAuthenticated();

const data = await this.apiClient!.listProjects();
const data = await this.apiClient.listProjects();

if (!data?.results?.length) {
throw new Error("No projects found in your MongoDB Atlas account.");
Expand Down
2 changes: 1 addition & 1 deletion src/tools/mongodb/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class ConnectTool extends MongoDBToolBase {
protected async execute({
connectionStringOrClusterName,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
connectionStringOrClusterName ??= config.connectionString || this.state.credentials.connectionString;
connectionStringOrClusterName ??= config.connectionString;
if (!connectionStringOrClusterName) {
return {
content: [
Expand Down
2 changes: 0 additions & 2 deletions src/tools/mongodb/mongodbTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,5 @@ export abstract class MongoDBToolBase extends ToolBase {
});

state.serviceProvider = provider;
state.credentials.connectionString = connectionString;
await state.persistCredentials();
}
}