Skip to content

Commit 7f3ed9c

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 e71e2ba commit 7f3ed9c

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
@@ -346,6 +346,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
346346
| `loggers` | `MDB_MCP_LOGGERS` | disk,mcp | Comma separated values, possible values are `mcp`, `disk` and `stderr`. See [Logger Options](#logger-options) for details. |
347347
| `logPath` | `MDB_MCP_LOG_PATH` | see note\* | Folder to store logs. |
348348
| `disabledTools` | `MDB_MCP_DISABLED_TOOLS` | <not set> | An array of tool names, operation types, and/or categories of tools that will be disabled. |
349+
| `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)**. |
349350
| `readOnly` | `MDB_MCP_READ_ONLY` | false | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. |
350351
| `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. |
351352
| `telemetry` | `MDB_MCP_TELEMETRY` | enabled | When set to disabled, disables telemetry collection. |
@@ -418,6 +419,14 @@ Operation types:
418419
- `metadata` - Tools that read metadata, such as list databases, list collections, collection schema, etc.
419420
- `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.
420421

422+
#### Require Confirmation
423+
424+
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.
425+
426+
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.
427+
428+
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`.
429+
421430
#### Read-Only Mode
422431

423432
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
@@ -160,7 +160,9 @@ export interface UserConfig extends CliOptions {
160160
exportTimeoutMs: number;
161161
exportCleanupIntervalMs: number;
162162
connectionString?: string;
163+
// TODO: Use a type tracking all tool names.
163164
disabledTools: Array<string>;
165+
confirmationRequiredTools: Array<string>;
164166
readOnly?: boolean;
165167
indexCheck?: boolean;
166168
transport: "stdio" | "http";
@@ -183,6 +185,13 @@ export const defaultUserConfig: UserConfig = {
183185
telemetry: "enabled",
184186
readOnly: false,
185187
indexCheck: false,
188+
confirmationRequiredTools: [
189+
"atlas-create-access-list",
190+
"atlas-create-db-user",
191+
"drop-database",
192+
"drop-collection",
193+
"delete-many",
194+
],
186195
transport: "stdio",
187196
httpPort: 3000,
188197
httpHost: "127.0.0.1",
@@ -442,6 +451,7 @@ export function setupUserConfig({
442451

443452
userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
444453
userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
454+
userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools);
445455

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

src/tools/atlas/create/createDBUser.ts

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

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)