Skip to content

Commit 3387b10

Browse files
authored
Add initial support for configuration (#1269)
* Enable binding for configuration + options. * Move ConfigureServiceOptions out of OpenTelemetryExtensions * Update program to use ConfigureMcpServerOptions * Add configuration properties to pojo * Add Development and default appsettings. * Add appsettings.json to build output * Remove hardcoded values. * Fix build error. * Fix extension method name. * Update Fabric to support configuration. * Add changelog * Fix class name to match file name. Remove duplicate variable. * Add tests for InitializeConfigurationAndOptions * Fix Analyze Code step. * Update pre-push Hook to ignore IL2026/IL3050.
1 parent fe2e0b3 commit 3387b10

File tree

26 files changed

+359
-65
lines changed

26 files changed

+359
-65
lines changed

core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
using Azure.Mcp.Core.Areas.Server.Commands.ToolLoading;
99
using Azure.Mcp.Core.Areas.Server.Options;
1010
using Azure.Mcp.Core.Commands;
11+
using Azure.Mcp.Core.Configuration;
1112
using Azure.Mcp.Core.Helpers;
13+
using Microsoft.Extensions.Configuration;
1214
using Microsoft.Extensions.DependencyInjection;
1315
using Microsoft.Extensions.Logging;
1416
using Microsoft.Extensions.Options;
@@ -23,10 +25,8 @@ namespace Azure.Mcp.Core.Areas.Server.Commands;
2325
/// <summary>
2426
/// Extension methods for configuring Azure MCP server services.
2527
/// </summary>
26-
public static class AzureMcpServiceCollectionExtensions
28+
public static class ServiceCollectionExtensions
2729
{
28-
private const string DefaultServerName = "Azure MCP Server";
29-
3030
/// <summary>
3131
/// Adds the Azure MCP server services to the specified <see cref="IServiceCollection"/>.
3232
/// </summary>
@@ -210,18 +210,15 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
210210

211211
var mcpServerOptions = services
212212
.AddOptions<McpServerOptions>()
213-
.Configure<IMcpRuntime>((mcpServerOptions, mcpRuntime) =>
213+
.Configure<IMcpRuntime, IOptions<AzureMcpServerConfiguration>>((mcpServerOptions, mcpRuntime, serverConfiguration) =>
214214
{
215-
var mcpServerOptionsBuilder = services.AddOptions<McpServerOptions>();
216-
var entryAssembly = Assembly.GetEntryAssembly();
217-
var assemblyName = entryAssembly?.GetName();
218-
var serverName = entryAssembly?.GetCustomAttribute<AssemblyTitleAttribute>()?.Title ?? DefaultServerName;
215+
var configuration = serverConfiguration.Value;
219216

220217
mcpServerOptions.ProtocolVersion = "2024-11-05";
221218
mcpServerOptions.ServerInfo = new Implementation
222219
{
223-
Name = serverName,
224-
Version = assemblyName?.Version?.ToString() ?? "1.0.0-beta"
220+
Name = configuration.DisplayName,
221+
Version = configuration.Version,
225222
};
226223

227224
mcpServerOptions.Handlers = new()
@@ -248,6 +245,48 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
248245
return services;
249246
}
250247

248+
/// <summary>
249+
/// Using <see cref="IConfiguration"/> configures <see cref="AzureMcpServerConfiguration"/>.
250+
/// </summary>
251+
/// <param name="services">Service Collection to add configuration logic to.</param>
252+
public static void InitializeConfigurationAndOptions(this IServiceCollection services)
253+
{
254+
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
255+
var configuration = new ConfigurationBuilder()
256+
.AddJsonFile("appsettings.json", optional: false)
257+
.AddJsonFile($"appsettings.{environment}.json", optional: true)
258+
.AddEnvironmentVariables()
259+
.SetBasePath(AppContext.BaseDirectory)
260+
.Build();
261+
services.AddSingleton<IConfiguration>(configuration);
262+
263+
services.AddOptions<AzureMcpServerConfiguration>()
264+
.BindConfiguration(string.Empty)
265+
.Configure<IConfiguration, IOptions<ServiceStartOptions>>((options, rootConfiguration, serviceStartOptions) =>
266+
{
267+
// This environment variable can be used to disable telemetry collection entirely. This takes precedence
268+
// over any other settings.
269+
var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true);
270+
var transport = serviceStartOptions.Value.Transport;
271+
var isStdioTransport = string.IsNullOrEmpty(transport)
272+
|| string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase);
273+
274+
// Assembly.GetEntryAssembly is used to retrieve the version of the server application as that is
275+
// the assembly that will run the tool calls.
276+
var entryAssembly = Assembly.GetEntryAssembly();
277+
if (entryAssembly == null)
278+
{
279+
throw new InvalidOperationException("Entry assembly must be a managed assembly.");
280+
}
281+
282+
options.Version = AssemblyHelper.GetAssemblyVersion(entryAssembly);
283+
284+
// if transport is not set (default to stdio) or is set to stdio, enable telemetry
285+
// telemetry is disabled for HTTP transport
286+
options.IsTelemetryEnabled = collectTelemetry && isStdioTransport;
287+
});
288+
}
289+
251290
/// <summary>
252291
/// Generates comprehensive instructions for using the Azure MCP Server effectively.
253292
/// Includes Azure best practices from embedded resource files.
@@ -281,7 +320,7 @@ private static string GetServerInstructions()
281320
/// <returns>Combined content from all Azure best practices resource files.</returns>
282321
private static string LoadAzureRulesForBestPractices()
283322
{
284-
var coreAssembly = typeof(AzureMcpServiceCollectionExtensions).Assembly;
323+
var coreAssembly = typeof(ServiceCollectionExtensions).Assembly;
285324
var azureRulesContent = new StringBuilder();
286325

287326
// List of known best practices resource files

core/Azure.Mcp.Core/src/Azure.Mcp.Core.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<IsAotCompatible>true</IsAotCompatible>
4+
<!-- Enable strongly typed binding for IConfiguration to support AOT and trimming. -->
5+
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
46
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
57
</PropertyGroup>
68

core/Azure.Mcp.Core/src/Commands/CommandFactory.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,19 @@ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOp
5050
}
5151
}
5252

53-
internal const string RootCommandGroupName = "azmcp";
5453

55-
public CommandFactory(IServiceProvider serviceProvider, IEnumerable<IAreaSetup> serviceAreas, ITelemetryService telemetryService, IOptions<AzureMcpServerConfiguration> configurationOptions, ILogger<CommandFactory> logger)
54+
public CommandFactory(IServiceProvider serviceProvider,
55+
IEnumerable<IAreaSetup> serviceAreas,
56+
ITelemetryService telemetryService,
57+
IOptions<AzureMcpServerConfiguration> configurationOptions,
58+
ILogger<CommandFactory> logger)
5659
{
5760
_serviceAreas = serviceAreas?.ToArray() ?? throw new ArgumentNullException(nameof(serviceAreas));
5861
_serviceProvider = serviceProvider;
5962
_logger = logger;
6063
_telemetryService = telemetryService;
6164
_configurationOptions = configurationOptions;
62-
_rootGroup = new CommandGroup(RootCommandGroupName, "Azure MCP Server");
65+
_rootGroup = new CommandGroup(_configurationOptions.Value.RootCommandGroupName, _configurationOptions.Value.DisplayName);
6366
_rootCommand = CreateRootCommand();
6467
_commandMap = CreateCommandDictionary(_rootGroup);
6568
_srcGenWithOptions = new ModelsJsonContext(new JsonSerializerOptions
@@ -140,7 +143,7 @@ private void RegisterCommandGroup()
140143

141144
// Create a temporary root node to register all the area's subgroups and commands to.
142145
// Use this to create the mapping of all commands to that area.
143-
var tempRoot = new CommandGroup(RootCommandGroupName, string.Empty);
146+
var tempRoot = new CommandGroup(_rootGroup.Name, string.Empty);
144147
tempRoot.AddSubGroup(commandTree);
145148

146149
var commandDictionary = CreateCommandDictionary(tempRoot);

core/Azure.Mcp.Core/src/Configuration/AzureMcpServerConfiguration.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,33 @@
33

44
namespace Azure.Mcp.Core.Configuration;
55

6+
/// <summary>
7+
/// Configuration settings for the MCP server.
8+
/// </summary>
69
public class AzureMcpServerConfiguration
710
{
8-
public const string DefaultName = "Azure.Mcp.Server";
11+
/// <summary>
12+
/// The default prefix for the MCP server commands and help menus.
13+
/// </summary>
14+
public required string RootCommandGroupName { get; set; }
915

10-
public string Name { get; set; } = DefaultName;
16+
/// <summary>
17+
/// The name of the MCP server. (i.e. Azure.Mcp.Server)
18+
/// </summary>
19+
public required string Name { get; set; }
1120

12-
public string Version { get; set; } = "1.0.0-beta";
21+
/// <summary>
22+
/// The display name of the MCP server.
23+
/// </summary>
24+
public required string DisplayName { get; set; }
1325

26+
/// <summary>
27+
/// The version of the MCP server.
28+
/// </summary>
29+
public required string Version { get; set; }
30+
31+
/// <summary>
32+
/// Indicates whether telemetry is enabled for the MCP server. By default, it is set to true.
33+
/// </summary>
1434
public bool IsTelemetryEnabled { get; set; } = true;
1535
}

core/Azure.Mcp.Core/src/Extensions/OpenTelemetryExtensions.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,6 @@ public static class OpenTelemetryExtensions
2828

2929
public static void ConfigureOpenTelemetry(this IServiceCollection services)
3030
{
31-
services.AddOptions<AzureMcpServerConfiguration>()
32-
.Configure<IOptions<ServiceStartOptions>>((options, serviceStartOptions) =>
33-
{
34-
// Assembly.GetEntryAssembly is used to retrieve the version of the server application as that is
35-
// the assembly that will run the tool calls.
36-
var entryAssembly = Assembly.GetEntryAssembly();
37-
if (entryAssembly != null)
38-
{
39-
options.Version = AssemblyHelper.GetAssemblyVersion(entryAssembly);
40-
}
41-
42-
// This environment variable can be used to disable telemetry collection entirely. This takes precedence
43-
// over any other settings.
44-
var collectTelemetry = Environment.GetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY");
45-
46-
options.IsTelemetryEnabled = string.IsNullOrWhiteSpace(collectTelemetry) || (bool.TryParse(collectTelemetry, out var shouldCollect) && shouldCollect);
47-
});
48-
4931
services.AddSingleton<ITelemetryService, TelemetryService>();
5032

5133
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/CommandFactoryHelpers.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ public static CommandFactory CreateCommandFactory(IServiceProvider? serviceProvi
101101
var configurationOptions = Microsoft.Extensions.Options.Options.Create(new AzureMcpServerConfiguration
102102
{
103103
Name = "Test Server",
104-
Version = "Test Version"
104+
Version = "Test Version",
105+
DisplayName = "Test Display",
106+
RootCommandGroupName = "azmcp"
105107
});
106108
var telemetryService = services.GetService<ITelemetryService>() ?? new NoOpTelemetryService();
107109
var commandFactory = new CommandFactory(services, areaSetups, telemetryService, configurationOptions, logger);

core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategyTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ private static ConsolidatedToolDiscoveryStrategy CreateStrategy(
2323
var configurationOptions = Microsoft.Extensions.Options.Options.Create(new AzureMcpServerConfiguration
2424
{
2525
Name = "Test Server",
26-
Version = "Test Version"
26+
Version = "Test Version",
27+
DisplayName = "Test Display",
28+
RootCommandGroupName = "azmcp"
2729
});
2830
var logger = NSubstitute.Substitute.For<Microsoft.Extensions.Logging.ILogger<ConsolidatedToolDiscoveryStrategy>>();
2931
var strategy = new ConsolidatedToolDiscoveryStrategy(factory, serviceProvider, startOptions, configurationOptions, logger);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Mcp.Core.Areas.Server.Commands;
5+
using Azure.Mcp.Core.Areas.Server.Options;
6+
using Azure.Mcp.Core.Commands;
7+
using Azure.Mcp.Core.Configuration;
8+
using Azure.Mcp.Core.Helpers;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
11+
using Xunit;
12+
13+
namespace Azure.Mcp.Core.UnitTests.Areas.Server.Commands;
14+
15+
// This is intentionally placed after the namespace declaration to avoid
16+
// conflicts with Azure.Mcp.Core.Areas.Server.Options
17+
using Options = Microsoft.Extensions.Options.Options;
18+
19+
[Collection("Sequential")]
20+
public class ServiceCollectionExtensionsSerializedTests
21+
{
22+
private IServiceCollection SetupBaseServices()
23+
{
24+
var services = CommandFactoryHelpers.SetupCommonServices();
25+
services.AddSingleton<CommandFactory>(sp => CommandFactoryHelpers.CreateCommandFactory(sp));
26+
27+
return services;
28+
}
29+
30+
[Fact]
31+
public void InitializeConfigurationAndOptions_Defaults()
32+
{
33+
// Assert
34+
var expectedVersion = AssemblyHelper.GetAssemblyVersion(typeof(ServiceCollectionExtensionsTests).Assembly);
35+
var services = SetupBaseServices();
36+
37+
// Act
38+
ServiceCollectionExtensions.InitializeConfigurationAndOptions(services);
39+
40+
// Assert
41+
var provider = services.BuildServiceProvider();
42+
var options = provider.GetRequiredService<IOptions<AzureMcpServerConfiguration>>();
43+
44+
Assert.NotNull(options.Value);
45+
46+
var actual = options.Value;
47+
Assert.Equal("Azure.Mcp.Server", actual.Name);
48+
Assert.Equal("Azure MCP Server", actual.DisplayName);
49+
Assert.Equal("azmcp", actual.RootCommandGroupName);
50+
Assert.Equal(expectedVersion, actual.Version);
51+
52+
Assert.True(actual.IsTelemetryEnabled);
53+
}
54+
55+
/// <summary>
56+
/// When <see cref="TransportTypes.Http"/> is used, telemetry is disabled
57+
/// even when AZURE_MCP_COLLECT_TELEMETRY is explicitly set to true.
58+
/// </summary>
59+
[Fact]
60+
public void InitializeConfigurationAndOptions_HttpTransport()
61+
{
62+
// Assert
63+
var serviceStartOptions = new ServiceStartOptions
64+
{
65+
Transport = TransportTypes.Http,
66+
};
67+
var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions));
68+
69+
// Act
70+
Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", "true");
71+
ServiceCollectionExtensions.InitializeConfigurationAndOptions(services);
72+
var provider = services.BuildServiceProvider();
73+
74+
// Assert
75+
var options = provider.GetRequiredService<IOptions<AzureMcpServerConfiguration>>();
76+
77+
Assert.NotNull(options.Value);
78+
79+
var actual = options.Value;
80+
Assert.Equal("Azure.Mcp.Server", actual.Name);
81+
Assert.Equal("Azure MCP Server", actual.DisplayName);
82+
Assert.Equal("azmcp", actual.RootCommandGroupName);
83+
Assert.False(actual.IsTelemetryEnabled);
84+
}
85+
86+
[Fact]
87+
public void InitializeConfigurationAndOptions_Stdio()
88+
{
89+
// Assert
90+
var expectedVersion = AssemblyHelper.GetAssemblyVersion(typeof(ServiceCollectionExtensionsTests).Assembly);
91+
var services = SetupBaseServices();
92+
93+
// Act
94+
Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", "false");
95+
ServiceCollectionExtensions.InitializeConfigurationAndOptions(services);
96+
var provider = services.BuildServiceProvider();
97+
98+
// Assert
99+
var options = provider.GetRequiredService<IOptions<AzureMcpServerConfiguration>>();
100+
101+
Assert.NotNull(options.Value);
102+
103+
var actual = options.Value;
104+
Assert.Equal("Azure.Mcp.Server", actual.Name);
105+
Assert.Equal("Azure MCP Server", actual.DisplayName);
106+
Assert.Equal("azmcp", actual.RootCommandGroupName);
107+
Assert.Equal(expectedVersion, actual.Version);
108+
109+
Assert.False(actual.IsTelemetryEnabled);
110+
}
111+
}

core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceInfoCommandTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public ServiceInfoCommandTests()
3232
_mcpServerConfiguration = new AzureMcpServerConfiguration
3333
{
3434
Name = "Test-Name?",
35-
Version = "Test-Version?"
35+
Version = "Test-Version?",
36+
DisplayName = "Test Display",
37+
RootCommandGroupName = "azmcp"
3638
};
3739
_command = new(Microsoft.Extensions.Options.Options.Create(_mcpServerConfiguration), _logger);
3840
_commandDefinition = _command.GetCommand();

core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Tools/UnitTests/ToolsListCommandTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,9 @@ public async Task ExecuteAsync_WithEmptyCommandFactory_ReturnsEmptyResults()
401401
var configurationOptions = Microsoft.Extensions.Options.Options.Create(new AzureMcpServerConfiguration
402402
{
403403
Name = "Test Server",
404-
Version = "Test Version"
404+
Version = "Test Version",
405+
DisplayName = "Test Display",
406+
RootCommandGroupName = "azmcp"
405407
});
406408

407409
// Create a NEW service collection just for the empty command factory

0 commit comments

Comments
 (0)