diff --git a/core/Microsoft.Mcp.Core/src/Commands/CommandFactory.cs b/core/Microsoft.Mcp.Core/src/Commands/CommandFactory.cs index 670e2d733a..495be79c53 100644 --- a/core/Microsoft.Mcp.Core/src/Commands/CommandFactory.cs +++ b/core/Microsoft.Mcp.Core/src/Commands/CommandFactory.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.CommandLine.Help; +using System.CommandLine.Parsing; using System.Diagnostics; using System.Net; using System.Reflection; @@ -191,6 +192,7 @@ private void ConfigureCommandHandler(Command command, IBaseCommand implementatio using var activity = _telemetryService.StartActivity(ActivityName.CommandExecuted); activity?.SetTag(TagName.ToolId, implementation.Id); + InjectToolAreaAndName(activity, parseResult); var cmdContext = new CommandContext(_serviceProvider, activity); var startTime = DateTime.UtcNow; try @@ -327,6 +329,34 @@ private void CustomizeHelpOption(Command command) } } + /// + /// Injects tool area and name tags into the activity based on the command being executed. The full command name + /// is parsed to determine the tool area and name, which are then added as tags to the activity. + /// + /// The activity to inject tool area and name tags into. + /// The parsing result for the command. + private void InjectToolAreaAndName(Activity? activity, ParseResult parseResult) + { + if (activity == null) + { + return; + } + + var commandResult = parseResult.CommandResult; + var fullCommandName = commandResult.Command.Name; + while (commandResult.Parent is not null + && commandResult?.Parent is CommandResult + && (commandResult.Parent as CommandResult)?.Command.Name != RootCommand.Name) + { + commandResult = (commandResult.Parent as CommandResult)!; + fullCommandName = commandResult.Command.Name + "_" + fullCommandName; + } + + var index = fullCommandName.IndexOf('_'); + activity.SetTag(TagName.ToolArea, index == -1 ? fullCommandName : fullCommandName.Substring(0, index)) + .SetTag(TagName.ToolName, fullCommandName); + } + /// /// Creates a command dictionary. Each sibling and child of the root node is created without using its name as a prefix. /// diff --git a/core/Microsoft.Mcp.Core/src/Extensions/OpenTelemetryExtensions.cs b/core/Microsoft.Mcp.Core/src/Extensions/OpenTelemetryExtensions.cs index dd0f7aa524..339ae90802 100644 --- a/core/Microsoft.Mcp.Core/src/Extensions/OpenTelemetryExtensions.cs +++ b/core/Microsoft.Mcp.Core/src/Extensions/OpenTelemetryExtensions.cs @@ -103,7 +103,7 @@ private static void EnableAzureMonitor(this IServiceCollection services) #endif var enableOtlp = Environment.GetEnvironmentVariable("AZURE_MCP_ENABLE_OTLP_EXPORTER"); - if (!string.IsNullOrEmpty(enableOtlp) && bool.TryParse(enableOtlp, out var shouldEnable) && shouldEnable) + if (true) { otelBuilder.WithTracing(tracing => tracing.AddOtlpExporter()) .WithMetrics(metrics => metrics.AddOtlpExporter()) diff --git a/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs index f75961ef4e..f81902249d 100644 --- a/servers/Azure.Mcp.Server/src/Program.cs +++ b/servers/Azure.Mcp.Server/src/Program.cs @@ -5,6 +5,7 @@ using Azure.Mcp.Core.Commands; using Azure.Mcp.Core.Helpers; using Azure.Mcp.Core.Services.Azure; +using Azure.Mcp.Core.Services.Azure.Authentication; using Azure.Mcp.Core.Services.Azure.ResourceGroup; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; @@ -12,6 +13,7 @@ using Azure.Mcp.Core.Services.ProcessExecution; using Azure.Mcp.Core.Services.Time; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Mcp.Core.Areas; @@ -50,7 +52,7 @@ private static async Task Main(string[] args) ServiceCollection services = new(); - ConfigureServices(services); + ConfigureServices(services, [.. Areas.Where(a => a is ServerSetup)]); services.AddLogging(builder => { @@ -64,7 +66,40 @@ private static async Task Main(string[] args) var commandFactory = serviceProvider.GetRequiredService(); var rootCommand = commandFactory.RootCommand; var parseResult = rootCommand.Parse(args); - var status = await parseResult.InvokeAsync(); + int status = 0; + + if (parseResult.Errors.Count > 0) + { + // Command wasn't one of the registered ServerSetup commands, so bind up a Host of all the services + // to run the command. + var builder = Host.CreateApplicationBuilder(); + builder.Logging.ClearProviders(); + builder.Logging.AddEventSourceLogger(); + builder.Services.AddSingleIdentityTokenCredentialProvider(); + ConfigureServices(builder.Services); + builder.Services.AddAzureMcpServer(new ServiceStartOptions() + { + Transport = TransportTypes.StdIo + }); + + using var host = builder.Build(); + + await InitializeServicesAsync(host.Services); + await host.StartAsync(); + + commandFactory = host.Services.GetRequiredService(); + rootCommand = commandFactory.RootCommand; + parseResult = rootCommand.Parse(args); + + status = await parseResult.InvokeAsync(); + + await host.StopAsync(); + await host.WaitForShutdownAsync(); + } + else + { + status = await parseResult.InvokeAsync(); + } if (status == 0) { @@ -214,7 +249,60 @@ private static void WriteResponse(CommandResponse response) /// /// /// A service collection. - internal static void ConfigureServices(IServiceCollection services) + internal static void ConfigureServices(IServiceCollection services) => ConfigureServices(services, Areas); + + /// + /// + /// Configures services for dependency injection. + /// + /// + /// WARNING: This method is being used for TWO DEPENDENCY INJECTION CONTAINERS: + /// + /// + /// + /// 's command picking: The container used to populate instances of + /// and selected by + /// based on the command line input. This container is a local variable in + /// , and it is not tied to + /// Microsoft.Extensions.Hosting.IHostBuilder (stdio) nor any + /// Microsoft.AspNetCore.Hosting.IWebHostBuilder (http). + /// + /// + /// 's execution: The container is created by some + /// dynamically created Microsoft.Extensions.Hosting.IHostBuilder (stdio) or + /// Microsoft.AspNetCore.Hosting.IWebHostBuilder (http). While the + /// instance of + /// is created by the first container, this second container it creates and runs is + /// built separately during . Thus, this + /// container is built and this method is called sometime + /// during that method execution. + /// + /// + /// + /// DUE TO THIS DUAL USAGE, PLEASE BE VERY CAREFUL WHEN MODIFYING THIS METHOD. This + /// method may have some expectations, but it and all methods it calls must be safe for + /// both the stdio and http transport modes. + /// + /// + /// For example, most instances take an indirect dependency + /// on or , both of which have + /// transport-specific implementations. This method can add the stdio-specific + /// implementation to allow the first container (used for command picking) to work, + /// but such transport-specific registrations must be overridden within + /// with the appropriate + /// transport-specific implementation based on command line arguments. + /// + /// + /// This large doc comment is copy/pasta in each Program.cs file of this repo, so if + /// you're reading this, please keep them in sync and/or add specific warnings per + /// project if needed. Below is the list of known differences: + /// + /// + /// No differences. This is also copy/pasta as a placeholder for this project. + /// + /// + /// A service collection. + private static void ConfigureServices(IServiceCollection services, IAreaSetup[] areas) { var thisAssembly = typeof(Program).Assembly; @@ -236,7 +324,7 @@ internal static void ConfigureServices(IServiceCollection services) services.AddAzureTenantService(); services.AddSingleUserCliCacheService(); - foreach (var area in Areas) + foreach (var area in areas) { services.AddSingleton(area); area.ConfigureServices(services); @@ -254,6 +342,7 @@ internal static void ConfigureServices(IServiceCollection services) ActivatorUtilities.CreateInstance(sp, thisAssembly, $"allowed-plugin-file-references.json")); } + internal static async Task InitializeServicesAsync(IServiceProvider serviceProvider) { ServiceStartOptions? options = serviceProvider.GetService>()?.Value;