Skip to content

refactor: move configuration to be passed #101

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 8 commits into from
Apr 23, 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
4 changes: 2 additions & 2 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import config from "../../config.js";
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
import { AccessToken, ClientCredentials } from "simple-oauth2";
import { ApiClientError } from "./apiClientError.js";
import { paths, operations } from "./openapi.js";
import { packageInfo } from "../../packageInfo.js";

const ATLAS_API_VERSION = "2025-03-12";

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/${packageInfo.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: 10 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ 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 { config } from "./config.js";
import { Session } from "./session.js";
import { Server } from "./server.js";
import { packageInfo } from "./packageInfo.js";

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

const server = new Server({
mcpServer,
session,
userConfig: 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
6 changes: 6 additions & 0 deletions src/packageInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import packageJson from "../package.json" with { type: "json" };

export const packageInfo = {
version: packageJson.version,
mcpServerName: "MongoDB MCP Server",
};
22 changes: 15 additions & 7 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ 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";

export interface ServerOptions {
session: Session;
userConfig: UserConfig;
mcpServer: McpServer;
}

export class Server {
public readonly session: Session;
private readonly mcpServer: McpServer;
private readonly userConfig: UserConfig;

constructor({ mcpServer, session }: { mcpServer: McpServer; session: Session }) {
this.mcpServer = mcpServer;
constructor({ session, mcpServer, userConfig }: ServerOptions) {
this.session = session;
this.mcpServer = mcpServer;
this.userConfig = userConfig;
}

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

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

await this.mcpServer.connect(transport);

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

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

private registerResources() {
if (config.connectionString) {
if (this.userConfig.connectionString) {
this.mcpServer.resource(
"connection-string",
"config://connection-string",
Expand All @@ -52,7 +60,7 @@ export class Server {
return {
contents: [
{
text: `Preconfigured connection string: ${config.connectionString}`,
text: `Preconfigured connection string: ${this.userConfig.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({ apiBaseUrl, apiClientId, apiClientSecret }: SessionOptions = {}) {
const credentials: ApiClientCredentials | undefined =
config.apiClientId && config.apiClientSecret
apiClientId && apiClientSecret
? {
clientId: config.apiClientId,
clientSecret: config.apiClientSecret,
clientId: apiClientId,
clientSecret: apiClientSecret,
}
: undefined;

this.apiClient = new ApiClient({
baseUrl: config.apiBaseUrl,
baseUrl: 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
Loading
Loading