Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ namespace Azure.Mcp.Core.Services.Azure.Authentication;
/// </summary>
public class AzureCloudConfiguration : IAzureCloudConfiguration
{

public enum AzureCloud
{
AzurePublicCloud,
AzureChinaCloud,
AzureUSGovernmentCloud,
}

private const string DefaultAuthorityHost = "https://login.microsoftonline.com";

/// <summary>
Expand All @@ -37,7 +45,7 @@ public AzureCloudConfiguration(
?? configuration["Cloud"]
?? Environment.GetEnvironmentVariable("AZURE_CLOUD");

(AuthorityHost, ArmEnvironment) = ParseCloudValue(cloudValue);
(AuthorityHost, ArmEnvironment, CloudType) = ParseCloudValue(cloudValue);

logger?.LogDebug(
"Azure cloud configuration initialized. Cloud value: '{CloudValue}', AuthorityHost: '{AuthorityHost}', ArmEnvironment: '{ArmEnvironment}'",
Expand All @@ -52,31 +60,33 @@ public AzureCloudConfiguration(
/// <inheritdoc/>
public ArmEnvironment ArmEnvironment { get; }

private static (Uri authorityHost, ArmEnvironment armEnvironment) ParseCloudValue(string? cloudValue)
public AzureCloud CloudType { get; }

private static (Uri authorityHost, ArmEnvironment armEnvironment, AzureCloud cloudType) ParseCloudValue(string? cloudValue)
{
if (string.IsNullOrWhiteSpace(cloudValue))
{
return (new Uri(DefaultAuthorityHost), ArmEnvironment.AzurePublicCloud);
return (new Uri(DefaultAuthorityHost), ArmEnvironment.AzurePublicCloud, AzureCloud.AzurePublicCloud);
}

// Check if it's already a URL - in this case we only have authority host
// and must default to public cloud for ARM (custom cloud scenario requires
// additional configuration not currently supported)
if (cloudValue.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return (new Uri(cloudValue), ArmEnvironment.AzurePublicCloud);
return (new Uri(cloudValue), ArmEnvironment.AzurePublicCloud, AzureCloud.AzurePublicCloud);
}

// Map common sovereign cloud names to authority hosts and ARM environments
return cloudValue.ToLowerInvariant() switch
{
"azurecloud" or "azurepubliccloud" or "public" =>
(new Uri("https://login.microsoftonline.com"), ArmEnvironment.AzurePublicCloud),
(new Uri("https://login.microsoftonline.com"), ArmEnvironment.AzurePublicCloud, AzureCloud.AzurePublicCloud),
"azurechinacloud" or "china" =>
(new Uri("https://login.chinacloudapi.cn"), ArmEnvironment.AzureChina),
(new Uri("https://login.chinacloudapi.cn"), ArmEnvironment.AzureChina, AzureCloud.AzureChinaCloud),
"azureusgovernment" or "azureusgovernmentcloud" or "usgov" or "usgovernment" =>
(new Uri("https://login.microsoftonline.us"), ArmEnvironment.AzureGovernment),
_ => (new Uri(DefaultAuthorityHost), ArmEnvironment.AzurePublicCloud) // Default to public cloud if unknown
(new Uri("https://login.microsoftonline.us"), ArmEnvironment.AzureGovernment, AzureCloud.AzureUSGovernmentCloud),
_ => (new Uri(DefaultAuthorityHost), ArmEnvironment.AzurePublicCloud, AzureCloud.AzurePublicCloud) // Default to public cloud if unknown
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ public interface IAzureCloudConfiguration
/// This determines the management endpoint used for Azure Resource Manager operations.
/// </summary>
ArmEnvironment ArmEnvironment { get; }

/// <summary>
/// Gets the type of Azure cloud environment.
/// </summary>
AzureCloudConfiguration.AzureCloud CloudType { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Added sovereign cloud endpoint support for Storage, Search, Postgres, ServiceFabric, Pricing, and Extension services"
55 changes: 50 additions & 5 deletions tools/Azure.Mcp.Tools.AppLens/src/Services/AppLensService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Channels;
using Azure.Core;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Authentication;
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Azure.Mcp.Tools.AppLens.Models;
Expand All @@ -20,9 +21,9 @@ namespace Azure.Mcp.Tools.AppLens.Services;
public class AppLensService(IHttpClientFactory httpClientFactory, ISubscriptionService subscriptionService, ITenantService tenantService) : BaseAzureService(tenantService), IAppLensService
{
private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService));
private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService));
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
private readonly AppLensOptions _options = new AppLensOptions();
private const string ConversationalDiagnosticsSignalREndpoint = "https://diagnosticschat.azure.com/chatHub";

/// <inheritdoc />
public async Task<DiagnosticResult> DiagnoseResourceAsync(
Expand Down Expand Up @@ -93,12 +94,12 @@ private async Task<GetAppLensSessionResult> GetAppLensSessionAsync(string resour

// Get ARM token
var token = await credential.GetTokenAsync(
new TokenRequestContext(["https://management.azure.com/user_impersonation"]),
new TokenRequestContext([GetManagementImpersonationEndpoint().ToString()]),
cancellationToken);

// Call the AppLens token endpoint
using var request = new HttpRequestMessage(HttpMethod.Get,
$"https://management.azure.com/{resourceId}/detectors/GetToken-db48586f-7d94-45fc-88ad-b30ccd3b571c?api-version=2015-08-01");
GetAppLensTokenEndpoint(resourceId));

request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);

Expand Down Expand Up @@ -156,10 +157,10 @@ public async IAsyncEnumerable<ChatMessageResponseBody> AskAppLensAsync(
// https://learn.microsoft.com/aspnet/core/signalr/configuration?view=aspnetcore-9.0&tabs=dotnet#jsonmessagepack-serialization-options
options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppLensJsonContext.Default);
})
.WithUrl(ConversationalDiagnosticsSignalREndpoint, options =>
.WithUrl(GetConversationalDiagnosticsSignalREndpoint(), options =>
{
options.AccessTokenProvider = () => Task.FromResult(session.Token)!;
options.Headers.Add("origin", "https://appservice-diagnostics.trafficmanager.net");
options.Headers.Add("origin", GetDiagnosticsPortalEndpoint().ToString());
})
.WithAutomaticReconnect()
.Build();
Expand Down Expand Up @@ -345,4 +346,48 @@ private static AppLensSession ParseGetTokenResponse(string rawResponse)

return session;
}

private Uri GetConversationalDiagnosticsSignalREndpoint()
{
return _tenantService.CloudConfiguration.CloudType switch
{
AzureCloudConfiguration.AzureCloud.AzurePublicCloud => new Uri("https://diagnosticschat.azure.com/chatHub"),
AzureCloudConfiguration.AzureCloud.AzureChinaCloud => new Uri("https://diagnosticschat.azure.cn/chatHub"),
AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud => new Uri("https://diagnosticschat.azure.us/chatHub"),
_ => new Uri("https://diagnosticschat.azure.com/chatHub"),
};
}

private Uri GetManagementImpersonationEndpoint()
{
return _tenantService.CloudConfiguration.CloudType switch
{
AzureCloudConfiguration.AzureCloud.AzurePublicCloud => new Uri("https://management.azure.com/user_impersonation"),
AzureCloudConfiguration.AzureCloud.AzureChinaCloud => new Uri("https://management.chinacloudapi.cn/user_impersonation"),
AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud => new Uri("https://management.usgovcloudapi.net/user_impersonation"),
_ => new Uri("https://management.azure.com/user_impersonation"),
};
}

private Uri GetAppLensTokenEndpoint(string resourceId)
{
return _tenantService.CloudConfiguration.CloudType switch
{
AzureCloudConfiguration.AzureCloud.AzurePublicCloud => new Uri($"https://management.azure.com/{resourceId}/detectors/GetToken-db48586f-7d94-45fc-88ad-b30ccd3b571c?api-version=2015-08-01"),
AzureCloudConfiguration.AzureCloud.AzureChinaCloud => new Uri($"https://management.chinacloudapi.cn/{resourceId}/detectors/GetToken-db48586f-7d94-45fc-88ad-b30ccd3b571c?api-version=2015-08-01"),
AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud => new Uri($"https://management.usgovcloudapi.net/{resourceId}/detectors/GetToken-db48586f-7d94-45fc-88ad-b30ccd3b571c?api-version=2015-08-01"),
_ => new Uri($"https://management.azure.com/{resourceId}/detectors/GetToken-db48586f-7d94-45fc-88ad-b30ccd3b571c?api-version=2015-08-01"),
};
}

private Uri GetDiagnosticsPortalEndpoint()
{
return _tenantService.CloudConfiguration.CloudType switch
{
AzureCloudConfiguration.AzureCloud.AzurePublicCloud => new Uri("https://appservice-diagnostics.trafficmanager.net"),
AzureCloudConfiguration.AzureCloud.AzureChinaCloud => new Uri("https://appservice-diagnostics.azure.cn"),
AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud => new Uri("https://appservice-diagnostics.azure.us"),
_ => new Uri("https://appservice-diagnostics.trafficmanager.net"),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Azure.Mcp.Core.Options;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Authentication;
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Azure.Mcp.Tools.AppService.Models;
Expand All @@ -17,6 +18,7 @@ public class AppServiceService(
ITenantService tenantService,
ILogger<AppServiceService> logger) : BaseAzureService(tenantService), IAppServiceService
{
private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService));
private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService));
private readonly ILogger<AppServiceService> _logger = logger;

Expand Down Expand Up @@ -95,7 +97,7 @@ private async Task<WebSiteResource> GetWebAppResourceAsync(string subscription,
return webAppResource.Value;
}

private static string PrepareConnectionString(string? connectionString, string databaseType,
private string PrepareConnectionString(string? connectionString, string databaseType,
string databaseServer, string databaseName)
{
return string.IsNullOrWhiteSpace(connectionString)
Expand Down Expand Up @@ -177,15 +179,30 @@ private static ConnectionStringType GetConnectionStringType(string databaseType)
};
}

private static string BuildConnectionString(string databaseType, string databaseServer, string databaseName)
private string BuildConnectionString(string databaseType, string databaseServer, string databaseName)
{
return databaseType.ToLowerInvariant() switch
{
"sqlserver" => $"Server={databaseServer};Database={databaseName};User Id={{username}};Password={{password}};TrustServerCertificate=True;",
"mysql" => $"Server={databaseServer};Database={databaseName};Uid={{username}};Pwd={{password}};",
"postgresql" => $"Host={databaseServer};Database={databaseName};Username={{username}};Password={{password}};",
"cosmosdb" => $"AccountEndpoint=https://{databaseServer}.documents.azure.com:443/;AccountKey={{key}};Database={databaseName};",
"cosmosdb" => BuildCosmosConnectionString(databaseServer, databaseName),
_ => throw new ArgumentException($"Unsupported database type: {databaseType}")
};
}

private string BuildCosmosConnectionString(string databaseServer, string databaseName)
{
switch (_tenantService.CloudConfiguration.CloudType)
{
case AzureCloudConfiguration.AzureCloud.AzurePublicCloud:
return $"AccountEndpoint=https://{databaseServer}.documents.azure.com:443/;AccountKey={{key}};Database={databaseName};";
case AzureCloudConfiguration.AzureCloud.AzureChinaCloud:
return $"AccountEndpoint=https://{databaseServer}.documents.azure.cn:443/;AccountKey={{key}};Database={databaseName};";
case AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud:
return $"AccountEndpoint=https://{databaseServer}.documents.azure.us:443/;AccountKey={{key}};Database={databaseName};";
default:
throw new ArgumentException($"Unsupported Azure cloud type: {_tenantService.CloudConfiguration.CloudType}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Azure.Core;
using Azure.Mcp.Core.Options;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Authentication;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Azure.Mcp.Tools.ApplicationInsights.Commands;
using Azure.Mcp.Tools.ApplicationInsights.Models;
Expand All @@ -27,9 +28,7 @@ public class ProfilerDataService(
ITenantService tenantService)
: BaseAzureService(tenantService), IProfilerDataService
{
private const string Endpoint = "https://dataplane.diagnosticservices.azure.com/";
private const string DefaultScope = "api://dataplane.diagnosticservices.azure.com/.default";

private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService));
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));

private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
Expand Down Expand Up @@ -98,7 +97,7 @@ await response.Content.ReadAsStreamAsync(cancellationToken),

private async Task<HttpRequestMessage> CreateRequestAsync(HttpMethod method, string path, IDictionary<string, string>? queries, string apiVersion, string? clientRequestId, HttpContent? httpContent, IDictionary<string, IEnumerable<string>>? additionalHeaders, CancellationToken cancellationToken)
{
UriBuilder uriBuilder = new(Endpoint)
UriBuilder uriBuilder = new(GetDiagnosticServiceEndpoint())
{
Path = path
};
Expand All @@ -119,7 +118,7 @@ private async Task<HttpRequestMessage> CreateRequestAsync(HttpMethod method, str

var scopes = new string[]
{
DefaultScope
GetDiagnosticServicesScope()
};
string clientRequestIdLocal = clientRequestId ?? Guid.NewGuid().ToString();
TokenRequestContext tokenRequestContext = new(scopes, clientRequestIdLocal);
Expand Down Expand Up @@ -199,4 +198,34 @@ private async Task<Guid> ResolveAppIdAsync(ResourceIdentifier resourceId, Cancel
_logger.LogInformation("Resolving appId: {resourceId} => {appId}", resourceId, appId);
return Guid.Parse(appId);
}

private Uri GetDiagnosticServiceEndpoint()
{
switch (_tenantService.CloudConfiguration.CloudType)
{
case AzureCloudConfiguration.AzureCloud.AzurePublicCloud:
return new Uri("https://dataplane.diagnosticservices.azure.com");
case AzureCloudConfiguration.AzureCloud.AzureChinaCloud:
return new Uri("https://dataplane.diagnosticservices.azure.cn");
case AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud:
return new Uri("https://dataplane.diagnosticservices.azure.us");
default:
return new Uri("https://dataplane.diagnosticservices.azure.com");
}
}

private string GetDiagnosticServicesScope()
{
switch (_tenantService.CloudConfiguration.CloudType)
{
case AzureCloudConfiguration.AzureCloud.AzurePublicCloud:
return "api://dataplane.diagnosticservices.azure.com/.default";
case AzureCloudConfiguration.AzureCloud.AzureChinaCloud:
return "api://dataplane.diagnosticservices.azure.cn/.default";
case AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud:
return "api://dataplane.diagnosticservices.azure.us/.default";
default:
return "api://dataplane.diagnosticservices.azure.com/.default";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text.Json;
using Azure.Core;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Authentication;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Azure.Mcp.Tools.ConfidentialLedger.Models;
using Azure.Security.ConfidentialLedger;
Expand All @@ -15,7 +16,7 @@ public class ConfidentialLedgerService(ITenantService tenantService)
: BaseAzureService(tenantService), IConfidentialLedgerService
{
// NOTE: We construct the data-plane endpoint from the ledger name.
private static Uri BuildLedgerUri(string ledgerName) => new($"https://{ledgerName}.confidential-ledger.azure.com");
private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService));

private static RequestContent CreateAppendEntryContent(string entryData)
{
Expand Down Expand Up @@ -43,7 +44,7 @@ public async Task<AppendEntryResult> AppendEntryAsync(string ledgerName, string
var credential = await GetCredential(cancellationToken);

// Configure client (retry etc. could be extended later)
ConfidentialLedgerClient client = new(BuildLedgerUri(ledgerName), credential);
ConfidentialLedgerClient client = new(GetLedgerUri(ledgerName), credential);

// Build RequestContent manually to avoid trimming issues from reflection-based serialization.
using var content = CreateAppendEntryContent(entryData);
Expand Down Expand Up @@ -74,7 +75,7 @@ public async Task<LedgerEntryGetResult> GetLedgerEntryAsync(string ledgerName, s
}

var credential = await GetCredential(cancellationToken);
ConfidentialLedgerClient client = new(BuildLedgerUri(ledgerName), credential);
ConfidentialLedgerClient client = new(GetLedgerUri(ledgerName), credential);

Response? getByCollectionResponse = null;
bool loaded = false;
Expand Down Expand Up @@ -115,4 +116,19 @@ public async Task<LedgerEntryGetResult> GetLedgerEntryAsync(string ledgerName, s
Contents = contents ?? string.Empty,
};
}

private Uri GetLedgerUri(string ledgerName)
{
switch (_tenantService.CloudConfiguration.CloudType)
{
case AzureCloudConfiguration.AzureCloud.AzurePublicCloud:
return new Uri($"https://{ledgerName}.confidential-ledger.azure.com");
case AzureCloudConfiguration.AzureCloud.AzureChinaCloud:
return new Uri($"https://{ledgerName}.confidential-ledger.azure.cn");
case AzureCloudConfiguration.AzureCloud.AzureUSGovernmentCloud:
return new Uri($"https://{ledgerName}.confidential-ledger.azure.us");
default:
return new Uri($"https://{ledgerName}.confidential-ledger.azure.com");
}
}
}
Loading