Skip to content

Commit 407bec4

Browse files
committed
Add remote HTTP server mode with OAuth authentication and downstream token acquisition
- Add RunAsRemoteHttpService and OutgoingAuthStrategy options to enable MCP server to run as a remote HTTP service - Introduce ITokenProvider abstraction for dependency injection of downstream authentication tokens and credentials - Add support for On-Behalf-Of (OBO) token flow and hosting environment identity modes for outgoing authentication - Implement OAuth Protected Resource Metadata endpoint at /.well-known/oauth-protected-resource with WWW-Authenticate challenge - Add Visual Studio launch profile for debugging remote MCP server with Microsoft.Identity.Web configuration - Modernize HTTP host creation using WebApplicationBuilder with authentication and authorization middleware
1 parent 539a5a9 commit 407bec4

File tree

31 files changed

+874
-102
lines changed

31 files changed

+874
-102
lines changed

Directory.Packages.props

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
6969
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
7070
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
71+
<PackageVersion Include="Microsoft.Identity.Abstractions" Version="9.5.0" />
72+
<PackageVersion Include="Microsoft.Identity.Web" Version="3.14.0" />
73+
<PackageVersion Include="Microsoft.Identity.Web.Azure" Version="3.14.0" />
7174
<PackageVersion Include="Microsoft.HybridRow" Version="1.1.0-preview3" />
7275
<PackageVersion Include="Microsoft.Azure.Cosmos.Aot" Version="0.1.4-preview.2" />
7376
<PackageVersion Include="Microsoft.Azure.Mcp.AzTypes.Internal.Compact" Version="0.2.802" />
@@ -78,7 +81,7 @@
7881
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
7982
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
8083
<PackageVersion Include="System.Formats.Asn1" Version="9.0.9" />
81-
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.11.0" />
84+
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
8285
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0-rc.1.25451.107" />
8386
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0-rc.1.25451.107" />
8487
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.0" />

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,7 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
6464

6565
// Register tool loader strategies
6666
services.AddSingleton<CommandFactoryToolLoader>();
67-
services.AddSingleton(sp =>
68-
{
69-
return new RegistryToolLoader(
70-
sp.GetRequiredService<RegistryDiscoveryStrategy>(),
71-
sp.GetRequiredService<IOptions<ToolLoaderOptions>>(),
72-
sp.GetRequiredService<ILogger<RegistryToolLoader>>()
73-
);
74-
});
67+
services.AddSingleton<RegistryToolLoader>();
7568

7669
services.AddSingleton<SingleProxyToolLoader>();
7770
services.AddSingleton<CompositeToolLoader>();
@@ -216,7 +209,7 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
216209

217210
var mcpServerBuilder = services.AddMcpServer();
218211

219-
if (serviceStartOptions.EnableInsecureTransports)
212+
if (serviceStartOptions.EnableInsecureTransports || serviceStartOptions.RunAsRemoteHttpService)
220213
{
221214
mcpServerBuilder.WithHttpTransport();
222215
}

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

Lines changed: 203 additions & 34 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Core.Areas.Server.Models;
7+
8+
/// <summary>
9+
/// OAuth 2.0 protected resource metadata response model. See https://datatracker.ietf.org/doc/rfc9728/.
10+
/// </summary>
11+
public sealed class OAuthProtectedResourceMetadata
12+
{
13+
[JsonPropertyName("resource")]
14+
public required string Resource { get; init; }
15+
16+
[JsonPropertyName("authorization_servers")]
17+
public required string[] AuthorizationServers { get; init; }
18+
19+
[JsonPropertyName("scopes_supported")]
20+
public required string[] ScopesSupported { get; init; }
21+
22+
[JsonPropertyName("bearer_methods_supported")]
23+
public required string[] BearerMethodsSupported { get; init; }
24+
25+
[JsonPropertyName("resource_documentation")]
26+
public required string ResourceDocumentation { get; init; }
27+
28+
[JsonPropertyName("resource_signing_alg_values_supported")]
29+
public required string[] ResourceSigningAlgValuesSupported { get; init; }
30+
}
31+
32+
/// <summary>
33+
/// JSON serializer context for AOT-safe serialization.
34+
/// </summary>
35+
[JsonSerializable(typeof(OAuthProtectedResourceMetadata))]
36+
internal partial class OAuthMetadataJsonContext : JsonSerializerContext
37+
{
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Azure.Identity;
2+
3+
namespace Azure.Mcp.Core.Areas.Server.Options;
4+
5+
public enum OutgoingAuthenticationTypes
6+
{
7+
/// <summary>
8+
/// The value is not set and is in a default state. A safe default will
9+
/// be chosen based on other settings.
10+
/// </summary>
11+
NotSet = 0,
12+
13+
/// <summary>
14+
/// Outgoing requests will use the hosting environment's identity resolving
15+
/// in the same way as <see cref="DefaultAzureCredential"/>. This is valid
16+
/// for all hosting scenarios. This means all outgoing requests will use the
17+
/// same identity regardless of the incoming authenticate request identity,
18+
/// if any.
19+
/// </summary>
20+
UseHostingEnvironmentIdentity = 1,
21+
22+
/// <summary>
23+
/// Outgoing requests will be authenticated based on exchanging the incoming
24+
/// request's access token for a new access token valid for the downstream
25+
/// service. This is only valid for remote MCP server scenarios.
26+
/// </summary>
27+
UseOnBehalfOf = 2
28+
}

core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public static class ServiceOptionDefinitions
1313
public const string DebugName = "debug";
1414
public const string EnableInsecureTransportsName = "enable-insecure-transports";
1515
public const string InsecureDisableElicitationName = "insecure-disable-elicitation";
16+
public const string RunAsRemoteHttpServiceName = "run-as-remote-http-service";
17+
public const string OutgoingAuthStrategyName = "outgoing-auth-strategy";
1618

1719
public static readonly Option<string> Transport = new($"--{TransportName}")
1820
{
@@ -83,4 +85,20 @@ public static class ServiceOptionDefinitions
8385
Description = "Disable elicitation (user confirmation) before allowing high risk commands to run, such as returning Secrets (passwords) from KeyVault.",
8486
DefaultValueFactory = _ => false
8587
};
88+
89+
public static readonly Option<bool> RunAsRemoteHttpService = new(
90+
$"--{RunAsRemoteHttpServiceName}")
91+
{
92+
Required = false,
93+
Description = "Run the server as a remote HTTP service requiring authentication and authorization.",
94+
DefaultValueFactory = _ => false
95+
};
96+
97+
public static readonly Option<OutgoingAuthenticationTypes> OutgoingAuthStrategy = new(
98+
$"--{OutgoingAuthStrategyName}")
99+
{
100+
Required = false,
101+
Description = "Outgoing authentication strategy for Azure service requests. Valid values: NotSet, UseHostingEnvironmentIdentity, UseOnBehalfOf.",
102+
DefaultValueFactory = _ => OutgoingAuthenticationTypes.NotSet
103+
};
86104
}

core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,18 @@ public class ServiceStartOptions
6464
/// </summary>
6565
[JsonPropertyName("insecureDisableElicitation")]
6666
public bool InsecureDisableElicitation { get; set; } = false;
67+
68+
/// <summary>
69+
/// Gets or sets whether the server should run as a remote HTTP service.
70+
/// When true, the server will require authentication and authorization.
71+
/// </summary>
72+
[JsonPropertyName("runAsRemoteHttpService")]
73+
public bool RunAsRemoteHttpService { get; set; } = false;
74+
75+
/// <summary>
76+
/// Gets or sets the outgoing authentication strategy for Azure service requests.
77+
/// Determines whether to use hosting environment identity or on-behalf-of flow.
78+
/// </summary>
79+
[JsonPropertyName("outgoingAuthStrategy")]
80+
public OutgoingAuthenticationTypes OutgoingAuthStrategy { get; set; } = OutgoingAuthenticationTypes.NotSet;
6781
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<PackageReference Include="Azure.ResourceManager" />
2222
<PackageReference Include="Azure.ResourceManager.ResourceGraph" />
2323
<PackageReference Include="Microsoft.Extensions.Azure" />
24+
<PackageReference Include="Microsoft.Identity.Abstractions" />
25+
<PackageReference Include="Microsoft.Identity.Web" />
26+
<PackageReference Include="Microsoft.Identity.Web.Azure" />
2427
<PackageReference Include="ModelContextProtocol.AspNetCore" />
2528
<PackageReference Include="System.Linq.AsyncEnumerable" />
2629
</ItemGroup>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Mcp.Core.Areas.Server.Options;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Identity.Web;
9+
10+
namespace Azure.Mcp.Core.Services.Azure.Authentication;
11+
12+
/// <summary>
13+
/// Extension methods for configuring Azure authentication services.
14+
/// </summary>
15+
public static class AuthenticationServiceCollectionExtensions
16+
{
17+
/// <summary>
18+
/// Adds <see cref="IAzureTokenCredentialProvider"/> with lifetime
19+
/// <see cref="ServiceLifetime.Singleton"/>to the service collection based on the specified
20+
/// <see cref="ServiceStartOptions"/>.
21+
/// </summary>
22+
/// <param name="services">The service collection.</param>
23+
/// <param name="serverOptions">The server options containing the authentication strategy.</param>
24+
/// <param name="configuration">
25+
/// Configuration required for on-behalf-of authentication. May be <see langword="null"/>
26+
/// otherwise.
27+
/// </param>
28+
/// <returns>The service collection.</returns>
29+
/// <exception cref="InvalidOperationException">
30+
/// Thrown when the authentication strategy is unsupported or when configuration is required but not provided.
31+
/// </exception>
32+
/// <remarks>
33+
/// This method registers the appropriate <see cref="IAzureTokenCredentialProvider"/> implementation
34+
/// based on the <see cref="ServiceStartOptions.OutgoingAuthStrategy"/>:
35+
/// <list type="bullet">
36+
/// <item><description><see cref="OutgoingAuthenticationTypes.UseHostingEnvironmentIdentity"/> - registers <see cref="SingleIdentityTokenCredentialProvider"/></description></item>
37+
/// <item><description><see cref="OutgoingAuthenticationTypes.UseOnBehalfOf"/> - registers <see cref="HttpOnBehalfOfTokenCredentialProvider"/> (requires configuration)</description></item>
38+
/// </list>
39+
/// </remarks>
40+
public static IServiceCollection AddAzureTokenCredentialProvider(
41+
this IServiceCollection services,
42+
ServiceStartOptions serverOptions,
43+
IConfiguration? configuration = null)
44+
{
45+
46+
// ASSUMPTION: validation has already occurred and defaults have been applied in ServiceStartCommand.BindOptions
47+
if (serverOptions.OutgoingAuthStrategy == OutgoingAuthenticationTypes.UseHostingEnvironmentIdentity)
48+
{
49+
services.AddSingleIdentityTokenCredentialProvider();
50+
}
51+
else if (serverOptions.OutgoingAuthStrategy == OutgoingAuthenticationTypes.UseOnBehalfOf)
52+
{
53+
if (configuration == null)
54+
{
55+
throw new InvalidOperationException("Configuration is required for On-Behalf-Of authentication strategy.");
56+
}
57+
services.AddHttpOnBehalfOfTokenCredentialProvider(configuration);
58+
}
59+
else
60+
{
61+
throw new InvalidOperationException($"Unsupported outgoing authentication strategy '{serverOptions.OutgoingAuthStrategy}'.");
62+
}
63+
64+
return services;
65+
}
66+
67+
/// <summary>
68+
/// Adds <see cref="SingleIdentityTokenCredentialProvider"/> as a
69+
/// <see cref="IAzureTokenCredentialProvider"/> with lifetime <see cref="ServiceLifetime.Singleton"/>
70+
/// into the service collection.
71+
/// </summary>
72+
/// <param name="services">The service collection.</param>
73+
/// <returns>The service collection.</returns>
74+
/// <remarks>
75+
/// <para>
76+
/// This method registers the single identity token credential provider which uses the hosting
77+
/// environment's identity (e.g., a Managed Identity or a user principal using Azure CLI, Visual
78+
/// Studio, etc.).
79+
/// </para>
80+
/// This method will not override any existing <see cref="IAzureTokenCredentialProvider"/>
81+
/// registration. It can be overridden as needed by command line arguments and configurations
82+
/// with <see cref="AddAzureTokenCredentialProvider"/>.
83+
/// </remarks>
84+
public static IServiceCollection AddSingleIdentityTokenCredentialProvider(this IServiceCollection services)
85+
{
86+
services.TryAddSingleton<IAzureTokenCredentialProvider, SingleIdentityTokenCredentialProvider>();
87+
return services;
88+
}
89+
90+
/// <summary>
91+
/// Adds <see cref="HttpOnBehalfOfTokenCredentialProvider"/> as a
92+
/// <see cref="IAzureTokenCredentialProvider"/> with lifetime <see cref="ServiceLifetime.Singleton"/>
93+
/// into the service collection, along with all required dependencies.
94+
/// </summary>
95+
/// <param name="services">The service collection.</param>
96+
/// <param name="configuration">The application configuration containing Azure AD settings.</param>
97+
/// <returns>The service collection.</returns>
98+
/// <remarks>
99+
/// This method will override any existing <see cref="IAzureTokenCredentialProvider"/> registration.
100+
/// This is unlike <see cref="AddSingleIdentityTokenCredentialProvider"/>.
101+
/// </remarks>
102+
private static IServiceCollection AddHttpOnBehalfOfTokenCredentialProvider(
103+
this IServiceCollection services,
104+
IConfiguration configuration)
105+
{
106+
// Dependencies - directly in constructor.
107+
services.AddHttpContextAccessor();
108+
109+
// Dependencies - indirectly required to get MicrosoftIdentityTokenCredential.
110+
services.AddMicrosoftIdentityWebAppAuthentication(configuration)
111+
.EnableTokenAcquisitionToCallDownstreamApi()
112+
.AddInMemoryTokenCaches();
113+
services.AddMicrosoftIdentityAzureTokenCredential();
114+
115+
// Register the OBO token provider. This uses AddSingleton (not TryAdd) to override
116+
// any default registration, since OBO is an explicit configuration choice.
117+
services.AddSingleton<IAzureTokenCredentialProvider, HttpOnBehalfOfTokenCredentialProvider>();
118+
return services;
119+
}
120+
}

core/Azure.Mcp.Core/src/Services/Azure/Authentication/CustomChainedCredential.cs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,47 @@ namespace Azure.Mcp.Core.Services.Azure.Authentication;
1515
/// InteractiveBrowserCredential to provide a seamless authentication experience.
1616
/// </summary>
1717
/// <remarks>
18+
/// <para>
19+
/// DO NOT INSTANTIATE THIS CLASS DIRECTLY. Use dependency injection to get an instance of
20+
/// <see cref="TokenCredential"/> from <see cref="IAzureTokenCredentialProvider"/>.
21+
/// </para>
22+
/// <para>
1823
/// The credential chain behavior can be controlled via the AZURE_TOKEN_CREDENTIALS environment variable:
19-
/// - "dev": Visual Studio → Visual Studio Code → Azure CLI → Azure PowerShell → Azure Developer CLI
20-
/// - "prod": Environment → Workload Identity → Managed Identity
21-
/// - Specific credential name (e.g., "AzureCliCredential"): Only that credential
22-
/// - Not set or empty: Development chain (Environment → Visual Studio → Visual Studio Code → Azure CLI → Azure PowerShell → Azure Developer CLI)
23-
///
24+
/// </para>
25+
/// <list type="table">
26+
/// <listheader>
27+
/// <term>Value</term>
28+
/// <description>Behavior</description>
29+
/// </listheader>
30+
/// <item>
31+
/// <term>"dev"</term>
32+
/// <description>Visual Studio → Visual Studio Code → Azure CLI → Azure PowerShell → Azure Developer CLI</description>
33+
/// </item>
34+
/// <item>
35+
/// <term>"prod"</term>
36+
/// <description>Environment → Workload Identity → Managed Identity</description>
37+
/// </item>
38+
/// <item>
39+
/// <term>Specific credential name</term>
40+
/// <description>Only that credential (e.g., "AzureCliCredential")</description>
41+
/// </item>
42+
/// <item>
43+
/// <term>Not set or empty</term>
44+
/// <description>Development chain (Environment → Visual Studio → Visual Studio Code → Azure CLI → Azure PowerShell → Azure Developer CLI)</description>
45+
/// </item>
46+
/// </list>
47+
/// <para>
2448
/// By default, production credentials (Workload Identity and Managed Identity) are excluded unless explicitly requested via AZURE_TOKEN_CREDENTIALS="prod".
25-
///
49+
/// </para>
50+
/// <para>
2651
/// Special behavior: When running in VS Code context (VSCODE_PID environment variable is set) and AZURE_TOKEN_CREDENTIALS is not explicitly specified,
2752
/// Visual Studio Code credential is automatically prioritized first in the chain.
28-
///
53+
/// </para>
54+
/// <para>
2955
/// After the credential chain, Interactive Browser Authentication with Identity Broker is always added as the final fallback.
56+
/// </para>
3057
/// </remarks>
31-
public class CustomChainedCredential(string? tenantId = null, ILogger<CustomChainedCredential>? logger = null) : TokenCredential
58+
internal class CustomChainedCredential(string? tenantId = null, ILogger<CustomChainedCredential>? logger = null) : TokenCredential
3259
{
3360
private TokenCredential? _credential;
3461
private readonly ILogger<CustomChainedCredential>? _logger = logger;

0 commit comments

Comments
 (0)