diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs index 39cd160cf4..243b9dc5bb 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs @@ -264,13 +264,6 @@ public static void InitializeConfigurationAndOptions(this IServiceCollection ser .BindConfiguration(string.Empty) .Configure>((options, rootConfiguration, serviceStartOptions) => { - // This environment variable can be used to disable telemetry collection entirely. This takes precedence - // over any other settings. - var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true); - var transport = serviceStartOptions.Value.Transport; - var isStdioTransport = string.IsNullOrEmpty(transport) - || string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase); - // Assembly.GetEntryAssembly is used to retrieve the version of the server application as that is // the assembly that will run the tool calls. var entryAssembly = Assembly.GetEntryAssembly(); @@ -281,6 +274,22 @@ public static void InitializeConfigurationAndOptions(this IServiceCollection ser options.Version = AssemblyHelper.GetAssemblyVersion(entryAssembly); + // Disable telemetry when support logging is enabled to prevent sensitive data from being sent + // to telemetry endpoints. Support logging captures debug-level information that may contain + // sensitive data, so we disable all telemetry as a safety measure. + if (!string.IsNullOrWhiteSpace(serviceStartOptions.Value.SupportLoggingFolder)) + { + options.IsTelemetryEnabled = false; + return; + } + + // This environment variable can be used to disable telemetry collection entirely. This takes precedence + // over any other settings. + var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true); + var transport = serviceStartOptions.Value.Transport; + var isStdioTransport = string.IsNullOrEmpty(transport) + || string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase); + // if transport is not set (default to stdio) or is set to stdio, enable telemetry // telemetry is disabled for HTTP transport options.IsTelemetryEnabled = collectTelemetry && isStdioTransport; diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs index 7ecb2bf4fc..1b93932300 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs @@ -7,6 +7,7 @@ using Azure.Mcp.Core.Areas.Server.Models; using Azure.Mcp.Core.Areas.Server.Options; using Azure.Mcp.Core.Helpers; +using Azure.Mcp.Core.Logging; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Authentication; using Azure.Mcp.Core.Services.Caching; @@ -82,6 +83,7 @@ protected override void RegisterOptions(Command command) command.Options.Add(ServiceOptionDefinitions.DangerouslyDisableHttpIncomingAuth); command.Options.Add(ServiceOptionDefinitions.InsecureDisableElicitation); command.Options.Add(ServiceOptionDefinitions.OutgoingAuthStrategy); + command.Options.Add(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir); command.Validators.Add(commandResult => { string transport = ResolveTransport(commandResult); @@ -93,9 +95,42 @@ protected override void RegisterOptions(Command command) commandResult.GetValueOrDefault(ServiceOptionDefinitions.Tool.Name), commandResult); ValidateOutgoingAuthStrategy(commandResult); + ValidateSupportLoggingFolder(commandResult); }); } + /// + /// Validates that the support logging folder path is valid when specified. + /// + /// Command result to update on failure. + private static void ValidateSupportLoggingFolder(CommandResult commandResult) + { + string? folderPath = commandResult.GetValueOrDefault(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name); + + if (folderPath is null) + { + return; // Option not specified, nothing to validate + } + + // Validate the folder path is not empty or whitespace + if (string.IsNullOrWhiteSpace(folderPath)) + { + commandResult.AddError("The --dangerously-write-support-logs-to-dir option requires a valid folder path."); + return; + } + + // Validate the folder path is actually a valid path format + try + { + // GetFullPath will throw for invalid path characters and other path format issues + _ = Path.GetFullPath(folderPath); + } + catch (Exception ex) when (ex is ArgumentException or PathTooLongException or NotSupportedException) + { + commandResult.AddError($"The --dangerously-write-support-logs-to-dir option contains an invalid folder path '{folderPath}': {ex.Message}"); + } + } + /// /// Binds the parsed command line arguments to the ServiceStartOptions object. /// @@ -124,7 +159,8 @@ protected override ServiceStartOptions BindOptions(ParseResult parseResult) Debug = parseResult.GetValueOrDefault(ServiceOptionDefinitions.Debug.Name), DangerouslyDisableHttpIncomingAuth = parseResult.GetValueOrDefault(ServiceOptionDefinitions.DangerouslyDisableHttpIncomingAuth.Name), InsecureDisableElicitation = parseResult.GetValueOrDefault(ServiceOptionDefinitions.InsecureDisableElicitation.Name), - OutgoingAuthStrategy = outgoingAuthStrategy + OutgoingAuthStrategy = outgoingAuthStrategy, + SupportLoggingFolder = parseResult.GetValueOrDefault(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name) }; return options; } @@ -196,6 +232,26 @@ internal static void LogStartTelemetry(ITelemetryService telemetryService, Servi } } + /// + /// Configures support logging when a support logging folder is specified. + /// This enables debug-level logging for troubleshooting and support purposes. + /// + /// The logging builder to configure. + /// The server configuration options. + private static void ConfigureSupportLogging(ILoggingBuilder logging, ServiceStartOptions options) + { + if (options.SupportLoggingFolder is null) + { + return; + } + + // Set minimum log level to Debug when support logging is enabled + logging.SetMinimumLevel(LogLevel.Debug); + + // Add file logging to the specified folder + logging.AddSupportFileLogging(options.SupportLoggingFolder); + } + /// /// Validates if the provided mode is a valid mode type. /// @@ -368,6 +424,8 @@ private IHost CreateStdioHost(ServiceStartOptions serverOptions) logging.AddFilter("Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider", LogLevel.Debug); logging.SetMinimumLevel(LogLevel.Debug); } + + ConfigureSupportLogging(logging, serverOptions); }) .ConfigureServices(services => { @@ -394,6 +452,7 @@ private IHost CreateHttpHost(ServiceStartOptions serverOptions) builder.Logging.ConfigureOpenTelemetryLogger(); builder.Logging.AddEventSourceLogger(); builder.Logging.AddConsole(); + ConfigureSupportLogging(builder.Logging, serverOptions); IServiceCollection services = builder.Services; @@ -571,6 +630,7 @@ private IHost CreateIncomingAuthDisabledHttpHost(ServiceStartOptions serverOptio builder.Logging.ConfigureOpenTelemetryLogger(); builder.Logging.AddEventSourceLogger(); builder.Logging.AddConsole(); + ConfigureSupportLogging(builder.Logging, serverOptions); IServiceCollection services = builder.Services; @@ -771,6 +831,14 @@ private static WebApplication UseHttpsRedirectionIfEnabled(WebApplication app) return null; } + // Disable telemetry when support logging is enabled to prevent sensitive data from being sent + // to telemetry endpoints. Support logging captures debug-level information that may contain + // sensitive data, so we disable all telemetry as a safety measure. + if (!string.IsNullOrWhiteSpace(options.SupportLoggingFolder)) + { + return null; + } + string? collectTelemetry = Environment.GetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY"); bool isTelemetryEnabled = string.IsNullOrWhiteSpace(collectTelemetry) || (bool.TryParse(collectTelemetry, out bool shouldCollectTelemetry) && shouldCollectTelemetry); diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs index 4b554e79e9..0cb4a951e1 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs @@ -14,6 +14,7 @@ public static class ServiceOptionDefinitions public const string DangerouslyDisableHttpIncomingAuthName = "dangerously-disable-http-incoming-auth"; public const string InsecureDisableElicitationName = "insecure-disable-elicitation"; public const string OutgoingAuthStrategyName = "outgoing-auth-strategy"; + public const string DangerouslyWriteSupportLogsToDirName = "dangerously-write-support-logs-to-dir"; public static readonly Option Transport = new($"--{TransportName}") { @@ -91,4 +92,12 @@ public static class ServiceOptionDefinitions Description = "Outgoing authentication strategy for Azure service requests. Valid values: NotSet, UseHostingEnvironmentIdentity, UseOnBehalfOf.", DefaultValueFactory = _ => Options.OutgoingAuthStrategy.NotSet }; + + public static readonly Option DangerouslyWriteSupportLogsToDir = new( + $"--{DangerouslyWriteSupportLogsToDirName}") + { + Required = false, + Description = "Dangerously enables detailed debug-level logging for support and troubleshooting purposes. Specify a folder path where log files will be automatically created with timestamp-based filenames (e.g., azmcp_20251202_143052.log). This may include sensitive information in logs. Use with extreme caution and only when requested by support.", + DefaultValueFactory = _ => null + }; } diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs index 8023c60605..dd3c6256f3 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs @@ -72,4 +72,13 @@ public class ServiceStartOptions /// [JsonPropertyName("outgoingAuthStrategy")] public OutgoingAuthStrategy OutgoingAuthStrategy { get; set; } = OutgoingAuthStrategy.NotSet; + + /// + /// Gets or sets the folder path for support logging. + /// When specified, detailed debug-level logging is enabled and logs are written to + /// automatically generated files in this folder with timestamp-based filenames. + /// Warning: This may include sensitive information in logs. + /// + [JsonPropertyName("supportLoggingFolder")] + public string? SupportLoggingFolder { get; set; } = null; } diff --git a/core/Azure.Mcp.Core/src/Logging/FileLogger.cs b/core/Azure.Mcp.Core/src/Logging/FileLogger.cs new file mode 100644 index 0000000000..450c721581 --- /dev/null +++ b/core/Azure.Mcp.Core/src/Logging/FileLogger.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Core.Logging; + +/// +/// A simple file logger that writes logs to a file for support and troubleshooting purposes. +/// +internal sealed class FileLogger(string categoryName, FileLoggerProvider provider) : ILogger +{ + private readonly string _categoryName = categoryName; + private readonly FileLoggerProvider _provider = provider; + + /// + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + /// + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var message = formatter(state, exception); + var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var logLevelString = GetLogLevelString(logLevel); + + var logEntry = $"[{timestamp}] [{logLevelString}] [{_categoryName}] {message}"; + + if (exception != null) + { + logEntry += Environment.NewLine + exception.ToString(); + } + + _provider.WriteLog(logEntry); + } + + private static string GetLogLevelString(LogLevel logLevel) => logLevel switch + { + LogLevel.Trace => "TRCE", + LogLevel.Debug => "DBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERRR", + LogLevel.Critical => "CRIT", + _ => "NONE" + }; +} diff --git a/core/Azure.Mcp.Core/src/Logging/FileLoggerProvider.cs b/core/Azure.Mcp.Core/src/Logging/FileLoggerProvider.cs new file mode 100644 index 0000000000..8807a74941 --- /dev/null +++ b/core/Azure.Mcp.Core/src/Logging/FileLoggerProvider.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Core.Logging; + +/// +/// A file logger provider that writes logs to a file using a background thread for improved performance. +/// Log entries are queued and written asynchronously to avoid blocking the calling thread. +/// Log files are automatically created with timestamp-based filenames and rotated hourly. +/// +public sealed class FileLoggerProvider : ILoggerProvider +{ + private readonly string _folderPath; + private readonly BlockingCollection _logQueue; + private readonly Thread _writerThread; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly object _writerLock = new(); + private StreamWriter? _writer; + private DateTime _currentFileHour; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// Creates a log file with an auto-generated timestamp-based filename in the specified folder. + /// Log files are rotated hourly to prevent excessively large files during long-running sessions. + /// + /// The folder path where the log file should be created. + public FileLoggerProvider(string folderPath) + { + ArgumentNullException.ThrowIfNull(folderPath); + + _folderPath = folderPath; + + // Ensure the directory exists + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + _logQueue = new BlockingCollection(boundedCapacity: 10000); + _cancellationTokenSource = new CancellationTokenSource(); + + // Create the initial log file + _currentFileHour = GetCurrentHour(); + _writer = CreateLogFileWriter(_currentFileHour); + + // Start the background writer thread + _writerThread = new Thread(ProcessLogQueue) + { + IsBackground = true, + Name = "FileLoggerWriter" + }; + _writerThread.Start(); + } + + /// + /// Gets the current time truncated to the hour for file rotation comparison. + /// + private static DateTime GetCurrentHour() + { + var now = DateTime.Now; + return new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0, DateTimeKind.Local); + } + + /// + /// Creates a new StreamWriter for a log file based on the specified hour. + /// + /// The hour to use for the filename. + /// A StreamWriter configured for the log file. + private StreamWriter CreateLogFileWriter(DateTime hour) + { + // Generate timestamp-based filename: azmcp_yyyyMMdd_HH.log (hourly rotation) + var timestamp = hour.ToString("yyyyMMdd_HH", CultureInfo.InvariantCulture); + var fileName = $"azmcp_{timestamp}.log"; + var filePath = Path.Combine(_folderPath, fileName); + + return new StreamWriter(filePath, append: true) + { + AutoFlush = true + }; + } + + /// + /// Rotates the log file if the current hour has changed since the last write. + /// This method is called from the background writer thread before each write operation. + /// + private void RotateFileIfNeeded() + { + var currentHour = GetCurrentHour(); + if (currentHour == _currentFileHour) + { + return; + } + + // Hour has changed, rotate to a new file + lock (_writerLock) + { + // Double-check after acquiring the lock + if (currentHour == _currentFileHour) + { + return; + } + + // Close the old writer + _writer?.Dispose(); + + // Create a new writer for the new hour + _currentFileHour = currentHour; + _writer = CreateLogFileWriter(_currentFileHour); + } + } + + /// + public ILogger CreateLogger(string categoryName) + { + return new FileLogger(categoryName, this); + } + + /// + /// Queues a log entry to be written to the file by the background thread. + /// + /// The log message to write. + internal void WriteLog(string message) + { + if (_disposed) + { + return; + } + + // Try to add to the queue, but don't block if the queue is full + // This prevents the logging from blocking the application if logs are being generated faster than they can be written + _logQueue.TryAdd(message); + } + + /// + /// Background thread method that processes the log queue and writes entries to the file. + /// Handles hourly log file rotation automatically. + /// + private void ProcessLogQueue() + { + try + { + foreach (var message in _logQueue.GetConsumingEnumerable(_cancellationTokenSource.Token)) + { + try + { + RotateFileIfNeeded(); + _writer?.WriteLine(message); + } + catch (ObjectDisposedException) + { + // Writer was disposed, exit the loop + break; + } + } + } + catch (OperationCanceledException) + { + // Expected when cancellation is requested during shutdown + } + + // Drain any remaining messages in the queue before exiting + while (_logQueue.TryTake(out var remainingMessage)) + { + try + { + _writer?.WriteLine(remainingMessage); + } + catch (ObjectDisposedException) + { + break; + } + } + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + // Signal the writer thread to stop and complete adding to the queue + _logQueue.CompleteAdding(); + _cancellationTokenSource.Cancel(); + + // Wait for the writer thread to finish (with timeout to prevent hanging) + _writerThread.Join(TimeSpan.FromSeconds(5)); + + // Clean up resources + _cancellationTokenSource.Dispose(); + _logQueue.Dispose(); + _writer?.Dispose(); + _writer = null; + } +} diff --git a/core/Azure.Mcp.Core/src/Logging/FileLoggingExtensions.cs b/core/Azure.Mcp.Core/src/Logging/FileLoggingExtensions.cs new file mode 100644 index 0000000000..207e3310e8 --- /dev/null +++ b/core/Azure.Mcp.Core/src/Logging/FileLoggingExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Core.Logging; + +/// +/// Extension methods for configuring file logging for support scenarios. +/// +public static class FileLoggingExtensions +{ + /// + /// Configures file logging to write debug-level logs to a folder for support and troubleshooting purposes. + /// Log files are automatically created with timestamp-based filenames (e.g., azmcp_20251202_14.log) + /// and rotated hourly to prevent excessively large files during long-running sessions. + /// + /// The logging builder to configure. + /// The folder path where log files should be written. + /// The logging builder for chaining. + public static ILoggingBuilder AddSupportFileLogging(this ILoggingBuilder builder, string logFolderPath) + { + builder.Services.AddSingleton(sp => + new FileLoggerProvider(logFolderPath)); + + return builder; + } +} diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs index 32e4e00688..c7f7527eb4 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs @@ -107,4 +107,78 @@ public void InitializeConfigurationAndOptions_Stdio() Assert.False(actual.IsTelemetryEnabled); } + + /// + /// When SupportLoggingFolder is set, telemetry should be automatically disabled + /// to prevent sensitive debug information from being sent to telemetry endpoints. + /// + [Fact] + public void InitializeConfigurationAndOptions_WithSupportLoggingFolder_DisablesTelemetry() + { + // Arrange + var serviceStartOptions = new ServiceStartOptions + { + SupportLoggingFolder = "/tmp/logs" + }; + var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions)); + + // Act + Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", null); + ServiceCollectionExtensions.InitializeConfigurationAndOptions(services); + var provider = services.BuildServiceProvider(); + + // Assert + var options = provider.GetRequiredService>(); + Assert.False(options.Value.IsTelemetryEnabled, "Telemetry should be disabled when support logging folder is set"); + } + + /// + /// SupportLoggingFolder takes precedence over AZURE_MCP_COLLECT_TELEMETRY=true. + /// When support logging is enabled, telemetry must be disabled regardless of env var. + /// + [Fact] + public void InitializeConfigurationAndOptions_WithSupportLoggingFolderAndEnvVarTrue_StillDisablesTelemetry() + { + // Arrange + var serviceStartOptions = new ServiceStartOptions + { + SupportLoggingFolder = "/tmp/logs" + }; + var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions)); + + // Act + Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", "true"); + ServiceCollectionExtensions.InitializeConfigurationAndOptions(services); + var provider = services.BuildServiceProvider(); + + // Assert + var options = provider.GetRequiredService>(); + Assert.False(options.Value.IsTelemetryEnabled, "Telemetry should be disabled when support logging folder is set, regardless of environment variable"); + } + + /// + /// Empty or whitespace SupportLoggingFolder should not disable telemetry. + /// + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void InitializeConfigurationAndOptions_WithEmptyOrWhitespaceSupportLoggingFolder_EnablesTelemetry(string? folderPath) + { + // Arrange + var serviceStartOptions = new ServiceStartOptions + { + SupportLoggingFolder = folderPath + }; + var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions)); + + // Act + Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", null); + ServiceCollectionExtensions.InitializeConfigurationAndOptions(services); + var provider = services.BuildServiceProvider(); + + // Assert + var options = provider.GetRequiredService>(); + Assert.True(options.Value.IsTelemetryEnabled, $"Telemetry should be enabled when support logging folder is '{folderPath}'"); + } } diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs index a90169df9f..4465af5a86 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs @@ -279,6 +279,62 @@ public void BindOptions_WithDefaults_ReturnsDefaultValues() Assert.False(options.Debug); Assert.False(options.DangerouslyDisableHttpIncomingAuth); Assert.False(options.InsecureDisableElicitation); + Assert.Null(options.SupportLoggingFolder); + } + + [Theory] + [InlineData("/tmp/logs")] + [InlineData("C:\\logs")] + [InlineData(null)] + public void DangerouslyWriteSupportLogsToDirOption_ParsesCorrectly(string? expectedFolder) + { + // Arrange + var parseResult = CreateParseResultWithSupportLogging(expectedFolder); + + // Act + var actualValue = parseResult.GetValue(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir); + + // Assert + Assert.Equal(expectedFolder, actualValue); + } + + [Fact] + public void BindOptions_WithSupportLoggingFolder_ReturnsCorrectlyConfiguredOptions() + { + // Arrange + var logFolder = "/tmp/mcp-support-logs"; + var parseResult = CreateParseResultWithSupportLogging(logFolder); + + // Act + var options = GetBoundOptions(parseResult); + + // Assert + Assert.Equal(logFolder, options.SupportLoggingFolder); + } + + [Fact] + public void BindOptions_WithoutSupportLoggingFolder_ReturnsCorrectlyConfiguredOptions() + { + // Arrange + var parseResult = CreateParseResultWithSupportLogging(null); + + // Act + var options = GetBoundOptions(parseResult); + + // Assert + Assert.Null(options.SupportLoggingFolder); + } + + [Fact] + public void AllOptionsRegistered_IncludesSupportLoggingToFolder() + { + // Arrange & Act + var command = _command.GetCommand(); + + // Assert + var hasSupportLoggingFolderOption = command.Options.Any(o => + o.Name == ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name); + Assert.True(hasSupportLoggingFolderOption, "DangerouslyWriteSupportLogsToDir option should be registered"); } [Fact] @@ -341,6 +397,67 @@ public void Validate_WithNamespaceAndTool_ReturnsInvalidResult() Assert.Contains("--namespace and --tool options cannot be used together", string.Join('\n', result.Errors)); } + [Fact] + public void Validate_WithSupportLoggingFolderWhitespace_ReturnsInvalidResult() + { + // Arrange + var parseResult = CreateParseResultWithSupportLogging(" "); + var commandResult = parseResult.CommandResult; + + // Act + var result = _command.Validate(commandResult, null); + + // Assert + Assert.False(result.IsValid); + Assert.Contains("The --dangerously-write-support-logs-to-dir option requires a valid folder path", string.Join('\n', result.Errors)); + } + + [Fact] + public void Validate_WithValidSupportLoggingFolder_ReturnsValidResult() + { + // Arrange + var parseResult = CreateParseResultWithSupportLogging("/tmp/mcp-support-logs"); + var commandResult = parseResult.CommandResult; + + // Act + var result = _command.Validate(commandResult, null); + + // Assert + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + + [Fact] + public void Validate_WithoutSupportLoggingFolder_ReturnsValidResult() + { + // Arrange + var parseResult = CreateParseResultWithSupportLogging(null); + var commandResult = parseResult.CommandResult; + + // Act + var result = _command.Validate(commandResult, null); + + // Assert + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + + [Fact] + public async Task ExecuteAsync_WithSupportLoggingFolderWhitespace_ReturnsValidationError() + { + // Arrange + var parseResult = CreateParseResultWithSupportLogging(" "); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var context = new CommandContext(serviceProvider); + + // Act + var response = await _command.ExecuteAsync(context, parseResult, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + Assert.Contains("The --dangerously-write-support-logs-to-dir option requires a valid folder path", response.Message); + } + [Fact] public async Task ExecuteAsync_WithNamespaceAndTool_ReturnsValidationError() { @@ -720,6 +837,22 @@ private ParseResult CreateParseResultWithMinimalOptions() return _command.GetCommand().Parse([]); } + private ParseResult CreateParseResultWithSupportLogging(string? folderPath) + { + var args = new List + { + "--transport", "stdio" + }; + + if (folderPath is not null) + { + args.Add("--dangerously-write-support-logs-to-dir"); + args.Add(folderPath); + } + + return _command.GetCommand().Parse([.. args]); + } + private ParseResult CreateParseResultWithToolsAndMode(string[] tools, string mode) { var args = new List diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Extensions/OpenTelemetryExtensionsTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Extensions/OpenTelemetryExtensionsTests.cs new file mode 100644 index 0000000000..e47696ebbd --- /dev/null +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Extensions/OpenTelemetryExtensionsTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Services.Telemetry; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Azure.Mcp.Core.UnitTests.Extensions; + +public class OpenTelemetryExtensionsTests +{ + /// + /// ConfigureOpenTelemetry registers the telemetry services. + /// Note: ConfigureOpenTelemetry does NOT configure IsTelemetryEnabled based on SupportLoggingFolder - + /// that logic is handled by ServiceCollectionExtensions.InitializeConfigurationAndOptions. + /// For tests that verify SupportLoggingFolder -> IsTelemetryEnabled behavior, + /// see ServiceCollectionExtensionsSerializedTests. + /// + [Fact] + public void ConfigureOpenTelemetry_RegistersTelemetryService() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.ConfigureOpenTelemetry(); + + // Assert - Verify that the telemetry service descriptor is registered + Assert.Contains(services, sd => sd.ServiceType == typeof(ITelemetryService)); + } +} diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 390ad4e4dc..838fa493bd 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -6,6 +6,7 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Features Added +- Added support logging capability with `--dangerously-write-support-logs-to-dir` option for troubleshooting and support scenarios. When enabled, detailed debug-level logs are written to automatically-generated timestamped log files (e.g., `azmcp_20251202_143052.log`) in the specified folder. All telemetry is automatically disabled when support logging is enabled to prevent sensitive debug information from being sent to telemetry endpoints. - Replace hard-coded strings for Azure.Mcp.Server with ones from IConfiguration. [[#1269](https://github.com/microsoft/mcp/pull/1269)] ### Breaking Changes diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index 5f71115909..aed6de218d 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -589,6 +589,7 @@ The Azure MCP Server provides tools for interacting with **40+ Azure service are ## Feedback and Support - Check the [Troubleshooting guide](https://aka.ms/azmcp/troubleshooting) to diagnose and resolve common issues with the Azure MCP Server. +- For advanced troubleshooting, you can enable [support logging](https://github.com/microsoft/mcp/blob/main/servers/Azure.Mcp.Server/TROUBLESHOOTING.md#support-logging) using the `--dangerously-write-support-logs-to-dir` option. - We're building this in the open. Your feedback is much appreciated, and will help us shape the future of the Azure MCP server. - 👉 [Open an issue](https://github.com/microsoft/mcp/issues) in the public GitHub repository — we’d love to hear from you! diff --git a/servers/Azure.Mcp.Server/TROUBLESHOOTING.md b/servers/Azure.Mcp.Server/TROUBLESHOOTING.md index 8fad429c1f..1d2fcfec11 100644 --- a/servers/Azure.Mcp.Server/TROUBLESHOOTING.md +++ b/servers/Azure.Mcp.Server/TROUBLESHOOTING.md @@ -21,6 +21,7 @@ This guide helps you diagnose and resolve common issues with the Azure MCP Serve - [Remote MCP Server (preview)](#remote-mcp-server-preview) - [Logging and Diagnostics](#logging-and-diagnostics) - [Logging](#logging) + - [Support Logging](#support-logging) - [Collecting logs with dotnet-trace](#collecting-logs-with-dotnet-trace) - [Collecting logs with VS Code](#collecting-logs-with-vs-code) - [Collecting logs with PerfView](#collecting-logs-with-perfview) @@ -913,6 +914,45 @@ The Azure MCP Server is instrumented using the .NET [EventSource](https://learn. Server logs can be obtained by capturing events for provider "Microsoft-Extensions-Logging". +#### Support Logging + +For troubleshooting scenarios, you can enable detailed debug-level logging using the `--dangerously-write-support-logs-to-dir` option. This option creates log files with automatically-generated timestamps (e.g., `azmcp_20251202_143052.log`) in the specified folder, ensuring logs are written locally and not accidentally sent over the network. + +> [!WARNING] +> Support logging may include sensitive information. Use with extreme caution and only when requested by support. + +> [!NOTE] +> When support logging is enabled, all telemetry is automatically disabled to prevent sensitive debug information from being sent to telemetry endpoints. + +**Example configuration in mcp.json:** + +```json +{ + "servers": { + "Azure MCP Server (Support Mode)": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@azure/mcp@latest", + "server", + "start", + "--dangerously-write-support-logs-to-dir", + "/path/to/logs" + ] + } + } +} +``` + +**Command-line usage:** + +```bash +azmcp server start --dangerously-write-support-logs-to-dir /path/to/logs +``` + +The log files will be created with timestamp-based names (e.g., `azmcp_20251202_143052.log`) and will contain detailed debug-level information that can help diagnose issues with the Azure MCP Server. + #### Collecting logs with dotnet-trace `dotnet-trace` is a cross-platform CLI for collecting .NET Core traces: