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