Skip to content
Draft
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
30 changes: 30 additions & 0 deletions core/Microsoft.Mcp.Core/src/Commands/CommandFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -327,6 +329,34 @@ private void CustomizeHelpOption(Command command)
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="activity">The activity to inject tool area and name tags into.</param>
/// <param name="parseResult">The parsing result for the command.</param>
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);
}

/// <summary>
/// Creates a command dictionary. Each sibling and child of the root node is created without using its name as a prefix.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
97 changes: 93 additions & 4 deletions servers/Azure.Mcp.Server/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
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;
using Azure.Mcp.Core.Services.Caching;
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;
Expand Down Expand Up @@ -50,7 +52,7 @@ private static async Task<int> Main(string[] args)

ServiceCollection services = new();

ConfigureServices(services);
ConfigureServices(services, [.. Areas.Where(a => a is ServerSetup)]);

services.AddLogging(builder =>
{
Expand All @@ -64,7 +66,40 @@ private static async Task<int> Main(string[] args)
var commandFactory = serviceProvider.GetRequiredService<ICommandFactory>();
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<ICommandFactory>();
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)
{
Expand Down Expand Up @@ -214,7 +249,60 @@ private static void WriteResponse(CommandResponse response)
/// </list>
/// </summary>
/// <param name="services">A service collection.</param>
internal static void ConfigureServices(IServiceCollection services)
internal static void ConfigureServices(IServiceCollection services) => ConfigureServices(services, Areas);

/// <summary>
/// <para>
/// Configures services for dependency injection.
/// </para>
/// <para>
/// WARNING: This method is being used for TWO DEPENDENCY INJECTION CONTAINERS:
/// </para>
/// <list type="number">
/// <item>
/// <see cref="Main"/>'s command picking: The container used to populate instances of
/// <see cref="IBaseCommand"/> and selected by <see cref="CommandFactory"/>
/// based on the command line input. This container is a local variable in
/// <see cref="Main"/>, and it is not tied to
/// <c>Microsoft.Extensions.Hosting.IHostBuilder</c> (stdio) nor any
/// <c>Microsoft.AspNetCore.Hosting.IWebHostBuilder</c> (http).
/// </item>
/// <item>
/// <see cref="ServiceStartCommand"/>'s execution: The container is created by some
/// dynamically created <c>Microsoft.Extensions.Hosting.IHostBuilder</c> (stdio) or
/// <c>Microsoft.AspNetCore.Hosting.IWebHostBuilder</c> (http). While the
/// <see cref="IBaseCommand.ExecuteAsync"/>instance of <see cref="ServiceStartCommand"/>
/// is created by the first container, this second container it creates and runs is
/// built separately during <see cref="ServiceStartCommand.ExecuteAsync"/>. Thus, this
/// container is built and this <see cref="ConfigureServices"/> method is called sometime
/// during that method execution.
/// </item>
/// </list>
/// <para>
/// 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.
/// </para>
/// <para>
/// For example, most <see cref="IBaseCommand"/> instances take an indirect dependency
/// on <see cref="ITenantService"/> or <see cref="ICacheService"/>, 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
/// <see cref="ServiceStartCommand.ExecuteAsync"/> with the appropriate
/// transport-specific implementation based on command line arguments.
/// </para>
/// <para>
/// 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:
/// </para>
/// <list type="bullet">
/// <item>No differences. This is also copy/pasta as a placeholder for this project.</item>
/// </list>
/// </summary>
/// <param name="services">A service collection.</param>
private static void ConfigureServices(IServiceCollection services, IAreaSetup[] areas)
{
var thisAssembly = typeof(Program).Assembly;

Expand All @@ -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);
Expand All @@ -254,6 +342,7 @@ internal static void ConfigureServices(IServiceCollection services)
ActivatorUtilities.CreateInstance<ResourcePluginFileReferenceAllowlistProvider>(sp, thisAssembly, $"allowed-plugin-file-references.json"));
}


internal static async Task InitializeServicesAsync(IServiceProvider serviceProvider)
{
ServiceStartOptions? options = serviceProvider.GetService<IOptions<ServiceStartOptions>>()?.Value;
Expand Down