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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
| `loggers` | `MDB_MCP_LOGGERS` | disk,mcp | Comma separated values, possible values are `mcp`, `disk` and `stderr`. See [Logger Options](#logger-options) for details. |
| `logPath` | `MDB_MCP_LOG_PATH` | see note\* | Folder to store logs. |
| `disabledTools` | `MDB_MCP_DISABLED_TOOLS` | <not set> | An array of tool names, operation types, and/or categories of tools that will be disabled. |
| `confirmationRequiredTools` | `MDB_MCP_CONFIRMATION_REQUIRED_TOOLS` | create-access-list,create-db-user,drop-database,drop-collection,delete-many | An array of tool names that require user confirmation before execution. **Requires the client to support [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation)**. |
| `readOnly` | `MDB_MCP_READ_ONLY` | false | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. |
| `indexCheck` | `MDB_MCP_INDEX_CHECK` | false | When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan. |
| `telemetry` | `MDB_MCP_TELEMETRY` | enabled | When set to disabled, disables telemetry collection. |
Expand Down Expand Up @@ -418,6 +419,14 @@ Operation types:
- `metadata` - Tools that read metadata, such as list databases, list collections, collection schema, etc.
- `connect` - Tools that allow you to connect or switch the connection to a MongoDB instance. If this is disabled, you will need to provide a connection string through the config when starting the server.

#### Require Confirmation

If your client supports [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation), you can set the MongoDB MCP server to request user confirmation before executing certain tools.

When a tool is marked as requiring confirmation, the server will send an elicitation request to the client. The client with elicitation support will then prompt the user for confirmation and send the response back to the server. If the client does not support elicitation, the tool will execute without confirmation.

You can set the `confirmationRequiredTools` configuration option to specify the names of tools which require confirmation. By default, the following tools have this setting enabled: `drop-database`, `drop-collection`, `delete-many`, `atlas-create-db-user`, `atlas-create-access-list`.

#### Read-Only Mode

The `readOnly` configuration option allows you to restrict the MCP server to only use tools with "read", "connect", and "metadata" operation types. When enabled, all tools that have "create", "update" or "delete" operation types will not be registered with the server.
Expand Down
10 changes: 10 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ export interface UserConfig extends CliOptions {
exportTimeoutMs: number;
exportCleanupIntervalMs: number;
connectionString?: string;
// TODO: Use a type tracking all tool names.
disabledTools: Array<string>;
confirmationRequiredTools: Array<string>;
readOnly?: boolean;
indexCheck?: boolean;
transport: "stdio" | "http";
Expand All @@ -183,6 +185,13 @@ export const defaultUserConfig: UserConfig = {
telemetry: "enabled",
readOnly: false,
indexCheck: false,
confirmationRequiredTools: [
"atlas-create-access-list",
"atlas-create-db-user",
"drop-database",
"drop-collection",
"delete-many",
],
transport: "stdio",
httpPort: 3000,
httpHost: "127.0.0.1",
Expand Down Expand Up @@ -442,6 +451,7 @@ export function setupUserConfig({

userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools);

if (userConfig.connectionString && userConfig.connectionSpecifier) {
const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);
Expand Down
59 changes: 59 additions & 0 deletions src/elicitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { PrimitiveSchemaDefinition } from "@modelcontextprotocol/sdk/types.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export class Elicitation {
private readonly server: McpServer["server"];
constructor({ server }: { server: McpServer["server"] }) {
this.server = server;
}

/**
* Checks if the client supports elicitation capabilities.
* @returns True if the client supports elicitation, false otherwise.
*/
public supportsElicitation(): boolean {
const clientCapabilities = this.server.getClientCapabilities();
return clientCapabilities?.elicitation !== undefined;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If one wants to be paranoid... there's might be a chance some client will claim it supports elicitation while not supporting it. Then this would likely make this tool a confusing no-op.
That said this would be client's fault and one could unblock themselves by setting confirmation required tools to nothing.

}

/**
* Requests a boolean confirmation from the user.
* @param message - The message to display to the user.
* @returns True if the user confirms the action or the client does not support elicitation, false otherwise.
*/
public async requestConfirmation(message: string): Promise<boolean> {
if (!this.supportsElicitation()) {
return true;
}

const result = await this.server.elicitInput({
message,
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
return result.action === "accept" && result.content?.confirmation === "Yes";
}

/**
* The schema for the confirmation question.
* TODO: In the future would be good to use Zod 4's toJSONSchema() to generate the schema.
*/
public static CONFIRMATION_SCHEMA: MCPElicitationSchema = {
type: "object",
properties: {
confirmation: {
type: "string",
title: "Would you like to confirm?",
description: "Would you like to confirm?",
enum: ["Yes", "No"],
enumNames: ["Yes, I confirm", "No, I do not confirm"],
},
},
required: ["confirmation"],
};
}

export type MCPElicitationSchema = {
type: "object";
properties: Record<string, PrimitiveSchemaDefinition>;
required?: string[];
};
16 changes: 13 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ import type { ToolBase } from "./tools/tool.js";
import { validateConnectionString } from "./helpers/connectionOptions.js";
import { packageInfo } from "./common/packageInfo.js";
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
import type { Elicitation } from "./elicitation.js";

export interface ServerOptions {
session: Session;
userConfig: UserConfig;
mcpServer: McpServer;
telemetry: Telemetry;
elicitation: Elicitation;
connectionErrorHandler: ConnectionErrorHandler;
}

Expand All @@ -36,6 +38,7 @@ export class Server {
public readonly mcpServer: McpServer;
private readonly telemetry: Telemetry;
public readonly userConfig: UserConfig;
public readonly elicitation: Elicitation;
public readonly tools: ToolBase[] = [];
public readonly connectionErrorHandler: ConnectionErrorHandler;

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

constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler }: ServerOptions) {
constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler, elicitation }: ServerOptions) {
this.startTime = Date.now();
this.session = session;
this.telemetry = telemetry;
this.mcpServer = mcpServer;
this.userConfig = userConfig;
this.elicitation = elicitation;
this.connectionErrorHandler = connectionErrorHandler;
}

Expand Down Expand Up @@ -184,6 +188,7 @@ export class Server {
event.properties.startup_time_ms = commandDuration;
event.properties.read_only_mode = this.userConfig.readOnly || false;
event.properties.disabled_tools = this.userConfig.disabledTools || [];
event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
}
if (command === "stop") {
event.properties.runtime_duration_ms = Date.now() - this.startTime;
Expand All @@ -193,12 +198,17 @@ export class Server {
}
}

this.telemetry.emitEvents([event]);
void this.telemetry.emitEvents([event]);
}

private registerTools(): void {
for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) {
const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
const tool = new toolConstructor({
session: this.session,
config: this.userConfig,
telemetry: this.telemetry,
elicitation: this.elicitation,
});
if (tool.register(this)) {
this.tools.push(tool);
}
Expand Down
1 change: 1 addition & 0 deletions src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type ServerEventProperties = {
runtime_duration_ms?: number;
read_only_mode?: boolean;
disabled_tools?: string[];
confirmation_required_tools?: string[];
};

export type ServerEvent = TelemetryEvent<ServerEventProperties>;
Expand Down
27 changes: 27 additions & 0 deletions src/tools/atlas/create/createAccessList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,31 @@ export class CreateAccessListTool extends AtlasToolBase {
],
};
}

protected getConfirmationMessage({
projectId,
ipAddresses,
cidrBlocks,
comment,
currentIpAddress,
}: ToolArgs<typeof this.argsShape>): string {
const accessDescription = [];
if (ipAddresses?.length) {
accessDescription.push(`- **IP addresses**: ${ipAddresses.join(", ")}`);
}
if (cidrBlocks?.length) {
accessDescription.push(`- **CIDR blocks**: ${cidrBlocks.join(", ")}`);
}
if (currentIpAddress) {
accessDescription.push("- **Current IP address**");
}

return (
`You are about to add the following entries to the access list for Atlas project "${projectId}":\n\n` +
accessDescription.join("\n") +
`\n\n**Comment**: ${comment || DEFAULT_ACCESS_LIST_COMMENT}\n\n` +
"This will allow network access to your MongoDB Atlas clusters from these IP addresses/ranges. " +
"Do you want to proceed?"
);
}
}
18 changes: 18 additions & 0 deletions src/tools/atlas/create/createDBUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,22 @@ export class CreateDBUserTool extends AtlasToolBase {
],
};
}

protected getConfirmationMessage({
projectId,
username,
password,
roles,
clusters,
}: ToolArgs<typeof this.argsShape>): string {
return (
`You are about to create a database user in Atlas project \`${projectId}\`:\n\n` +
`**Username**: \`${username}\`\n\n` +
`**Password**: ${password ? "(User-provided password)" : "(Auto-generated secure password)"}\n\n` +
`**Roles**: ${roles.map((role) => `${role.roleName}${role.collectionName ? ` on ${role.databaseName}.${role.collectionName}` : ` on ${role.databaseName}`}`).join(", ")}\n\n` +
`**Cluster Access**: ${clusters?.length ? clusters.join(", ") : "All clusters in the project"}\n\n` +
"This will create a new database user with the specified permissions. " +
"**Do you confirm the execution of the action?**"
);
}
}
9 changes: 3 additions & 6 deletions src/tools/mongodb/connect/connect.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { ToolArgs, OperationType, ToolConstructorParams } from "../../tool.js";
import assert from "assert";
import type { UserConfig } from "../../../common/config.js";
import type { Telemetry } from "../../../telemetry/telemetry.js";
import type { Session } from "../../../common/session.js";
import type { Server } from "../../../server.js";

const disconnectedSchema = z
Expand Down Expand Up @@ -44,8 +41,8 @@ export class ConnectTool extends MongoDBToolBase {

public operationType: OperationType = "connect";

constructor(session: Session, config: UserConfig, telemetry: Telemetry) {
super(session, config, telemetry);
constructor({ session, config, telemetry, elicitation }: ToolConstructorParams) {
super({ session, config, telemetry, elicitation });
session.on("connect", () => {
this.updateMetadata();
});
Expand Down
14 changes: 14 additions & 0 deletions src/tools/mongodb/delete/deleteMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
import { EJSON } from "bson";

export class DeleteManyTool extends MongoDBToolBase {
public name = "delete-many";
Expand Down Expand Up @@ -55,4 +56,17 @@ export class DeleteManyTool extends MongoDBToolBase {
],
};
}

protected getConfirmationMessage({ database, collection, filter }: ToolArgs<typeof this.argsShape>): string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

q1: Do we want to get confirmation for deletions even when we have a filter specified? I thought we only want to do it if you try and delete everything in the collection.
q2: Do we want to give the approximate count of documents that will be deleted in the message?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

q1: Do we want to get confirmation for deletions even when we have a filter specified? I thought we only want to do it if you try and delete everything in the collection.

I don't remember the discussions around this but maybe we still should? I think with deletions it's still good to review the filter and whatnot.

q2: Do we want to give the approximate count of documents that will be deleted in the message?

This does seem useful but could this be an expensive operation for extremely large collections? Seems like a potentially problematic side effect.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would not expect the MCP server to be used for perf critical applications, so am more inclined to think it's a worthwhile tradeoff, but don't feel strongly either way.

Copy link
Collaborator Author

@gagik gagik Sep 12, 2025

Choose a reason for hiding this comment

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

I do think running other DB functions for the sake of a confirmation message is a bit unexpected but probably not critically terrible yeah.
I'm going to leave it as is mainly because it'd require some changes to make the get confirmation async (which is fine and doomed to happen), so we can revisit later

const filterDescription =
filter && Object.keys(filter).length > 0 ? EJSON.stringify(filter) : "All documents (No filter)";
return (
`You are about to delete documents from the \`${collection}\` collection in the \`${database}\` database:\n\n` +
"```json\n" +
`{ "filter": ${filterDescription} }\n` +
"```\n\n" +
"This operation will permanently remove all documents matching the filter.\n\n" +
"**Do you confirm the execution of the action?**"
);
}
}
8 changes: 8 additions & 0 deletions src/tools/mongodb/delete/dropCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ export class DropCollectionTool extends MongoDBToolBase {
],
};
}

protected getConfirmationMessage({ database, collection }: ToolArgs<typeof this.argsShape>): string {
return (
`You are about to drop the \`${collection}\` collection from the \`${database}\` database:\n\n` +
"This operation will permanently remove the collection and all its data, including indexes.\n\n" +
"**Do you confirm the execution of the action?**"
);
}
}
8 changes: 8 additions & 0 deletions src/tools/mongodb/delete/dropDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@ export class DropDatabaseTool extends MongoDBToolBase {
],
};
}

protected getConfirmationMessage({ database }: ToolArgs<typeof this.argsShape>): string {
return (
`You are about to drop the \`${database}\` database:\n\n` +
"This operation will permanently remove the database and ALL its collections, documents, and indexes.\n\n" +
"**Do you confirm the execution of the action?**"
);
}
}
Loading
Loading