Skip to content
This repository was archived by the owner on Sep 4, 2025. It is now read-only.

Commit 01da131

Browse files
Copilotjongio
andauthored
Implement centralized HttpClient service with proxy support (#857)
* Initial plan * Implement HttpClient service infrastructure and update existing services Co-authored-by: jongio <[email protected]> * Add comprehensive unit tests for HttpClient service Co-authored-by: jongio <[email protected]> * Add comprehensive documentation for HttpClient service design Co-authored-by: jongio <[email protected]> * Run dotnet format to fix code formatting issues Co-authored-by: jongio <[email protected]> * Update CHANGELOG.md with link to PR 857 for HttpClient service implementation Co-authored-by: jongio <[email protected]> * Fix compilation errors in HttpClient service implementation Co-authored-by: jongio <[email protected]> * Fix regex parsing error in proxy bypass patterns Co-authored-by: jongio <[email protected]> * Fix whitespace formatting in KustoCommandTests.cs Co-authored-by: jongio <[email protected]> * Update .gitignore * Enhance FoundryService constructor to accept optional ITenantService and update HttpClientService proxy logic to prioritize HTTPS_PROXY --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: jongio <[email protected]>
1 parent 94af815 commit 01da131

File tree

15 files changed

+767
-12
lines changed

15 files changed

+767
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The Azure MCP Server updates automatically by default whenever a new release com
5353

5454
### Other Changes
5555

56+
- Implemented centralized HttpClient service with proxy support for better resource management and enterprise compatibility. [[#857](https://github.com/Azure/azure-mcp/pull/857)]
5657
- Added caching for Cosmos DB databases and containers. [[813](https://github.com/Azure/azure-mcp/pull/813)]
5758
- Refactored PostgreSQL commands to follow ObjectVerb naming pattern, fix command hierarchy, and ensure all commands end with verbs. This improves consistency and discoverability across all postgres commands. [[#865](https://github.com/Azure/azure-mcp/issues/865)] [[#866](https://github.com/Azure/azure-mcp/pull/866)]
5859

areas/foundry/src/AzureMcp.Foundry/Services/FoundryService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
using Azure.ResourceManager.Resources;
1111
using AzureMcp.Core.Options;
1212
using AzureMcp.Core.Services.Azure;
13+
using AzureMcp.Core.Services.Azure.Tenant;
14+
using AzureMcp.Core.Services.Http;
1315
using AzureMcp.Foundry.Commands;
1416
using AzureMcp.Foundry.Models;
1517

1618
namespace AzureMcp.Foundry.Services;
1719

18-
public class FoundryService : BaseAzureService, IFoundryService
20+
public class FoundryService(IHttpClientService httpClientService, ITenantService? tenantService = null) : BaseAzureService(tenantService), IFoundryService
1921
{
22+
private readonly IHttpClientService _httpClientService = httpClientService ?? throw new ArgumentNullException(nameof(httpClientService));
2023
public async Task<List<ModelInformation>> ListModels(
2124
bool searchForFreePlayground = false,
2225
string publisherName = "",
@@ -63,7 +66,7 @@ public async Task<List<ModelInformation>> ListModels(
6366
Encoding.UTF8,
6467
"application/json");
6568

66-
var httpResponse = await new HttpClient().PostAsync(url, content);
69+
var httpResponse = await _httpClientService.DefaultClient.PostAsync(url, content);
6770
httpResponse.EnsureSuccessStatusCode();
6871

6972
var responseText = await httpResponse.Content.ReadAsStringAsync();

areas/kusto/src/AzureMcp.Kusto/Services/KustoClient.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33

44
using System.Text.Json.Nodes;
55
using Azure.Core;
6+
using AzureMcp.Core.Services.Http;
67

78
namespace AzureMcp.Kusto.Services;
89

910
public class KustoClient(
1011
string clusterUri,
1112
TokenCredential tokenCredential,
12-
string userAgent)
13+
string userAgent,
14+
IHttpClientService httpClientService)
1315
{
1416
private readonly string _clusterUri = clusterUri;
1517
private readonly TokenCredential _tokenCredential = tokenCredential;
1618
private readonly string _userAgent = userAgent;
17-
private readonly HttpClient _httpClient = new() { BaseAddress = new Uri(clusterUri) };
19+
private readonly HttpClient _httpClient = httpClientService.CreateClient(new Uri(clusterUri));
1820
private static readonly string s_application = "AzureMCP";
1921
private static readonly string s_clientRequestIdPrefix = "AzMcp";
2022
private static readonly string s_default_scope = "https://kusto.kusto.windows.net/.default";

areas/kusto/src/AzureMcp.Kusto/Services/KustoService.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using AzureMcp.Core.Services.Azure.Subscription;
88
using AzureMcp.Core.Services.Azure.Tenant;
99
using AzureMcp.Core.Services.Caching;
10+
using AzureMcp.Core.Services.Http;
1011
using AzureMcp.Kusto.Commands;
1112

1213
namespace AzureMcp.Kusto.Services;
@@ -15,10 +16,12 @@ namespace AzureMcp.Kusto.Services;
1516
public sealed class KustoService(
1617
ISubscriptionService subscriptionService,
1718
ITenantService tenantService,
18-
ICacheService cacheService) : BaseAzureService(tenantService), IKustoService
19+
ICacheService cacheService,
20+
IHttpClientService httpClientService) : BaseAzureService(tenantService), IKustoService
1921
{
2022
private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService));
2123
private readonly ICacheService _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
24+
private readonly IHttpClientService _httpClientService = httpClientService ?? throw new ArgumentNullException(nameof(httpClientService));
2225

2326
private const string CacheGroup = "kusto";
2427
private const string KustoClustersCacheKey = "clusters";
@@ -285,7 +288,7 @@ private async Task<KustoClient> GetOrCreateKustoClient(string clusterUri, string
285288
if (kustoClient == null)
286289
{
287290
var tokenCredential = await GetCredential(tenant);
288-
kustoClient = new KustoClient(clusterUri, tokenCredential, UserAgent);
291+
kustoClient = new KustoClient(clusterUri, tokenCredential, UserAgent, _httpClientService);
289292
await _cacheService.SetAsync(CacheGroup, providerCacheKey, kustoClient, s_providerCacheDuration);
290293
}
291294

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

areas/kusto/tests/AzureMcp.Kusto.LiveTests/KustoCommandTests.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
using System.Text.Json;
55
using Azure.Identity;
6+
using AzureMcp.Core.Services.Http;
67
using AzureMcp.Kusto.Services;
78
using AzureMcp.Tests;
89
using AzureMcp.Tests.Client;
910
using AzureMcp.Tests.Client.Helpers;
11+
using Microsoft.Extensions.Options;
1012
using ModelContextProtocol.Client;
1113
using Xunit;
14+
using MsOptions = Microsoft.Extensions.Options.Options;
1215

1316
namespace AzureMcp.Kusto.LiveTests;
1417

@@ -38,7 +41,12 @@ public async ValueTask InitializeAsync()
3841
{ "cluster", Settings.ResourceBaseName }
3942
});
4043
var clusterUri = clusterInfo.AssertProperty("cluster").AssertProperty("clusterUri").GetString();
41-
var kustoClient = new KustoClient(clusterUri ?? string.Empty, credentials, "ua");
44+
45+
// Create HttpClientService for KustoClient
46+
var httpClientOptions = new HttpClientOptions();
47+
var httpClientService = new HttpClientService(MsOptions.Create(httpClientOptions));
48+
49+
var kustoClient = new KustoClient(clusterUri ?? string.Empty, credentials, "ua", httpClientService);
4250
var resp = await kustoClient.ExecuteControlCommandAsync(
4351
TestDatabaseName,
4452
".set-or-replace ToDoList <| datatable (Title: string, IsCompleted: bool) [' Hello World!', false]",

areas/monitor/src/AzureMcp.Monitor/Services/MonitorHealthModelService.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
using AzureMcp.Core.Options;
77
using AzureMcp.Core.Services.Azure;
88
using AzureMcp.Core.Services.Azure.Tenant;
9+
using AzureMcp.Core.Services.Http;
910

1011
namespace AzureMcp.Monitor.Services;
1112

12-
public class MonitorHealthModelService(ITenantService tenantService)
13+
public class MonitorHealthModelService(ITenantService tenantService, IHttpClientService httpClientService)
1314
: BaseAzureService(tenantService), IMonitorHealthModelService
1415
{
1516
private const int TokenExpirationBuffer = 300;
1617
private const string ManagementApiBaseUrl = "https://management.azure.com";
1718
private const string HealthModelsDataApiScope = "https://data.healthmodels.azure.com";
1819
private const string ApiVersion = "2023-10-01-preview";
19-
private static readonly HttpClient s_sharedHttpClient = new HttpClient();
20+
private readonly IHttpClientService _httpClientService = httpClientService;
2021

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

63-
HttpResponseMessage healthResponse = await s_sharedHttpClient.SendAsync(request);
64+
HttpResponseMessage healthResponse = await _httpClientService.DefaultClient.SendAsync(request);
6465
healthResponse.EnsureSuccessStatusCode();
6566

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

78-
HttpResponseMessage response = await s_sharedHttpClient.SendAsync(request);
79+
HttpResponseMessage response = await _httpClientService.DefaultClient.SendAsync(request);
7980
response.EnsureSuccessStatusCode();
8081
string responseString = await response.Content.ReadAsStringAsync();
8182

core/src/AzureMcp.Core/Areas/Server/Commands/ServiceCollectionExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using AzureMcp.Core.Areas.Server.Commands.ToolLoading;
88
using AzureMcp.Core.Areas.Server.Options;
99
using AzureMcp.Core.Commands;
10+
using AzureMcp.Core.Extensions;
1011
using AzureMcp.Core.Services.Telemetry;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.Extensions.Logging;
@@ -34,6 +35,9 @@ public static class AzureMcpServiceCollectionExtensions
3435
/// <returns>The service collection with MCP server services added.</returns>
3536
public static IServiceCollection AddAzureMcpServer(this IServiceCollection services, ServiceStartOptions serviceStartOptions)
3637
{
38+
// Register HTTP client services
39+
services.AddHttpClientServices();
40+
3741
// Register options for service start
3842
services.AddSingleton(serviceStartOptions);
3943
services.AddSingleton(Options.Create(serviceStartOptions));
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using AzureMcp.Core.Services.Http;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace AzureMcp.Core.Extensions;
8+
9+
/// <summary>
10+
/// Extension methods for registering HTTP client services.
11+
/// </summary>
12+
public static class HttpClientServiceCollectionExtensions
13+
{
14+
/// <summary>
15+
/// Adds HTTP client services to the service collection with default configuration.
16+
/// </summary>
17+
/// <param name="services">The service collection.</param>
18+
/// <returns>The service collection for chaining.</returns>
19+
public static IServiceCollection AddHttpClientServices(this IServiceCollection services)
20+
{
21+
ArgumentNullException.ThrowIfNull(services);
22+
return services.AddHttpClientServices(_ => { });
23+
}
24+
25+
/// <summary>
26+
/// Adds HTTP client services to the service collection with custom configuration.
27+
/// </summary>
28+
/// <param name="services">The service collection.</param>
29+
/// <param name="configureOptions">Action to configure HttpClient options.</param>
30+
/// <returns>The service collection for chaining.</returns>
31+
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, Action<HttpClientOptions> configureOptions)
32+
{
33+
ArgumentNullException.ThrowIfNull(services);
34+
ArgumentNullException.ThrowIfNull(configureOptions);
35+
36+
// Configure options with environment variables
37+
services.Configure<HttpClientOptions>(options =>
38+
{
39+
// Read proxy configuration from environment variables
40+
options.AllProxy = Environment.GetEnvironmentVariable("ALL_PROXY");
41+
options.HttpProxy = Environment.GetEnvironmentVariable("HTTP_PROXY");
42+
options.HttpsProxy = Environment.GetEnvironmentVariable("HTTPS_PROXY");
43+
options.NoProxy = Environment.GetEnvironmentVariable("NO_PROXY");
44+
45+
// Apply custom configuration
46+
configureOptions(options);
47+
});
48+
49+
// Register the HTTP client service
50+
services.AddSingleton<IHttpClientService, HttpClientService>();
51+
52+
return services;
53+
}
54+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace AzureMcp.Core.Services.Http;
5+
6+
/// <summary>
7+
/// Configuration options for HttpClient services.
8+
/// </summary>
9+
public sealed class HttpClientOptions
10+
{
11+
/// <summary>
12+
/// Gets or sets the HTTP proxy address. Can be set via HTTP_PROXY environment variable.
13+
/// </summary>
14+
public string? HttpProxy { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the HTTPS proxy address. Can be set via HTTPS_PROXY environment variable.
18+
/// </summary>
19+
public string? HttpsProxy { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the proxy address for all protocols. Can be set via ALL_PROXY environment variable.
23+
/// </summary>
24+
public string? AllProxy { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the comma-separated list of hostnames that should bypass the proxy. Can be set via NO_PROXY environment variable.
28+
/// </summary>
29+
public string? NoProxy { get; set; }
30+
31+
/// <summary>
32+
/// Gets or sets the default timeout for HTTP requests. Defaults to 100 seconds.
33+
/// </summary>
34+
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);
35+
36+
/// <summary>
37+
/// Gets or sets the default User-Agent header value.
38+
/// </summary>
39+
public string? DefaultUserAgent { get; set; }
40+
}

0 commit comments

Comments
 (0)