Skip to content

Commit f74e753

Browse files
committed
feat(elicitation): add user consent configuration through elicitation MCP-185
Adds an option to require confirmation for certain tools using the new elicitation API. This is not supported by most client yet, only notably VSCode. Clients which support it will see a confirmation option with a summary before the action is run. If the client doesn't support elicitation, the action will simply be auto-approved. This option can be confirmed with `confirmationRequiredTools` and has a default set of `drop-database`, `drop-collection`, `delete-many`, `atlas-create-db-user`, `atlas-create-access-list` enabled. In VSCode one must first click "Respond" (which sets action to "accepted") and then choose a value. I decided to let there be an explcit choice of Yes / No in JSON schema instead of opting to just rely on "Respond" as it is not immediately clear that `Respond = Yes` and I imagine this vagueness in the API spec will lead to confusion across clients so it's best to have an explicit JSON schema value for confirmation. I also went with enum string Yes / No and not boolean since the displayed value for this is more user friendly.
1 parent dd7760b commit f74e753

File tree

17 files changed

+1012
-16
lines changed

17 files changed

+1012
-16
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
331331
| `loggers` | `MDB_MCP_LOGGERS` | disk,mcp | Comma separated values, possible values are `mcp`, `disk` and `stderr`. See [Logger Options](#logger-options) for details. |
332332
| `logPath` | `MDB_MCP_LOG_PATH` | see note\* | Folder to store logs. |
333333
| `disabledTools` | `MDB_MCP_DISABLED_TOOLS` | <not set> | An array of tool names, operation types, and/or categories of tools that will be disabled. |
334+
| `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)**. |
334335
| `readOnly` | `MDB_MCP_READ_ONLY` | false | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. |
335336
| `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. |
336337
| `telemetry` | `MDB_MCP_TELEMETRY` | enabled | When set to disabled, disables telemetry collection. |
@@ -400,6 +401,14 @@ Operation types:
400401
- `metadata` - Tools that read metadata, such as list databases, list collections, collection schema, etc.
401402
- `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.
402403

404+
#### Require Confirmation
405+
406+
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.
407+
408+
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.
409+
410+
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`.
411+
403412
#### Read-Only Mode
404413

405414
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.

src/common/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ export interface UserConfig extends CliOptions {
151151
exportTimeoutMs: number;
152152
exportCleanupIntervalMs: number;
153153
connectionString?: string;
154+
// TODO: Use a type tracking all tool names.
154155
disabledTools: Array<string>;
156+
confirmationRequiredTools: Array<string>;
155157
readOnly?: boolean;
156158
indexCheck?: boolean;
157159
transport: "stdio" | "http";
@@ -173,6 +175,13 @@ export const defaultUserConfig: UserConfig = {
173175
telemetry: "enabled",
174176
readOnly: false,
175177
indexCheck: false,
178+
confirmationRequiredTools: [
179+
"atlas-create-access-list",
180+
"atlas-create-db-user",
181+
"drop-database",
182+
"drop-collection",
183+
"delete-many",
184+
],
176185
transport: "stdio",
177186
httpPort: 3000,
178187
httpHost: "127.0.0.1",
@@ -431,6 +440,7 @@ export function setupUserConfig({
431440

432441
userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
433442
userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
443+
userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools);
434444

435445
if (userConfig.connectionString && userConfig.connectionSpecifier) {
436446
const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);

src/elicitation.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { PrimitiveSchemaDefinition } from "@modelcontextprotocol/sdk/types.js";
2+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
4+
export class Elicitation {
5+
private readonly server: McpServer["server"];
6+
constructor({ server }: { server: McpServer["server"] }) {
7+
this.server = server;
8+
}
9+
10+
/**
11+
* Checks if the client supports elicitation capabilities.
12+
* @returns True if the client supports elicitation, false otherwise.
13+
*/
14+
public supportsElicitation(): boolean {
15+
const clientCapabilities = this.server.getClientCapabilities();
16+
return clientCapabilities?.elicitation !== undefined;
17+
}
18+
19+
/**
20+
* Requests a boolean confirmation from the user.
21+
* @param message - The message to display to the user.
22+
* @returns True if the user confirms the action or the client does not support elicitation, false otherwise.
23+
*/
24+
public async requestConfirmation(message: string): Promise<boolean> {
25+
if (!this.supportsElicitation()) {
26+
return true;
27+
}
28+
29+
const result = await this.server.elicitInput({
30+
message,
31+
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
32+
});
33+
return result.action === "accept" && result.content?.confirmation === "Yes";
34+
}
35+
36+
/**
37+
* The schema for the confirmation question.
38+
* TODO: In the future would be good to use Zod 4's toJSONSchema() to generate the schema.
39+
*/
40+
public static CONFIRMATION_SCHEMA: MCPElicitationSchema = {
41+
type: "object",
42+
properties: {
43+
confirmation: {
44+
type: "string",
45+
title: "Would you like to confirm?",
46+
description: "Would you like to confirm?",
47+
enum: ["Yes", "No"],
48+
enumNames: ["Yes, I confirm", "No, I do not confirm"],
49+
},
50+
},
51+
required: ["confirmation"],
52+
};
53+
}
54+
55+
export type MCPElicitationSchema = {
56+
type: "object";
57+
properties: Record<string, PrimitiveSchemaDefinition>;
58+
required?: string[];
59+
};

src/server.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import type { ToolBase } from "./tools/tool.js";
2222
import { validateConnectionString } from "./helpers/connectionOptions.js";
2323
import { packageInfo } from "./common/packageInfo.js";
2424
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
25+
import type { Elicitation } from "./elicitation.js";
2526

2627
export interface ServerOptions {
2728
session: Session;
2829
userConfig: UserConfig;
2930
mcpServer: McpServer;
3031
telemetry: Telemetry;
32+
elicitation: Elicitation;
3133
connectionErrorHandler: ConnectionErrorHandler;
3234
}
3335

@@ -36,6 +38,7 @@ export class Server {
3638
public readonly mcpServer: McpServer;
3739
private readonly telemetry: Telemetry;
3840
public readonly userConfig: UserConfig;
41+
public readonly elicitation: Elicitation;
3942
public readonly tools: ToolBase[] = [];
4043
public readonly connectionErrorHandler: ConnectionErrorHandler;
4144

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

51-
constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler }: ServerOptions) {
54+
constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler, elicitation }: ServerOptions) {
5255
this.startTime = Date.now();
5356
this.session = session;
5457
this.telemetry = telemetry;
5558
this.mcpServer = mcpServer;
5659
this.userConfig = userConfig;
60+
this.elicitation = elicitation;
5761
this.connectionErrorHandler = connectionErrorHandler;
5862
}
5963

@@ -184,6 +188,7 @@ export class Server {
184188
event.properties.startup_time_ms = commandDuration;
185189
event.properties.read_only_mode = this.userConfig.readOnly || false;
186190
event.properties.disabled_tools = this.userConfig.disabledTools || [];
191+
event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
187192
}
188193
if (command === "stop") {
189194
event.properties.runtime_duration_ms = Date.now() - this.startTime;
@@ -193,12 +198,17 @@ export class Server {
193198
}
194199
}
195200

196-
this.telemetry.emitEvents([event]);
201+
void this.telemetry.emitEvents([event]);
197202
}
198203

199204
private registerTools(): void {
200205
for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) {
201-
const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
206+
const tool = new toolConstructor({
207+
session: this.session,
208+
config: this.userConfig,
209+
telemetry: this.telemetry,
210+
elicitation: this.elicitation,
211+
});
202212
if (tool.register(this)) {
203213
this.tools.push(tool);
204214
}

src/telemetry/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type ServerEventProperties = {
4545
runtime_duration_ms?: number;
4646
read_only_mode?: boolean;
4747
disabled_tools?: string[];
48+
confirmation_required_tools?: string[];
4849
};
4950

5051
export type ServerEvent = TelemetryEvent<ServerEventProperties>;

src/tools/atlas/create/createAccessList.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,31 @@ export class CreateAccessListTool extends AtlasToolBase {
7575
],
7676
};
7777
}
78+
79+
protected getConfirmationMessage({
80+
projectId,
81+
ipAddresses,
82+
cidrBlocks,
83+
comment,
84+
currentIpAddress,
85+
}: ToolArgs<typeof this.argsShape>): string {
86+
const accessDescription = [];
87+
if (ipAddresses?.length) {
88+
accessDescription.push(`- **IP addresses**: ${ipAddresses.join(", ")}`);
89+
}
90+
if (cidrBlocks?.length) {
91+
accessDescription.push(`- **CIDR blocks**: ${cidrBlocks.join(", ")}`);
92+
}
93+
if (currentIpAddress) {
94+
accessDescription.push("- **Current IP address**");
95+
}
96+
97+
return (
98+
`You are about to add the following entries to the access list for Atlas project "${projectId}":\n\n` +
99+
accessDescription.join("\n") +
100+
`\n\n**Comment**: ${comment || DEFAULT_ACCESS_LIST_COMMENT}\n\n` +
101+
"This will allow network access to your MongoDB Atlas clusters from these IP addresses/ranges. " +
102+
"Do you want to proceed?"
103+
);
104+
}
78105
}

src/tools/atlas/create/createDBUser.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,22 @@ export class CreateDBUserTool extends AtlasToolBase {
9292
],
9393
};
9494
}
95+
96+
protected getConfirmationMessage({
97+
projectId,
98+
username,
99+
password,
100+
roles,
101+
clusters,
102+
}: ToolArgs<typeof this.argsShape>): string {
103+
return (
104+
`You are about to create a database user in Atlas project \`${projectId}\`:\n\n` +
105+
`**Username**: \`${username}\`\n\n` +
106+
`**Password**: ${password ? "(User-provided password)" : "(Auto-generated secure password)"}\n\n` +
107+
`**Roles**: ${roles.map((role) => `${role.roleName}${role.collectionName ? ` on ${role.databaseName}.${role.collectionName}` : ` on ${role.databaseName}`}`).join(", ")}\n\n` +
108+
`**Cluster Access**: ${clusters?.length ? clusters.join(", ") : "All clusters in the project"}\n\n` +
109+
"This will create a new database user with the specified permissions. " +
110+
"**Do you confirm the execution of the action?**"
111+
);
112+
}
95113
}

src/tools/mongodb/connect/connect.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { z } from "zod";
22
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { MongoDBToolBase } from "../mongodbTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
4+
import type { ToolArgs, OperationType, ToolConstructorParams } from "../../tool.js";
55
import assert from "assert";
6-
import type { UserConfig } from "../../../common/config.js";
7-
import type { Telemetry } from "../../../telemetry/telemetry.js";
8-
import type { Session } from "../../../common/session.js";
96
import type { Server } from "../../../server.js";
107

118
const disconnectedSchema = z
@@ -44,8 +41,8 @@ export class ConnectTool extends MongoDBToolBase {
4441

4542
public operationType: OperationType = "connect";
4643

47-
constructor(session: Session, config: UserConfig, telemetry: Telemetry) {
48-
super(session, config, telemetry);
44+
constructor({ session, config, telemetry, elicitation }: ToolConstructorParams) {
45+
super({ session, config, telemetry, elicitation });
4946
session.on("connect", () => {
5047
this.updateMetadata();
5148
});

src/tools/mongodb/delete/deleteMany.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import type { ToolArgs, OperationType } from "../../tool.js";
55
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
6+
import { EJSON } from "bson";
67

78
export class DeleteManyTool extends MongoDBToolBase {
89
public name = "delete-many";
@@ -55,4 +56,17 @@ export class DeleteManyTool extends MongoDBToolBase {
5556
],
5657
};
5758
}
59+
60+
protected getConfirmationMessage({ database, collection, filter }: ToolArgs<typeof this.argsShape>): string {
61+
const filterDescription =
62+
filter && Object.keys(filter).length > 0 ? EJSON.stringify(filter) : "All documents (No filter)";
63+
return (
64+
`You are about to delete documents from the \`${collection}\` collection in the \`${database}\` database:\n\n` +
65+
"```json\n" +
66+
`{ "filter": ${filterDescription} }\n` +
67+
"```\n\n" +
68+
"This operation will permanently remove all documents matching the filter.\n\n" +
69+
"**Do you confirm the execution of the action?**"
70+
);
71+
}
5872
}

src/tools/mongodb/delete/dropCollection.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ export class DropCollectionTool extends MongoDBToolBase {
2424
],
2525
};
2626
}
27+
28+
protected getConfirmationMessage({ database, collection }: ToolArgs<typeof this.argsShape>): string {
29+
return (
30+
`You are about to drop the \`${collection}\` collection from the \`${database}\` database:\n\n` +
31+
"This operation will permanently remove the collection and all its data, including indexes.\n\n" +
32+
"**Do you confirm the execution of the action?**"
33+
);
34+
}
2735
}

0 commit comments

Comments
 (0)