From cc2bec5840bab3adee8b5c60f74e28742705309f Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 18:31:44 +0100 Subject: [PATCH 01/10] feat: add readOnly flag --- README.md | 14 ++++++++++++++ src/config.ts | 2 ++ src/logger.ts | 5 +++++ src/server.ts | 11 +++++++++++ src/tools/tool.ts | 11 +++++++++++ tests/integration/server.test.ts | 24 ++++++++++++++++++++++++ 6 files changed, 67 insertions(+) diff --git a/README.md b/README.md index 8c625566..7352a61b 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `connectionString` | MongoDB connection string for direct database connections (optional users may choose to inform it on every tool call) | | `logPath` | Folder to store logs | | `disabledTools` | An array of tool names, operation types, and/or categories of tools that will be disabled. | +| `readOnlyMode` | When set to true, only allows read and metadata operation types, disabling create/update/delete operations | #### `logPath` @@ -181,6 +182,19 @@ Operation types: - `read` - Tools that read resources, such as find, aggregate, list clusters, etc. - `metadata` - Tools that read metadata, such as list databases, list collections, collection schema, etc. +#### Read-Only Mode + +The `readOnlyMode` configuration option allows you to restrict the MCP server to only use tools with "read" and "metadata" operation types. When enabled, all tools that have "create", "update", "delete", or "cluster" operation types will not be registered with the server. + +This is useful for scenarios where you want to provide access to MongoDB data for analysis without allowing any modifications to the data or infrastructure. + +You can enable read-only mode using: + +- **Environment variable**: `export MDB_MCP_READ_ONLY_MODE=true` +- **Command-line argument**: `--readOnlyMode=true` + +When read-only mode is active, you'll see a message in the server logs indicating which tools were prevented from registering due to this restriction. + ### Atlas API Access To use the Atlas API tools, you'll need to create a service account in MongoDB Atlas: diff --git a/src/config.ts b/src/config.ts index dabfd789..9e97cd11 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,7 @@ export interface UserConfig { timeoutMS: number; }; disabledTools: Array; + readOnlyMode?: boolean; } const defaults: UserConfig = { @@ -32,6 +33,7 @@ const defaults: UserConfig = { }, disabledTools: [], telemetry: "disabled", + readOnlyMode: false, }; export const config = { diff --git a/src/logger.ts b/src/logger.ts index 0ff292cc..99b50921 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -106,6 +106,11 @@ class McpLogger extends LoggerBase { } log(level: LogLevel, _: MongoLogId, context: string, message: string): void { + // Only log if the server is connected + if (this.server?.isConnected() === false) { + return; + } + void this.server.server.sendLoggingMessage({ level, data: `[${context}]: ${message}`, diff --git a/src/server.ts b/src/server.ts index 1bec50b0..ca997ea9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -36,6 +36,16 @@ export class Server { async connect(transport: Transport) { this.mcpServer.server.registerCapabilities({ logging: {} }); + + // Log read-only mode status if enabled + if (this.userConfig.readOnlyMode) { + logger.info( + mongoLogId(1_000_005), + "server", + "Server starting in READ-ONLY mode. Only read and metadata operations will be available." + ); + } + this.registerTools(); this.registerResources(); @@ -116,6 +126,7 @@ export class Server { if (command === "start") { event.properties.startup_time_ms = commandDuration; + event.properties.read_only_mode = this.userConfig.readOnlyMode || false; } if (command === "stop") { event.properties.runtime_duration_ms = Date.now() - this.startTime; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index a37c7224..2da47d6f 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -86,6 +86,17 @@ export abstract class ToolBase { // Checks if a tool is allowed to run based on the config protected verifyAllowed(): boolean { let errorClarification: string | undefined; + + // Check read-only mode first + if (this.config.readOnlyMode && !["read", "metadata"].includes(this.operationType)) { + logger.debug( + mongoLogId(1_000_010), + "tool", + `Prevented registration of ${this.name} because it has operation type \`${this.operationType}\` and read-only mode is enabled` + ); + return false; + } + if (this.config.disabledTools.includes(this.category)) { errorClarification = `its category, \`${this.category}\`,`; } else if (this.config.disabledTools.includes(this.operationType)) { diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index 3d12f129..dec74d51 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -58,4 +58,28 @@ describe("Server integration test", () => { }); }); }); + + describe("with read-only mode", () => { + const integration = setupIntegrationTest({ + ...config, + readOnlyMode: true, + }); + + it("should only register read and metadata operation tools when read-only mode is enabled", async () => { + const tools = await integration.mcpClient().listTools(); + expectDefined(tools); + expect(tools.tools.length).toBeGreaterThan(0); + + // Check that we have some tools available (the read and metadata ones) + expect(tools.tools.some((tool) => tool.name === "find")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "collection-schema")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "list-databases")).toBe(true); + + // Check that non-read tools are NOT available + expect(tools.tools.some((tool) => tool.name === "insert-one")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "update-many")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "delete-one")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "drop-collection")).toBe(false); + }); + }); }); From e34b8735cca563fe7c30b5d67375e2d063c4c0e4 Mon Sep 17 00:00:00 2001 From: Bianca Lisle <40155621+blva@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:53:14 +0100 Subject: [PATCH 02/10] Update src/logger.ts Co-authored-by: Filipe Constantinov Menezes --- src/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.ts b/src/logger.ts index 99b50921..94c50f0a 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -107,7 +107,7 @@ class McpLogger extends LoggerBase { log(level: LogLevel, _: MongoLogId, context: string, message: string): void { // Only log if the server is connected - if (this.server?.isConnected() === false) { + if (!this.server?.isConnected()) { return; } From 937edfca945661f96fc90ce765415480f8d203b3 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 18:56:05 +0100 Subject: [PATCH 03/10] address comment: update flag --- README.md | 6 +++--- src/config.ts | 4 ++-- src/server.ts | 4 ++-- src/tools/tool.ts | 2 +- tests/integration/server.test.ts | 6 +++++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7352a61b..61617b8e 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `connectionString` | MongoDB connection string for direct database connections (optional users may choose to inform it on every tool call) | | `logPath` | Folder to store logs | | `disabledTools` | An array of tool names, operation types, and/or categories of tools that will be disabled. | -| `readOnlyMode` | When set to true, only allows read and metadata operation types, disabling create/update/delete operations | +| `readOnly` | When set to true, only allows read and metadata operation types, disabling create/update/delete operations | #### `logPath` @@ -184,14 +184,14 @@ Operation types: #### Read-Only Mode -The `readOnlyMode` configuration option allows you to restrict the MCP server to only use tools with "read" and "metadata" operation types. When enabled, all tools that have "create", "update", "delete", or "cluster" operation types will not be registered with the server. +The `readOnly` configuration option allows you to restrict the MCP server to only use tools with "read" and "metadata" operation types. When enabled, all tools that have "create", "update", "delete", or "cluster" operation types will not be registered with the server. This is useful for scenarios where you want to provide access to MongoDB data for analysis without allowing any modifications to the data or infrastructure. You can enable read-only mode using: - **Environment variable**: `export MDB_MCP_READ_ONLY_MODE=true` -- **Command-line argument**: `--readOnlyMode=true` +- **Command-line argument**: `--readOnly=true` When read-only mode is active, you'll see a message in the server logs indicating which tools were prevented from registering due to this restriction. diff --git a/src/config.ts b/src/config.ts index 9e97cd11..676247a5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,7 +20,7 @@ export interface UserConfig { timeoutMS: number; }; disabledTools: Array; - readOnlyMode?: boolean; + readOnly?: boolean; } const defaults: UserConfig = { @@ -33,7 +33,7 @@ const defaults: UserConfig = { }, disabledTools: [], telemetry: "disabled", - readOnlyMode: false, + readOnly: false, }; export const config = { diff --git a/src/server.ts b/src/server.ts index ca997ea9..64b99e55 100644 --- a/src/server.ts +++ b/src/server.ts @@ -38,7 +38,7 @@ export class Server { this.mcpServer.server.registerCapabilities({ logging: {} }); // Log read-only mode status if enabled - if (this.userConfig.readOnlyMode) { + if (this.userConfig.readOnly) { logger.info( mongoLogId(1_000_005), "server", @@ -126,7 +126,7 @@ export class Server { if (command === "start") { event.properties.startup_time_ms = commandDuration; - event.properties.read_only_mode = this.userConfig.readOnlyMode || false; + event.properties.read_only_mode = this.userConfig.readOnly || false; } if (command === "stop") { event.properties.runtime_duration_ms = Date.now() - this.startTime; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 2da47d6f..830f37bd 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -88,7 +88,7 @@ export abstract class ToolBase { let errorClarification: string | undefined; // Check read-only mode first - if (this.config.readOnlyMode && !["read", "metadata"].includes(this.operationType)) { + if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) { logger.debug( mongoLogId(1_000_010), "tool", diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index dec74d51..b5c9e98f 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -62,7 +62,9 @@ describe("Server integration test", () => { describe("with read-only mode", () => { const integration = setupIntegrationTest({ ...config, - readOnlyMode: true, + readOnly: true, + apiClientId: "test", + apiClientSecret: "test", }); it("should only register read and metadata operation tools when read-only mode is enabled", async () => { @@ -74,6 +76,8 @@ describe("Server integration test", () => { expect(tools.tools.some((tool) => tool.name === "find")).toBe(true); expect(tools.tools.some((tool) => tool.name === "collection-schema")).toBe(true); expect(tools.tools.some((tool) => tool.name === "list-databases")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "atlas-list-orgs")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "atlas-list-projects")).toBe(true); // Check that non-read tools are NOT available expect(tools.tools.some((tool) => tool.name === "insert-one")).toBe(false); From c6a928cd927663a97c40860189e3366f29ed9d7e Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 18:57:03 +0100 Subject: [PATCH 04/10] update env variable --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61617b8e..60f38691 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ This is useful for scenarios where you want to provide access to MongoDB data fo You can enable read-only mode using: -- **Environment variable**: `export MDB_MCP_READ_ONLY_MODE=true` +- **Environment variable**: `export MDB_MCP_READ_ONLY=true` - **Command-line argument**: `--readOnly=true` When read-only mode is active, you'll see a message in the server logs indicating which tools were prevented from registering due to this restriction. From 9270c4f725bea80733c6fe1cfd1c4cfee3f97ead Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 22:54:20 +0100 Subject: [PATCH 05/10] address comment: remove cluster operation type --- README.md | 2 +- src/tools/tool.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60f38691..d9f4e03f 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ Operation types: #### Read-Only Mode -The `readOnly` configuration option allows you to restrict the MCP server to only use tools with "read" and "metadata" operation types. When enabled, all tools that have "create", "update", "delete", or "cluster" operation types will not be registered with the server. +The `readOnly` configuration option allows you to restrict the MCP server to only use tools with "read" and "metadata" operation types. When enabled, all tools that have "create", "update" or "delete" operation types will not be registered with the server. This is useful for scenarios where you want to provide access to MongoDB data for analysis without allowing any modifications to the data or infrastructure. diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 830f37bd..4ac0da6e 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -10,7 +10,7 @@ import { UserConfig } from "../config.js"; export type ToolArgs = z.objectOutputType; -export type OperationType = "metadata" | "read" | "create" | "delete" | "update" | "cluster"; +export type OperationType = "metadata" | "read" | "create" | "delete" | "update"; export type ToolCategory = "mongodb" | "atlas"; export abstract class ToolBase { From a2819fb9c25364194e268f08ea8696065e2779fb Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 23:02:13 +0100 Subject: [PATCH 06/10] address comment: logId --- src/server.ts | 9 --------- src/tools/tool.ts | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/server.ts b/src/server.ts index 9038f710..d292a12e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -36,15 +36,6 @@ export class Server { async connect(transport: Transport) { this.mcpServer.server.registerCapabilities({ logging: {} }); - // Log read-only mode status if enabled - if (this.userConfig.readOnly) { - logger.info( - mongoLogId(1_000_005), - "server", - "Server starting in READ-ONLY mode. Only read and metadata operations will be available." - ); - } - this.registerTools(); this.registerResources(); diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 21b2f3a7..4a9f9f6b 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -113,7 +113,7 @@ export abstract class ToolBase { // Check read-only mode first if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) { logger.debug( - mongoLogId(1_000_010), + LogId.toolDisabled, "tool", `Prevented registration of ${this.name} because it has operation type \`${this.operationType}\` and read-only mode is enabled` ); From de8e99969ccb3f0ea41726ea65fa76b674b7d334 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 23:13:21 +0100 Subject: [PATCH 07/10] chore: fix test --- tests/integration/server.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index d73a52cc..b9072dca 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -54,12 +54,12 @@ describe("Server integration test", () => { }); describe("with read-only mode", () => { - const integration = setupIntegrationTest({ + const integration = setupIntegrationTest(() => ({ ...config, readOnly: true, apiClientId: "test", apiClientSecret: "test", - }); + })); it("should only register read and metadata operation tools when read-only mode is enabled", async () => { const tools = await integration.mcpClient().listTools(); From efbf964654e9fac0b628894d0d27576e9e8f746f Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 23:18:52 +0100 Subject: [PATCH 08/10] address comment: boolena flag --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d9f4e03f..479c04bd 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ This is useful for scenarios where you want to provide access to MongoDB data fo You can enable read-only mode using: - **Environment variable**: `export MDB_MCP_READ_ONLY=true` -- **Command-line argument**: `--readOnly=true` +- **Command-line argument**: `--readOnly` When read-only mode is active, you'll see a message in the server logs indicating which tools were prevented from registering due to this restriction. @@ -235,6 +235,7 @@ export MDB_MCP_API_CLIENT_SECRET="your-atlas-client-secret" export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" export MDB_MCP_LOG_PATH="/path/to/logs" + ``` #### Command-Line Arguments From 17ab204b937042e65e1fb29d1cb9e0d722d33044 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 23:21:29 +0100 Subject: [PATCH 09/10] address comment: emit disabled tools --- src/server.ts | 1 + src/telemetry/types.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/server.ts b/src/server.ts index d292a12e..3d4802f3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -117,6 +117,7 @@ export class Server { if (command === "start") { event.properties.startup_time_ms = commandDuration; event.properties.read_only_mode = this.userConfig.readOnly || false; + event.properties.disallowed_tools = this.userConfig.disabledTools || []; } if (command === "stop") { event.properties.runtime_duration_ms = Date.now() - this.startTime; diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index bd1ef2a1..5199590f 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -47,6 +47,8 @@ export interface ServerEvent extends BaseEvent { reason?: string; startup_time_ms?: number; runtime_duration_ms?: number; + read_only_mode?: boolean; + disabled_tools?: string[]; } & BaseEvent["properties"]; } From 9612966122c98dffbf3582c9f6c04ffeddd7cf98 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 25 Apr 2025 23:27:40 +0100 Subject: [PATCH 10/10] use reror clarification --- src/tools/tool.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 4a9f9f6b..f8091deb 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -112,15 +112,8 @@ export abstract class ToolBase { // Check read-only mode first if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) { - logger.debug( - LogId.toolDisabled, - "tool", - `Prevented registration of ${this.name} because it has operation type \`${this.operationType}\` and read-only mode is enabled` - ); - return false; - } - - if (this.config.disabledTools.includes(this.category)) { + errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`; + } else if (this.config.disabledTools.includes(this.category)) { errorClarification = `its category, \`${this.category}\`,`; } else if (this.config.disabledTools.includes(this.operationType)) { errorClarification = `its operation type, \`${this.operationType}\`,`;