Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import config from "../../config.js";
import version from "../../version.js";
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
import { AccessToken, ClientCredentials } from "simple-oauth2";
import { ApiClientError } from "./apiClientError.js";
Expand Down Expand Up @@ -67,7 +67,7 @@ export class ApiClient {
baseUrl: options?.baseUrl || "https://cloud.mongodb.com/",
userAgent:
options?.userAgent ||
`AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
`AtlasMCP/${version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
};

this.client = createClient<paths>({
Expand Down
12 changes: 2 additions & 10 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import path from "path";
import os from "os";
import argv from "yargs-parser";

import packageJson from "../package.json" with { type: "json" };
import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb";

// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
// env variables.
interface UserConfig {
export interface UserConfig {
apiBaseUrl?: string;
apiClientId?: string;
apiClientSecret?: string;
Expand All @@ -33,19 +32,12 @@ const defaults: UserConfig = {
disabledTools: [],
};

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

const config = {
...mergedUserConfig,
version: packageJson.version,
};

export default config;

function getLogPath(): string {
const localDataPath =
process.platform === "win32"
Expand Down
15 changes: 2 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,11 @@
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import logger from "./logger.js";
import { mongoLogId } from "mongodb-log-writer";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import config from "./config.js";
import { Session } from "./session.js";
import { Server } from "./server.js";
import { config } from "./config.js";

try {
const session = new Session();
const mcpServer = new McpServer({
name: "MongoDB Atlas",
version: config.version,
});

const server = new Server({
mcpServer,
session,
});
const server = new Server(config);

const transport = new StdioServerTransport();

Expand Down
7 changes: 3 additions & 4 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from "fs/promises";
import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer";
import config from "./config.js";
import redact from "mongodb-redact";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";
Expand Down Expand Up @@ -98,11 +97,11 @@ class ProxyingLogger extends LoggerBase {
const logger = new ProxyingLogger();
export default logger;

export async function initializeLogger(server: McpServer): Promise<void> {
await fs.mkdir(config.logPath, { recursive: true });
export async function initializeLogger(server: McpServer, logPath: string): Promise<void> {
await fs.mkdir(logPath, { recursive: true });

const manager = new MongoLogManager({
directory: config.logPath,
directory: logPath,
retentionDays: 30,
onwarn: console.warn,
onerror: console.error,
Expand Down
25 changes: 16 additions & 9 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import { AtlasTools } from "./tools/atlas/tools.js";
import { MongoDbTools } from "./tools/mongodb/tools.js";
import logger, { initializeLogger } from "./logger.js";
import { mongoLogId } from "mongodb-log-writer";
import config from "./config.js";
import { UserConfig } from "./config.js";
import version from "./version.js";

export class Server {
public readonly session: Session;
private readonly mcpServer: McpServer;
private readonly mcpServer: McpServer = new McpServer({
Copy link
Collaborator

Choose a reason for hiding this comment

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

it'd be better for the server to be passed in the MCP server + session as it was before, from dependency injection perspective. We might i.e. have other constructs that use the MCP server or the session, so we don't want it to be a derivative of the server (though one can also argue this class is like the main function).

name: "MongoDB Atlas",
version,
});

constructor({ mcpServer, session }: { mcpServer: McpServer; session: Session }) {
this.mcpServer = mcpServer;
this.session = session;
constructor(private readonly config: UserConfig) {
this.session = new Session({
apiBaseUrl: config.apiBaseUrl,
apiClientId: config.apiClientId,
apiClientSecret: config.apiClientSecret,
});
}

async connect(transport: Transport) {
Expand All @@ -22,7 +29,7 @@ export class Server {
this.registerTools();
this.registerResources();

await initializeLogger(this.mcpServer);
await initializeLogger(this.mcpServer, this.config.logPath);

await this.mcpServer.connect(transport);

Expand All @@ -36,12 +43,12 @@ export class Server {

private registerTools() {
for (const tool of [...AtlasTools, ...MongoDbTools]) {
new tool(this.session).register(this.mcpServer);
new tool(this.session, this.config).register(this.mcpServer);
}
}

private registerResources() {
if (config.connectionString) {
if (this.config.connectionString) {
this.mcpServer.resource(
"connection-string",
"config://connection-string",
Expand All @@ -52,7 +59,7 @@ export class Server {
return {
contents: [
{
text: `Preconfigured connection string: ${config.connectionString}`,
text: `Preconfigured connection string: ${this.config.connectionString}`,
uri: uri.href,
},
],
Expand Down
17 changes: 11 additions & 6 deletions src/session.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { ApiClient, ApiClientCredentials } from "./common/atlas/apiClient.js";
import config from "./config.js";

export interface SessionOptions {
apiBaseUrl?: string;
apiClientId?: string;
apiClientSecret?: string;
}

export class Session {
serviceProvider?: NodeDriverServiceProvider;
apiClient: ApiClient;

constructor() {
constructor(options?: SessionOptions) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
constructor(options?: SessionOptions) {
constructor(options: SessionOptions) {

Copy link
Collaborator

Choose a reason for hiding this comment

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

(or setting a default)

const credentials: ApiClientCredentials | undefined =
config.apiClientId && config.apiClientSecret
options?.apiClientId && options?.apiClientSecret
? {
clientId: config.apiClientId,
clientSecret: config.apiClientSecret,
clientId: options?.apiClientId,
clientSecret: options?.apiClientSecret,
}
: undefined;

this.apiClient = new ApiClient({
baseUrl: config.apiBaseUrl,
baseUrl: options?.apiBaseUrl,
credentials,
});
}
Expand Down
8 changes: 1 addition & 7 deletions src/tools/atlas/atlasTool.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { ToolBase, ToolCategory } from "../tool.js";
import { Session } from "../../session.js";
import config from "../../config.js";

export abstract class AtlasToolBase extends ToolBase {
constructor(protected readonly session: Session) {
super(session);
}

protected category: ToolCategory = "atlas";

protected verifyAllowed(): boolean {
if (!config.apiClientId || !config.apiClientSecret) {
if (!this.config.apiClientId || !this.config.apiClientSecret) {
return false;
}
return super.verifyAllowed();
Expand Down
11 changes: 5 additions & 6 deletions src/tools/mongodb/metadata/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs, OperationType } from "../../tool.js";
import config from "../../../config.js";
import { MongoError as DriverError } from "mongodb";

export class ConnectTool extends MongoDBToolBase {
Expand Down Expand Up @@ -35,7 +34,7 @@ export class ConnectTool extends MongoDBToolBase {
protected async execute({ options: optionsArr }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const options = optionsArr?.[0];
let connectionString: string;
if (!options && !config.connectionString) {
if (!options && !this.config.connectionString) {
return {
content: [
{ type: "text", text: "No connection details provided." },
Expand All @@ -46,7 +45,7 @@ export class ConnectTool extends MongoDBToolBase {

if (!options) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
connectionString = config.connectionString!;
connectionString = this.config.connectionString!;
} else if ("connectionString" in options) {
connectionString = options.connectionString;
} else {
Expand All @@ -72,17 +71,17 @@ export class ConnectTool extends MongoDBToolBase {
// Sometimes the model will supply an incorrect connection string. If the user has configured
// a different one as environment variable or a cli argument, suggest using that one instead.
if (
config.connectionString &&
this.config.connectionString &&
error instanceof DriverError &&
config.connectionString !== connectionString
this.config.connectionString !== connectionString
) {
return {
content: [
{
type: "text",
text:
`Failed to connect to MongoDB at '${connectionString}' due to error: '${error.message}.` +
`Your config lists a different connection string: '${config.connectionString}' - do you want to try connecting to it instead?`,
`Your config lists a different connection string: '${this.config.connectionString}' - do you want to try connecting to it instead?`,
},
],
};
Expand Down
18 changes: 6 additions & 12 deletions src/tools/mongodb/mongodbTool.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
import { z } from "zod";
import { ToolArgs, ToolBase, ToolCategory } from "../tool.js";
import { Session } from "../../session.js";
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { ErrorCodes, MongoDBError } from "../../errors.js";
import config from "../../config.js";

export const DbOperationArgs = {
database: z.string().describe("Database name"),
collection: z.string().describe("Collection name"),
};

export abstract class MongoDBToolBase extends ToolBase {
constructor(session: Session) {
super(session);
}

protected category: ToolCategory = "mongodb";

protected async ensureConnected(): Promise<NodeDriverServiceProvider> {
if (!this.session.serviceProvider && config.connectionString) {
await this.connectToMongoDB(config.connectionString);
if (!this.session.serviceProvider && this.config.connectionString) {
await this.connectToMongoDB(this.config.connectionString);
}

if (!this.session.serviceProvider) {
Expand Down Expand Up @@ -58,13 +52,13 @@ export abstract class MongoDBToolBase extends ToolBase {
productDocsLink: "https://docs.mongodb.com/todo-mcp",
productName: "MongoDB MCP",
readConcern: {
level: config.connectOptions.readConcern,
level: this.config.connectOptions.readConcern,
},
readPreference: config.connectOptions.readPreference,
readPreference: this.config.connectOptions.readPreference,
writeConcern: {
w: config.connectOptions.writeConcern,
w: this.config.connectOptions.writeConcern,
},
timeoutMS: config.connectOptions.timeoutMS,
timeoutMS: this.config.connectOptions.timeoutMS,
});

this.session.serviceProvider = provider;
Expand Down
13 changes: 8 additions & 5 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { Session } from "../session.js";
import logger from "../logger.js";
import { mongoLogId } from "mongodb-log-writer";
import config from "../config.js";
import { UserConfig } from "../config.js";

export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;

Expand All @@ -24,7 +24,10 @@ export abstract class ToolBase {

protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;

protected constructor(protected session: Session) {}
constructor(
protected readonly session: Session,
protected readonly config: UserConfig
) {}

public register(server: McpServer): void {
if (!this.verifyAllowed()) {
Expand Down Expand Up @@ -54,11 +57,11 @@ export abstract class ToolBase {
// Checks if a tool is allowed to run based on the config
protected verifyAllowed(): boolean {
let errorClarification: string | undefined;
if (config.disabledTools.includes(this.category)) {
if (this.config.disabledTools.includes(this.category)) {
errorClarification = `its category, \`${this.category}\`,`;
} else if (config.disabledTools.includes(this.operationType)) {
} else if (this.config.disabledTools.includes(this.operationType)) {
errorClarification = `its operation type, \`${this.operationType}\`,`;
} else if (config.disabledTools.includes(this.name)) {
} else if (this.config.disabledTools.includes(this.name)) {
errorClarification = `it`;
}

Expand Down
3 changes: 3 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import packageJson from "../package.json" with { type: "json" };

export default packageJson.version;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export default packageJson.version;
export const version = packageJson.version;

Copy link
Collaborator

Choose a reason for hiding this comment

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

if we're using this in only one place we could do:

import { version } from "../package.json" with { type: "json" };

and not have this file

Copy link
Collaborator Author

@fmenezes fmenezes Apr 23, 2025

Choose a reason for hiding this comment

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

It's read in two places right now, renamed to packageInfo.ts added another string in there, we can remove it later

25 changes: 3 additions & 22 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import { Server } from "../../src/server.js";
import runner, { MongoCluster } from "mongodb-runner";
import path from "path";
import fs from "fs/promises";
import { Session } from "../../src/session.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { MongoClient, ObjectId } from "mongodb";
import { toIncludeAllMembers } from "jest-extended";
import config from "../../src/config.js";
import { config, UserConfig } from "../../src/config.js";
import { McpError } from "@modelcontextprotocol/sdk/types.js";

interface ParameterInfo {
Expand All @@ -29,7 +27,7 @@ export interface IntegrationTest {
randomDbName: () => string;
}

export function setupIntegrationTest(): IntegrationTest {
export function setupIntegrationTest(cfg: UserConfig = config): IntegrationTest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit

Suggested change
export function setupIntegrationTest(cfg: UserConfig = config): IntegrationTest {
export function setupIntegrationTest(userConfig: UserConfig = config): IntegrationTest {

let mongoCluster: runner.MongoCluster | undefined;
let mongoClient: MongoClient | undefined;

Expand Down Expand Up @@ -58,13 +56,7 @@ export function setupIntegrationTest(): IntegrationTest {
}
);

mcpServer = new Server({
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's beneficial for example for us to create a test server here as opposed to expanding the config

mcpServer: new McpServer({
name: "test-server",
version: "1.2.3",
}),
session: new Session(),
});
mcpServer = new Server(cfg);
await mcpServer.connect(serverTransport);
await mcpClient.connect(clientTransport);
});
Expand Down Expand Up @@ -315,14 +307,3 @@ export function validateThrowsForInvalidArguments(
}
});
}

export function describeAtlas(name: number | string | Function | jest.FunctionLike, fn: jest.EmptyFunction) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

drive-by: duplicated from atlasHelpers.ts

if (!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length) {
return describe.skip("atlas", () => {
describe(name, fn);
});
}
return describe("atlas", () => {
describe(name, fn);
});
}
Loading
Loading