Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
| `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | 300000 | Time in milliseconds after which an export is considered expired and eligible for cleanup. |
| `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | 120000 | Time in milliseconds between export cleanup cycles that remove expired export files. |
| `atlasTemporaryDatabaseUserLifetimeMs` | `MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS` | 14400000 | Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted. |
| `voyageApiKey` | `MDB_VOYAGE_API_KEY` | <not set> | API key for communicating with Voyage AI. Used for generating embeddings for Vector search. |

#### Logger Options

Expand Down
3 changes: 3 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const OPTIONS = {
"exportsPath",
"exportTimeoutMs",
"exportCleanupIntervalMs",
"voyageApiKey",
],
boolean: [
"apiDeprecationErrors",
Expand Down Expand Up @@ -181,6 +182,7 @@ export interface UserConfig extends CliOptions {
maxDocumentsPerQuery: number;
maxBytesPerQuery: number;
atlasTemporaryDatabaseUserLifetimeMs: number;
voyageApiKey: string;
}

export const defaultUserConfig: UserConfig = {
Expand Down Expand Up @@ -210,6 +212,7 @@ export const defaultUserConfig: UserConfig = {
maxDocumentsPerQuery: 100, // By default, we only fetch a maximum 100 documents per query / aggregation
maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation
atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
voyageApiKey: "",
};

export const config = setupUserConfig({
Expand Down
17 changes: 14 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
UnsubscribeRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import assert from "assert";
import type { ToolBase } from "./tools/tool.js";
import type { ToolBase, ToolConstructorParams } from "./tools/tool.js";
import { validateConnectionString } from "./helpers/connectionOptions.js";
import { packageInfo } from "./common/packageInfo.js";
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
Expand All @@ -31,6 +31,7 @@ export interface ServerOptions {
telemetry: Telemetry;
elicitation: Elicitation;
connectionErrorHandler: ConnectionErrorHandler;
toolConstructors?: (new (params: ToolConstructorParams) => ToolBase)[];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is only to make it easy to test tool registration behaviour.

}

export class Server {
Expand All @@ -39,6 +40,7 @@ export class Server {
private readonly telemetry: Telemetry;
public readonly userConfig: UserConfig;
public readonly elicitation: Elicitation;
private readonly toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[];
public readonly tools: ToolBase[] = [];
public readonly connectionErrorHandler: ConnectionErrorHandler;

Expand All @@ -51,14 +53,23 @@ export class Server {
private readonly startTime: number;
private readonly subscriptions = new Set<string>();

constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler, elicitation }: ServerOptions) {
constructor({
session,
mcpServer,
userConfig,
telemetry,
connectionErrorHandler,
elicitation,
toolConstructors,
}: ServerOptions) {
this.startTime = Date.now();
this.session = session;
this.telemetry = telemetry;
this.mcpServer = mcpServer;
this.userConfig = userConfig;
this.elicitation = elicitation;
this.connectionErrorHandler = connectionErrorHandler;
this.toolConstructors = toolConstructors ?? [...AtlasTools, ...MongoDbTools];
}

async connect(transport: Transport): Promise<void> {
Expand Down Expand Up @@ -206,7 +217,7 @@ export class Server {
}

private registerTools(): void {
for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) {
for (const toolConstructor of this.toolConstructors) {
const tool = new toolConstructor({
session: this.session,
config: this.userConfig,
Expand Down
82 changes: 54 additions & 28 deletions tests/integration/tools/mongodb/mongodbTool.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { vi, it, describe, beforeEach, afterEach, type MockedFunction, afterAll, expect } from "vitest";
import { vi, it, describe, beforeEach, afterEach, afterAll, expect } from "vitest";
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { MongoDBToolBase } from "../../../../src/tools/mongodb/mongodbTool.js";
import { type OperationType } from "../../../../src/tools/tool.js";
import { type ToolBase, type ToolConstructorParams, type OperationType } from "../../../../src/tools/tool.js";
import { defaultDriverOptions, type UserConfig } from "../../../../src/common/config.js";
import { MCPConnectionManager } from "../../../../src/common/connectionManager.js";
import { Session } from "../../../../src/common/session.js";
Expand All @@ -19,6 +19,7 @@ import { setupMongoDBIntegrationTest } from "./mongodbHelpers.js";
import { ErrorCodes } from "../../../../src/common/errors.js";
import { Keychain } from "../../../../src/common/keychain.js";
import { Elicitation } from "../../../../src/elicitation.js";
import { MongoDbTools } from "../../../../src/tools/mongodb/tools.js";

const injectedErrorHandler: ConnectionErrorHandler = (error) => {
switch (error.code) {
Expand Down Expand Up @@ -51,29 +52,45 @@ const injectedErrorHandler: ConnectionErrorHandler = (error) => {
}
};

describe("MongoDBTool implementations", () => {
const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []);
const executeStub: MockedFunction<() => Promise<CallToolResult>> = vi
.fn()
.mockResolvedValue({ content: [{ type: "text", text: "Something" }] });
class RandomTool extends MongoDBToolBase {
name = "Random";
operationType: OperationType = "read";
protected description = "This is a tool.";
protected argsShape = {};
public async execute(): Promise<CallToolResult> {
await this.ensureConnected();
return executeStub();
class RandomTool extends MongoDBToolBase {
name = "Random";
operationType: OperationType = "read";
protected description = "This is a tool.";
protected argsShape = {};
public async execute(): Promise<CallToolResult> {
await this.ensureConnected();
return { content: [{ type: "text", text: "Something" }] };
}
}

class UnusableVoyageTool extends MongoDBToolBase {
name = "UnusableVoyageTool";
operationType: OperationType = "read";
protected description = "This is a Voyage tool.";
protected argsShape = {};

override verifyAllowed(): boolean {
if (this.config.voyageApiKey.trim()) {
return super.verifyAllowed();
}
return false;
}
public async execute(): Promise<CallToolResult> {
await this.ensureConnected();
return { content: [{ type: "text", text: "Something" }] };
}
}

describe("MongoDBTool implementations", () => {
const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []);

let tool: RandomTool | undefined;
let mcpClient: Client | undefined;
let mcpServer: Server | undefined;
let deviceId: DeviceId | undefined;

async function cleanupAndStartServer(
config: Partial<UserConfig> | undefined = {},
toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[] = [...MongoDbTools, RandomTool],
errorHandler: ConnectionErrorHandler | undefined = connectionErrorHandler
): Promise<void> {
await cleanup();
Expand Down Expand Up @@ -126,16 +143,9 @@ describe("MongoDBTool implementations", () => {
mcpServer: internalMcpServer,
connectionErrorHandler: errorHandler,
elicitation,
toolConstructors,
});

tool = new RandomTool({
session,
config: userConfig,
telemetry,
elicitation,
});
tool.register(mcpServer);

await mcpServer.connect(serverTransport);
await mcpClient.connect(clientTransport);
}
Expand All @@ -150,8 +160,6 @@ describe("MongoDBTool implementations", () => {

deviceId?.close();
deviceId = undefined;

tool = undefined;
}

beforeEach(async () => {
Expand Down Expand Up @@ -232,7 +240,7 @@ describe("MongoDBTool implementations", () => {

describe("when MCP is using injected connection error handler", () => {
beforeEach(async () => {
await cleanupAndStartServer(defaultTestConfig, injectedErrorHandler);
await cleanupAndStartServer(defaultTestConfig, [...MongoDbTools, RandomTool], injectedErrorHandler);
});

describe("and comes across a MongoDB Error - NotConnectedToMongoDB", () => {
Expand All @@ -256,7 +264,11 @@ describe("MongoDBTool implementations", () => {
describe("and comes across a MongoDB Error - MisconfiguredConnectionString", () => {
it("should handle the error", async () => {
// This is a misconfigured connection string
await cleanupAndStartServer({ connectionString: "mongodb://localhost:1234" }, injectedErrorHandler);
await cleanupAndStartServer(
{ connectionString: "mongodb://localhost:1234" },
[...MongoDbTools, RandomTool],
injectedErrorHandler
);
const toolResponse = await mcpClient?.callTool({
name: "Random",
arguments: {},
Expand All @@ -278,6 +290,7 @@ describe("MongoDBTool implementations", () => {
// This is a misconfigured connection string
await cleanupAndStartServer(
{ connectionString: mdbIntegration.connectionString(), indexCheck: true },
[...MongoDbTools, RandomTool],
injectedErrorHandler
);
const toolResponse = await mcpClient?.callTool({
Expand All @@ -299,4 +312,17 @@ describe("MongoDBTool implementations", () => {
});
});
});

describe("when a tool is not usable", () => {
it("should not even be registered", async () => {
await cleanupAndStartServer(
{ connectionString: mdbIntegration.connectionString(), indexCheck: true },
[RandomTool, UnusableVoyageTool],
injectedErrorHandler
);
const tools = await mcpClient?.listTools({});
expect(tools?.tools).toHaveLength(1);
expect(tools?.tools.find((tool) => tool.name === "UnusableTool")).toBeUndefined();
});
});
});
Loading