diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/BaseToolLoader.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/BaseToolLoader.cs
index a3e80ad6e..88e65dcf5 100644
--- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/BaseToolLoader.cs
+++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/BaseToolLoader.cs
@@ -2,7 +2,9 @@
// Licensed under the MIT License.
using System.Text.Json;
+using Azure.Mcp.Core.Models.Elicitation;
using Microsoft.Extensions.Logging;
+using ModelContextProtocol;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
@@ -150,4 +152,81 @@ protected McpClientOptions CreateClientOptions(McpServer server)
return clientOptions;
}
+
+ ///
+ /// Handles elicitation for commands that access sensitive data.
+ /// If elicitation is disabled or not supported, returns appropriate error result.
+ ///
+ /// The request context containing the MCP server.
+ /// The name of the tool being invoked.
+ /// Whether elicitation has been disabled via insecure option.
+ /// Logger instance for recording elicitation events.
+ /// Cancellation token for the operation.
+ ///
+ /// Null if elicitation was accepted or bypassed (operation should proceed).
+ /// A CallToolResult with IsError=true if elicitation was rejected or failed (operation should not proceed).
+ ///
+ protected static async Task HandleSecretElicitationAsync(
+ RequestContext request,
+ string toolName,
+ bool insecureDisableElicitation,
+ ILogger logger,
+ CancellationToken cancellationToken)
+ {
+ // Check if elicitation is disabled by insecure option
+ if (insecureDisableElicitation)
+ {
+ logger.LogWarning("Tool '{Tool}' handles sensitive data but elicitation is disabled via --insecure-disable-elicitation. Proceeding without user consent (INSECURE).", toolName);
+ return null;
+ }
+
+ // If client doesn't support elicitation, treat as rejected and don't execute
+ if (!request.Server.SupportsElicitation())
+ {
+ logger.LogWarning("Tool '{Tool}' handles sensitive data but client does not support elicitation. Operation rejected.", toolName);
+ return new CallToolResult
+ {
+ Content = [new TextContentBlock { Text = "This tool handles sensitive data and requires user consent, but the client does not support elicitation. Operation rejected for security." }],
+ IsError = true
+ };
+ }
+
+ try
+ {
+ logger.LogInformation("Tool '{Tool}' handles sensitive data. Requesting user confirmation via elicitation.", toolName);
+
+ // Create the elicitation request using our custom model
+ var elicitationRequest = new ElicitationRequestParams
+ {
+ Message = $"⚠️ SECURITY WARNING: The tool '{toolName}' may expose secrets or sensitive information.\n\nThis operation could reveal confidential data such as passwords, API keys, certificates, or other sensitive values.\n\nDo you want to continue with this potentially sensitive operation?",
+ RequestedSchema = ElicitationSchema.CreateSecretSchema("confirmation", "Confirm Action", "Type 'yes' to confirm you want to proceed with this sensitive operation", true)
+ };
+
+ // Use our extension method to handle the elicitation
+ var elicitationResponse = await request.Server.RequestElicitationAsync(elicitationRequest, cancellationToken);
+
+ if (elicitationResponse.Action != ElicitationAction.Accept)
+ {
+ logger.LogInformation("User {Action} the elicitation for tool '{Tool}'. Operation not executed.",
+ elicitationResponse.Action.ToString().ToLower(), toolName);
+ return new CallToolResult
+ {
+ Content = [new TextContentBlock { Text = $"Operation cancelled by user ({elicitationResponse.Action.ToString().ToLower()})." }],
+ IsError = true
+ };
+ }
+
+ logger.LogInformation("User accepted elicitation for tool '{Tool}'. Proceeding with execution.", toolName);
+ return null;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error during elicitation for tool '{Tool}': {Error}", toolName, ex.Message);
+ return new CallToolResult
+ {
+ Content = [new TextContentBlock { Text = $"Elicitation failed for sensitive tool '{toolName}': {ex.Message}. Operation not executed for security." }],
+ IsError = true
+ };
+ }
+ }
}
diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoader.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoader.cs
index 77c66b408..c7806b0ea 100644
--- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoader.cs
+++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoader.cs
@@ -23,7 +23,7 @@ public sealed class CommandFactoryToolLoader(
IServiceProvider serviceProvider,
CommandFactory commandFactory,
IOptions options,
- ILogger logger) : IToolLoader
+ ILogger logger) : BaseToolLoader(logger)
{
private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
private readonly CommandFactory _commandFactory = commandFactory;
@@ -32,7 +32,6 @@ public sealed class CommandFactoryToolLoader(
(options.Value.Namespace == null || options.Value.Namespace.Length == 0)
? commandFactory.AllCommands
: commandFactory.GroupCommands(options.Value.Namespace);
- private readonly ILogger _logger = logger;
public const string RawMcpToolInputOptionName = "raw-mcp-tool-input";
@@ -60,7 +59,7 @@ private static bool IsRawMcpToolInputOption(Option option)
/// The request context containing parameters and metadata.
/// A cancellation token.
/// A result containing the list of available tools.
- public ValueTask ListToolsHandler(RequestContext request, CancellationToken cancellationToken)
+ public override ValueTask ListToolsHandler(RequestContext request, CancellationToken cancellationToken)
{
var visibleCommands = CommandFactory.GetVisibleCommands(_toolCommands);
@@ -92,7 +91,7 @@ public ValueTask ListToolsHandler(RequestContextThe request context containing parameters and metadata.
/// A cancellation token.
/// The result of the tool call operation.
- public async ValueTask CallToolHandler(RequestContext request, CancellationToken cancellationToken)
+ public override async ValueTask CallToolHandler(RequestContext request, CancellationToken cancellationToken)
{
if (request.Params == null)
{
@@ -156,60 +155,16 @@ public async ValueTask CallToolHandler(RequestContext
- public async ValueTask DisposeAsync()
+ protected override ValueTask DisposeAsyncCore()
{
// CommandFactoryToolLoader doesn't create or manage disposable resources
- await ValueTask.CompletedTask;
+ return ValueTask.CompletedTask;
}
}
diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs
index e5db1298d..035116d1d 100644
--- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs
+++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs
@@ -336,60 +336,16 @@ private async Task InvokeChildToolAsync(
var metadata = cmd.Metadata;
if (metadata.Secret)
{
- // Check if elicitation is disabled by insecure option
- if (_options.Value.InsecureDisableElicitation)
+ var elicitationResult = await HandleSecretElicitationAsync(
+ request,
+ $"{namespaceName} {command}",
+ _options.Value.InsecureDisableElicitation,
+ _logger,
+ cancellationToken);
+
+ if (elicitationResult != null)
{
- _logger.LogWarning("Tool '{Namespace} {Command}' handles sensitive data but elicitation is disabled via --insecure-disable-elicitation. Proceeding without user consent (INSECURE).", namespaceName, command);
- }
- else
- {
- // If client doesn't support elicitation, treat as rejected and don't execute
- if (!request.Server.SupportsElicitation())
- {
- _logger.LogWarning("Tool '{Namespace} {Command}' handles sensitive data but client does not support elicitation. Operation rejected.", namespaceName, command);
- return new CallToolResult
- {
- Content = [new TextContentBlock { Text = "This tool handles sensitive data and requires user consent, but the client does not support elicitation. Operation rejected for security." }],
- IsError = true
- };
- }
-
- try
- {
- _logger.LogInformation("Tool '{Namespace} {Command}' handles sensitive data. Requesting user confirmation via elicitation.", namespaceName, command);
-
- // Create the elicitation request using our custom model
- var elicitationRequest = new ElicitationRequestParams
- {
- Message = $"⚠️ SECURITY WARNING: The tool '{namespaceName} {command}' may expose secrets or sensitive information.\n\nThis operation could reveal confidential data such as passwords, API keys, certificates, or other sensitive values.\n\nDo you want to continue with this potentially sensitive operation?",
- RequestedSchema = ElicitationSchema.CreateSecretSchema("confirmation", "Confirm Action", "Type 'yes' to confirm you want to proceed with this sensitive operation", true)
- };
-
- // Use our extension method to handle the elicitation
- var elicitationResponse = await request.Server.RequestElicitationAsync(elicitationRequest, cancellationToken);
-
- if (elicitationResponse.Action != ElicitationAction.Accept)
- {
- _logger.LogInformation("User {Action} the elicitation for tool '{Namespace} {Command}'. Operation not executed.",
- elicitationResponse.Action.ToString().ToLower(), namespaceName, command);
- return new CallToolResult
- {
- Content = [new TextContentBlock { Text = $"Operation cancelled by user ({elicitationResponse.Action.ToString().ToLower()})." }],
- IsError = true
- };
- }
-
- _logger.LogInformation("User accepted elicitation for tool '{Namespace} {Command}'. Proceeding with execution.", namespaceName, command);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error during elicitation for tool '{Namespace} {Command}': {Error}", namespaceName, command, ex.Message);
- return new CallToolResult
- {
- Content = [new TextContentBlock { Text = $"Elicitation failed for sensitive tool '{namespaceName} {command}': {ex.Message}. Operation not executed for security." }],
- IsError = true
- };
- }
+ return elicitationResult;
}
}
diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md
index 831bdab21..1dd5163c0 100644
--- a/servers/Azure.Mcp.Server/CHANGELOG.md
+++ b/servers/Azure.Mcp.Server/CHANGELOG.md
@@ -14,6 +14,8 @@ The Azure MCP Server updates automatically by default whenever a new release com
### Other Changes
+- Refactored duplicate elicitation handling code in `CommandFactoryToolLoader` and `NamespaceToolLoader` into shared `BaseToolLoader.HandleSecretElicitationAsync` method. [[#1028](https://github.com/microsoft/mcp/pull/1028)]
+
## 2.0.0-beta.2 (2025-11-06)
### Features Added