Skip to content
This repository was archived by the owner on Sep 4, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The Azure MCP Server updates automatically by default whenever a new release com

### Other Changes

- Implemented centralized HttpClient service with proxy support for better resource management and enterprise compatibility. [[#857](https://github.com/Azure/azure-mcp/pull/857)]
- Added caching for Cosmos DB databases and containers. [[813](https://github.com/Azure/azure-mcp/pull/813)]

#### Dependency Updates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
using Azure.ResourceManager.Resources;
using AzureMcp.Core.Options;
using AzureMcp.Core.Services.Azure;
using AzureMcp.Core.Services.Http;
using AzureMcp.Foundry.Commands;
using AzureMcp.Foundry.Models;

namespace AzureMcp.Foundry.Services;

public class FoundryService : BaseAzureService, IFoundryService
public class FoundryService(IHttpClientService httpClientService) : BaseAzureService, IFoundryService
{
private readonly IHttpClientService _httpClientService = httpClientService ?? throw new ArgumentNullException(nameof(httpClientService));
public async Task<List<ModelInformation>> ListModels(
bool searchForFreePlayground = false,
string publisherName = "",
Expand Down Expand Up @@ -63,7 +65,7 @@ public async Task<List<ModelInformation>> ListModels(
Encoding.UTF8,
"application/json");

var httpResponse = await new HttpClient().PostAsync(url, content);
var httpResponse = await _httpClientService.DefaultClient.PostAsync(url, content);
httpResponse.EnsureSuccessStatusCode();

var responseText = await httpResponse.Content.ReadAsStringAsync();
Expand Down
6 changes: 4 additions & 2 deletions areas/kusto/src/AzureMcp.Kusto/Services/KustoClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@

using System.Text.Json.Nodes;
using Azure.Core;
using AzureMcp.Core.Services.Http;

namespace AzureMcp.Kusto.Services;

public class KustoClient(
string clusterUri,
TokenCredential tokenCredential,
string userAgent)
string userAgent,
IHttpClientService httpClientService)
{
private readonly string _clusterUri = clusterUri;
private readonly TokenCredential _tokenCredential = tokenCredential;
private readonly string _userAgent = userAgent;
private readonly HttpClient _httpClient = new() { BaseAddress = new Uri(clusterUri) };
private readonly HttpClient _httpClient = httpClientService.CreateClient(new Uri(clusterUri));
private static readonly string s_application = "AzureMCP";
private static readonly string s_clientRequestIdPrefix = "AzMcp";
private static readonly string s_default_scope = "https://kusto.kusto.windows.net/.default";
Expand Down
9 changes: 6 additions & 3 deletions areas/kusto/src/AzureMcp.Kusto/Services/KustoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using AzureMcp.Core.Services.Azure.Subscription;
using AzureMcp.Core.Services.Azure.Tenant;
using AzureMcp.Core.Services.Caching;
using AzureMcp.Core.Services.Http;
using AzureMcp.Kusto.Commands;

namespace AzureMcp.Kusto.Services;
Expand All @@ -15,10 +16,12 @@ namespace AzureMcp.Kusto.Services;
public sealed class KustoService(
ISubscriptionService subscriptionService,
ITenantService tenantService,
ICacheService cacheService) : BaseAzureService(tenantService), IKustoService
ICacheService cacheService,
IHttpClientService httpClientService) : BaseAzureService(tenantService), IKustoService
{
private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService));
private readonly ICacheService _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
private readonly IHttpClientService _httpClientService = httpClientService ?? throw new ArgumentNullException(nameof(httpClientService));

private const string CacheGroup = "kusto";
private const string KustoClustersCacheKey = "clusters";
Expand Down Expand Up @@ -285,7 +288,7 @@ private async Task<KustoClient> GetOrCreateKustoClient(string clusterUri, string
if (kustoClient == null)
{
var tokenCredential = await GetCredential(tenant);
kustoClient = new KustoClient(clusterUri, tokenCredential, UserAgent);
kustoClient = new KustoClient(clusterUri, tokenCredential, UserAgent, _httpClientService);
await _cacheService.SetAsync(CacheGroup, providerCacheKey, kustoClient, s_providerCacheDuration);
}

Expand All @@ -299,7 +302,7 @@ private async Task<KustoClient> GetOrCreateCslQueryProvider(string clusterUri, s
if (kustoClient == null)
{
var tokenCredential = await GetCredential(tenant);
kustoClient = new KustoClient(clusterUri, tokenCredential, UserAgent);
kustoClient = new KustoClient(clusterUri, tokenCredential, UserAgent, _httpClientService);
await _cacheService.SetAsync(CacheGroup, providerCacheKey, kustoClient, s_providerCacheDuration);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

using System.Text.Json;
using Azure.Identity;
using AzureMcp.Core.Services.Http;
using AzureMcp.Kusto.Services;
using AzureMcp.Tests;
using AzureMcp.Tests.Client;
using AzureMcp.Tests.Client.Helpers;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using Xunit;

Expand Down Expand Up @@ -38,7 +40,12 @@ public async ValueTask InitializeAsync()
{ "cluster-name", Settings.ResourceBaseName }
});
var clusterUri = clusterInfo.AssertProperty("cluster").AssertProperty("clusterUri").GetString();
var kustoClient = new KustoClient(clusterUri ?? string.Empty, credentials, "ua");

// Create HttpClientService for KustoClient
var httpClientOptions = new HttpClientOptions();
var httpClientService = new HttpClientService(Microsoft.Extensions.Options.Options.Create(httpClientOptions));

var kustoClient = new KustoClient(clusterUri ?? string.Empty, credentials, "ua", httpClientService);
var resp = await kustoClient.ExecuteControlCommandAsync(
TestDatabaseName,
".set-or-replace ToDoList <| datatable (Title: string, IsCompleted: bool) [' Hello World!', false]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@
using AzureMcp.Core.Options;
using AzureMcp.Core.Services.Azure;
using AzureMcp.Core.Services.Azure.Tenant;
using AzureMcp.Core.Services.Http;

namespace AzureMcp.Monitor.Services;

public class MonitorHealthModelService(ITenantService tenantService)
public class MonitorHealthModelService(ITenantService tenantService, IHttpClientService httpClientService)
: BaseAzureService(tenantService), IMonitorHealthModelService
{
private const int TokenExpirationBuffer = 300;
private const string ManagementApiBaseUrl = "https://management.azure.com";
private const string HealthModelsDataApiScope = "https://data.healthmodels.azure.com";
private const string ApiVersion = "2023-10-01-preview";
private static readonly HttpClient s_sharedHttpClient = new HttpClient();
private readonly IHttpClientService _httpClientService = httpClientService;

private string? _cachedDataplaneAccessToken;
private string? _cachedControlPlaneAccessToken;
Expand Down Expand Up @@ -60,7 +61,7 @@ private async Task<string> GetDataplaneResponseAsync(string url)
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", dataplaneToken);

HttpResponseMessage healthResponse = await s_sharedHttpClient.SendAsync(request);
HttpResponseMessage healthResponse = await _httpClientService.DefaultClient.SendAsync(request);
healthResponse.EnsureSuccessStatusCode();

string healthResponseString = await healthResponse.Content.ReadAsStringAsync();
Expand All @@ -75,7 +76,7 @@ private async Task<string> GetDataplaneEndpointAsync(string subscriptionId, stri
using var request = new HttpRequestMessage(HttpMethod.Get, healthModelUrl);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

HttpResponseMessage response = await s_sharedHttpClient.SendAsync(request);
HttpResponseMessage response = await _httpClientService.DefaultClient.SendAsync(request);
response.EnsureSuccessStatusCode();
string responseString = await response.Content.ReadAsStringAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using AzureMcp.Core.Areas.Server.Commands.ToolLoading;
using AzureMcp.Core.Areas.Server.Options;
using AzureMcp.Core.Commands;
using AzureMcp.Core.Extensions;
using AzureMcp.Core.Services.Telemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -34,6 +35,9 @@ public static class AzureMcpServiceCollectionExtensions
/// <returns>The service collection with MCP server services added.</returns>
public static IServiceCollection AddAzureMcpServer(this IServiceCollection services, ServiceStartOptions serviceStartOptions)
{
// Register HTTP client services
services.AddHttpClientServices();

// Register options for service start
services.AddSingleton(serviceStartOptions);
services.AddSingleton(Options.Create(serviceStartOptions));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using AzureMcp.Core.Services.Http;
using Microsoft.Extensions.DependencyInjection;

namespace AzureMcp.Core.Extensions;

/// <summary>
/// Extension methods for registering HTTP client services.
/// </summary>
public static class HttpClientServiceCollectionExtensions
{
/// <summary>
/// Adds HTTP client services to the service collection with default configuration.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddHttpClientServices(this IServiceCollection services)
{
return services.AddHttpClientServices(_ => { });
}

/// <summary>
/// Adds HTTP client services to the service collection with custom configuration.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configureOptions">Action to configure HttpClient options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, Action<HttpClientOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);

// Configure options with environment variables
services.Configure<HttpClientOptions>(options =>
{
// Read proxy configuration from environment variables
options.AllProxy = Environment.GetEnvironmentVariable("ALL_PROXY");
options.HttpProxy = Environment.GetEnvironmentVariable("HTTP_PROXY");
options.HttpsProxy = Environment.GetEnvironmentVariable("HTTPS_PROXY");
options.NoProxy = Environment.GetEnvironmentVariable("NO_PROXY");

// Apply custom configuration
configureOptions(options);
});

// Register the HTTP client service
services.AddSingleton<IHttpClientService, HttpClientService>();

return services;
}
}
40 changes: 40 additions & 0 deletions core/src/AzureMcp.Core/Services/Http/HttpClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace AzureMcp.Core.Services.Http;

/// <summary>
/// Configuration options for HttpClient services.
/// </summary>
public sealed class HttpClientOptions
{
/// <summary>
/// Gets or sets the HTTP proxy address. Can be set via HTTP_PROXY environment variable.
/// </summary>
public string? HttpProxy { get; set; }

/// <summary>
/// Gets or sets the HTTPS proxy address. Can be set via HTTPS_PROXY environment variable.
/// </summary>
public string? HttpsProxy { get; set; }

/// <summary>
/// Gets or sets the proxy address for all protocols. Can be set via ALL_PROXY environment variable.
/// </summary>
public string? AllProxy { get; set; }

/// <summary>
/// Gets or sets the comma-separated list of hostnames that should bypass the proxy. Can be set via NO_PROXY environment variable.
/// </summary>
public string? NoProxy { get; set; }

/// <summary>
/// Gets or sets the default timeout for HTTP requests. Defaults to 100 seconds.
/// </summary>
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);

/// <summary>
/// Gets or sets the default User-Agent header value.
/// </summary>
public string? DefaultUserAgent { get; set; }
}
Loading
Loading