import type { UserConfig } from "../common/config/userConfig.js";
import { packageInfo } from "../common/packageInfo.js";
import { Server } from "../server.js";
import { Session } from "../common/session.js";
import { Telemetry } from "../telemetry/telemetry.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { LoggerBase } from "../common/logger.js";
import { CompositeLogger, ConsoleLogger, DiskLogger, McpLogger } from "../common/logger.js";
import { ExportsManager } from "../common/exportsManager.js";
import { DeviceId } from "../helpers/deviceId.js";
import { Keychain } from "../common/keychain.js";
import { createMCPConnectionManager, type ConnectionManagerFactoryFn } from "../common/connectionManager.js";
import {
type ConnectionErrorHandler,
connectionErrorHandler as defaultConnectionErrorHandler,
} from "../common/connectionErrorHandler.js";
import type { CommonProperties } from "../telemetry/types.js";
import { Elicitation } from "../elicitation.js";
import type { AtlasLocalClientFactoryFn } from "../common/atlasLocal.js";
import { defaultCreateAtlasLocalClient } from "../common/atlasLocal.js";
import type { Client } from "@mongodb-js/atlas-local";
import { VectorSearchEmbeddingsManager } from "../common/search/vectorSearchEmbeddingsManager.js";
import type { ToolClass } from "../tools/tool.js";
import { applyConfigOverrides } from "../common/config/configOverrides.js";
export type RequestContext = {
headers?: Record<string, string | string[] | undefined>;
query?: Record<string, string | string[] | undefined>;
};
/**
* A function to dynamically generate `UserConfig` object, potentially unique to
* each MCP client session.
*
* The function is passed a config context object containing:
* 1. `userConfig`: The base `UserConfig` object that MongoDB MCP Server was
* started with, either through parsed CLI arguments or a static
* configuration injected through `TransportRunnerConfig`
* 2. `request`: An optional, `RequestContext` object, available only when
* MongoDB MCP server is running over HTTP transport, that contains headers
* and query parameters received in MCP session initialization object.
*
* @see {@link UserConfig} to inspect the properties available on `userConfig`
* object.
* @see {@link RequestContext} to inspect the properties available on
* `requestContext` object.
*/
type CreateSessionConfigFn = (context: {
userConfig: UserConfig;
request?: RequestContext;
}) => Promise<UserConfig> | UserConfig;
/**
* Configuration options for customizing how transport runners are initialized.
* This includes specifying the base user configuration, providing custom
* connection management, and other advanced options.
*
* You may want to customize this configuration if you need to:
* - Provide a custom user configuration for different environments or users.
* - Override the default connection management to MongoDB deployments.
* - Provide a specific list of tools to be registered with the MCP server.
*
* In most cases, just providing the `UserConfig` object is sufficient, but
* advanced use-cases (such as embedding the MCP server in another application
* or supporting custom authentication flows) may require customizing other
* `TransportRunnerConfig` options as well.
*/
export type TransportRunnerConfig = {
/**
* Base user configuration for the server.
*
* Can be generated by parsing CLI arguments, environment variables or
* written manually while conforming the interface requirements of
* `UserConfig`.
*
* To parse CLI arguments and environment variables in order to generate a
* `UserConfig` object, you can use `createUserConfig` function, also
* exported as `parseCliArgumentsAsUserConfig` through MCP server library
* exports.
*
* Optionally, you can also use `UserConfigSchema` (available through MCP
* server library exports) to create a default configuration -
* `UserConfigSchema.parse({})`.
*/
userConfig: UserConfig;
/**
* An optional factory function to generates an instance of
* `ConnectionManager`. When not provided, MongoDB MCP Server uses an
* internal implementation to manage connection to MongoDB deployments.
*
* Customize this only if the use-case involves handling the MongoDB
* connections differently and outside of MongoDB MCP server.
*/
createConnectionManager?: ConnectionManagerFactoryFn;
/**
* An optional function to handle connection related errors. When not
* provided, MongoDB MCP Server uses an internal implementation to handle
* the errors raised by internal implementation of `ConnectionManager`
* class.
*
* Customize this only if you need to handle the Connection errors different
* from the internal implementation or if you have provided a different
* implementation of `ConnectionManager` that might raise errors unknown to
* default internal connection error handler.
*/
connectionErrorHandler?: ConnectionErrorHandler;
/**
* An optional factory function to create a client for working with Atlas
* local deployments. When not provided, MongoDB MCP Server uses an internal
* implementation to create the local Atlas client.
*/
createAtlasLocalClient?: AtlasLocalClientFactoryFn;
/**
* An optional list of loggers to be used in addition to the default logger
* implementations. When not provided, MongoDB MCP Server will not utilize
* any loggers other than the default that it works with.
*
* Customize this only if the default enabled loggers (disk/stderr/mcp) are
* not covering your use-case.
*/
additionalLoggers?: LoggerBase[];
/**
* An optional key value pair of telemetry properties that are reported to
* the telemetry backend. Most, if not all, of the properties are captured
* automatically.
*/
telemetryProperties?: Partial<CommonProperties>;
/**
* An optional list of tools constructors to be registered to the MongoDB
* MCP Server.
*
* When not provided, MongoDB MCP Server will register all internal tools.
* When specified, **only** the tools in this list will be registered.
*
* This allows you to:
* - Register only custom tools (excluding all internal tools)
* - Register a subset of internal tools alongside custom tools
* - Register all internal tools plus custom tools
*
* To include internal tools, import them from `mongodb-mcp-server/tools`:
*
* ```typescript
* import { AllTools, AggregateTool, FindTool } from "mongodb-mcp-server/tools";
*
* // Register all internal tools plus custom tools
* tools: [...AllTools, MyCustomTool]
*
* // Register only specific MongoDB tools plus custom tools
* tools: [AggregateTool, FindTool, MyCustomTool]
*
* // Register all internal tools of mongodb category
* tools: [AllTools.filter((tool) => tool.category === "mongodb")]
* ```
*
* Note: Ensure that each tool has unique names otherwise the server will
* throw an error when initializing an MCP Client session. If you're using
* only the internal tools, then you don't have to worry about it unless,
* you've overridden the tool names.
*
* To ensure that you provide compliant tool implementations extend your
* tool implementation using `ToolBase` class and ensure that they conform
* to `ToolClass` type.
*
* @see {@link ToolClass} for the type that tool classes must conform to
* @see {@link ToolBase} for base class for all the tools
*/
tools?: ToolClass[];
/**
* An optional function to hook into session configuration lifecycle and
* provide session specific configuration (`UserConfig`).
*
* The function is called before each session is created, allowing you to:
* - Fetch configuration from external sources (secrets managers, APIs)
* - Apply user-specific permissions and limits
* - Modify connection strings dynamically
* - Validate authentication credentials
*
* This function is called for each new MCP client connection. For stdio
* transport, this is called once at server startup. For HTTP transport,
* this is called for each new session.
*/
createSessionConfig?: CreateSessionConfigFn;
};
export abstract class TransportRunnerBase {
public logger: LoggerBase;
public deviceId: DeviceId;
protected readonly userConfig: UserConfig;
private readonly createConnectionManager: ConnectionManagerFactoryFn;
private readonly connectionErrorHandler: ConnectionErrorHandler;
private readonly atlasLocalClient: Promise<Client | undefined>;
private readonly telemetryProperties: Partial<CommonProperties>;
private readonly tools?: ToolClass[];
private readonly createSessionConfig?: CreateSessionConfigFn;
protected constructor({
userConfig,
createConnectionManager = createMCPConnectionManager,
connectionErrorHandler = defaultConnectionErrorHandler,
createAtlasLocalClient = defaultCreateAtlasLocalClient,
additionalLoggers = [],
telemetryProperties = {},
tools,
createSessionConfig,
}: TransportRunnerConfig) {
this.userConfig = userConfig;
this.createConnectionManager = createConnectionManager;
this.connectionErrorHandler = connectionErrorHandler;
this.atlasLocalClient = createAtlasLocalClient();
this.telemetryProperties = telemetryProperties;
this.tools = tools;
this.createSessionConfig = createSessionConfig;
const loggers: LoggerBase[] = [...additionalLoggers];
if (this.userConfig.loggers.includes("stderr")) {
loggers.push(new ConsoleLogger(Keychain.root));
}
if (this.userConfig.loggers.includes("disk")) {
loggers.push(
new DiskLogger(
this.userConfig.logPath,
(err) => {
// If the disk logger fails to initialize, we log the error to stderr and exit
console.error("Error initializing disk logger:", err);
process.exit(1);
},
Keychain.root
)
);
}
this.logger = new CompositeLogger(...loggers);
this.deviceId = DeviceId.create(this.logger);
}
protected async setupServer(request?: RequestContext): Promise<Server> {
let userConfig: UserConfig = this.userConfig;
if (this.createSessionConfig) {
userConfig = await this.createSessionConfig({ userConfig, request });
} else {
userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request });
}
const mcpServer = new McpServer(
{
name: packageInfo.mcpServerName,
version: packageInfo.version,
},
{
instructions: TransportRunnerBase.getInstructions(userConfig),
}
);
const logger = new CompositeLogger(this.logger);
const exportsManager = ExportsManager.init(userConfig, logger);
const connectionManager = await this.createConnectionManager({
logger,
userConfig,
deviceId: this.deviceId,
});
const session = new Session({
userConfig,
atlasLocalClient: await this.atlasLocalClient,
logger,
exportsManager,
connectionManager,
keychain: Keychain.root,
vectorSearchEmbeddingsManager: new VectorSearchEmbeddingsManager(userConfig, connectionManager),
});
const telemetry = Telemetry.create(session, userConfig, this.deviceId, {
commonProperties: this.telemetryProperties,
});
const elicitation = new Elicitation({ server: mcpServer.server });
const result = new Server({
mcpServer,
session,
telemetry,
userConfig,
connectionErrorHandler: this.connectionErrorHandler,
elicitation,
tools: this.tools,
});
// We need to create the MCP logger after the server is constructed
// because it needs the server instance
if (userConfig.loggers.includes("mcp")) {
logger.addLogger(new McpLogger(result, Keychain.root));
}
return result;
}
abstract start(): Promise<void>;
abstract closeTransport(): Promise<void>;
async close(): Promise<void> {
try {
await this.closeTransport();
} finally {
this.deviceId.close();
}
}
private static getInstructions(config: UserConfig): string {
let instructions = `
This is the MongoDB MCP server.
`;
if (config.connectionString) {
instructions += `
This MCP server was configured with a MongoDB connection string, and you can assume that you are connected to a MongoDB cluster.
`;
}
if (config.apiClientId && config.apiClientSecret) {
instructions += `
This MCP server was configured with MongoDB Atlas API credentials.`;
}
return instructions;
}
}