Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -150,4 +152,81 @@ protected McpClientOptions CreateClientOptions(McpServer server)

return clientOptions;
}

/// <summary>
/// Handles elicitation for commands that access sensitive data.
/// If elicitation is disabled or not supported, returns appropriate error result.
/// </summary>
/// <param name="request">The request context containing the MCP server.</param>
/// <param name="toolName">The name of the tool being invoked.</param>
/// <param name="insecureDisableElicitation">Whether elicitation has been disabled via insecure option.</param>
/// <param name="logger">Logger instance for recording elicitation events.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>
/// 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).
/// </returns>
protected static async Task<CallToolResult?> HandleSecretElicitationAsync(
RequestContext<CallToolRequestParams> 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
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public sealed class CommandFactoryToolLoader(
IServiceProvider serviceProvider,
CommandFactory commandFactory,
IOptions<ToolLoaderOptions> options,
ILogger<CommandFactoryToolLoader> logger) : IToolLoader
ILogger<CommandFactoryToolLoader> logger) : BaseToolLoader(logger)
{
private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
private readonly CommandFactory _commandFactory = commandFactory;
Expand All @@ -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<CommandFactoryToolLoader> _logger = logger;

public const string RawMcpToolInputOptionName = "raw-mcp-tool-input";

Expand Down Expand Up @@ -60,7 +59,7 @@ private static bool IsRawMcpToolInputOption(Option option)
/// <param name="request">The request context containing parameters and metadata.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A result containing the list of available tools.</returns>
public ValueTask<ListToolsResult> ListToolsHandler(RequestContext<ListToolsRequestParams> request, CancellationToken cancellationToken)
public override ValueTask<ListToolsResult> ListToolsHandler(RequestContext<ListToolsRequestParams> request, CancellationToken cancellationToken)
{
var visibleCommands = CommandFactory.GetVisibleCommands(_toolCommands);

Expand Down Expand Up @@ -92,7 +91,7 @@ public ValueTask<ListToolsResult> ListToolsHandler(RequestContext<ListToolsReque
/// <param name="request">The request context containing parameters and metadata.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The result of the tool call operation.</returns>
public async ValueTask<CallToolResult> CallToolHandler(RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken)
public override async ValueTask<CallToolResult> CallToolHandler(RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken)
{
if (request.Params == null)
{
Expand Down Expand Up @@ -156,60 +155,16 @@ public async ValueTask<CallToolResult> CallToolHandler(RequestContext<CallToolRe
var metadata = command.Metadata;
if (metadata.Secret)
{
// Check if elicitation is disabled by insecure option
if (_options.Value.InsecureDisableElicitation)
var elicitationResult = await HandleSecretElicitationAsync(
request,
toolName,
_options.Value.InsecureDisableElicitation,
_logger,
cancellationToken);

if (elicitationResult != null)
{
_logger.LogWarning("Tool '{Tool}' handles sensitive data but elicitation is disabled via --insecure-disable-elicitation. Proceeding without user consent (INSECURE).", toolName);
}
else
{
// 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);
}
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
};
}
return elicitationResult;
}
}

Expand Down Expand Up @@ -330,9 +285,9 @@ private static Tool GetTool(string fullName, IBaseCommand command)
/// Disposes resources owned by this tool loader.
/// CommandFactoryToolLoader doesn't own external resources that need disposal.
/// </summary>
public async ValueTask DisposeAsync()
protected override ValueTask DisposeAsyncCore()
{
// CommandFactoryToolLoader doesn't create or manage disposable resources
await ValueTask.CompletedTask;
return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -336,60 +336,16 @@ private async Task<CallToolResult> 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;
}
}

Expand Down
2 changes: 2 additions & 0 deletions servers/Azure.Mcp.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down