Skip to content

Commit cc2bec5

Browse files
committed
feat: add readOnly flag
1 parent a6f9ce2 commit cc2bec5

File tree

6 files changed

+67
-0
lines changed

6 files changed

+67
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
150150
| `connectionString` | MongoDB connection string for direct database connections (optional users may choose to inform it on every tool call) |
151151
| `logPath` | Folder to store logs |
152152
| `disabledTools` | An array of tool names, operation types, and/or categories of tools that will be disabled. |
153+
| `readOnlyMode` | When set to true, only allows read and metadata operation types, disabling create/update/delete operations |
153154

154155
#### `logPath`
155156

@@ -181,6 +182,19 @@ Operation types:
181182
- `read` - Tools that read resources, such as find, aggregate, list clusters, etc.
182183
- `metadata` - Tools that read metadata, such as list databases, list collections, collection schema, etc.
183184

185+
#### Read-Only Mode
186+
187+
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.
188+
189+
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.
190+
191+
You can enable read-only mode using:
192+
193+
- **Environment variable**: `export MDB_MCP_READ_ONLY_MODE=true`
194+
- **Command-line argument**: `--readOnlyMode=true`
195+
196+
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.
197+
184198
### Atlas API Access
185199

186200
To use the Atlas API tools, you'll need to create a service account in MongoDB Atlas:

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface UserConfig {
2020
timeoutMS: number;
2121
};
2222
disabledTools: Array<string>;
23+
readOnlyMode?: boolean;
2324
}
2425

2526
const defaults: UserConfig = {
@@ -32,6 +33,7 @@ const defaults: UserConfig = {
3233
},
3334
disabledTools: [],
3435
telemetry: "disabled",
36+
readOnlyMode: false,
3537
};
3638

3739
export const config = {

src/logger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ class McpLogger extends LoggerBase {
106106
}
107107

108108
log(level: LogLevel, _: MongoLogId, context: string, message: string): void {
109+
// Only log if the server is connected
110+
if (this.server?.isConnected() === false) {
111+
return;
112+
}
113+
109114
void this.server.server.sendLoggingMessage({
110115
level,
111116
data: `[${context}]: ${message}`,

src/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ export class Server {
3636

3737
async connect(transport: Transport) {
3838
this.mcpServer.server.registerCapabilities({ logging: {} });
39+
40+
// Log read-only mode status if enabled
41+
if (this.userConfig.readOnlyMode) {
42+
logger.info(
43+
mongoLogId(1_000_005),
44+
"server",
45+
"Server starting in READ-ONLY mode. Only read and metadata operations will be available."
46+
);
47+
}
48+
3949
this.registerTools();
4050
this.registerResources();
4151

@@ -116,6 +126,7 @@ export class Server {
116126

117127
if (command === "start") {
118128
event.properties.startup_time_ms = commandDuration;
129+
event.properties.read_only_mode = this.userConfig.readOnlyMode || false;
119130
}
120131
if (command === "stop") {
121132
event.properties.runtime_duration_ms = Date.now() - this.startTime;

src/tools/tool.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ export abstract class ToolBase {
8686
// Checks if a tool is allowed to run based on the config
8787
protected verifyAllowed(): boolean {
8888
let errorClarification: string | undefined;
89+
90+
// Check read-only mode first
91+
if (this.config.readOnlyMode && !["read", "metadata"].includes(this.operationType)) {
92+
logger.debug(
93+
mongoLogId(1_000_010),
94+
"tool",
95+
`Prevented registration of ${this.name} because it has operation type \`${this.operationType}\` and read-only mode is enabled`
96+
);
97+
return false;
98+
}
99+
89100
if (this.config.disabledTools.includes(this.category)) {
90101
errorClarification = `its category, \`${this.category}\`,`;
91102
} else if (this.config.disabledTools.includes(this.operationType)) {

tests/integration/server.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,28 @@ describe("Server integration test", () => {
5858
});
5959
});
6060
});
61+
62+
describe("with read-only mode", () => {
63+
const integration = setupIntegrationTest({
64+
...config,
65+
readOnlyMode: true,
66+
});
67+
68+
it("should only register read and metadata operation tools when read-only mode is enabled", async () => {
69+
const tools = await integration.mcpClient().listTools();
70+
expectDefined(tools);
71+
expect(tools.tools.length).toBeGreaterThan(0);
72+
73+
// Check that we have some tools available (the read and metadata ones)
74+
expect(tools.tools.some((tool) => tool.name === "find")).toBe(true);
75+
expect(tools.tools.some((tool) => tool.name === "collection-schema")).toBe(true);
76+
expect(tools.tools.some((tool) => tool.name === "list-databases")).toBe(true);
77+
78+
// Check that non-read tools are NOT available
79+
expect(tools.tools.some((tool) => tool.name === "insert-one")).toBe(false);
80+
expect(tools.tools.some((tool) => tool.name === "update-many")).toBe(false);
81+
expect(tools.tools.some((tool) => tool.name === "delete-one")).toBe(false);
82+
expect(tools.tools.some((tool) => tool.name === "drop-collection")).toBe(false);
83+
});
84+
});
6185
});

0 commit comments

Comments
 (0)