Skip to content

Commit 8b36076

Browse files
committed
Use McpAuthenticationEvents to customize ProtectedResourceMetadata per-request
1 parent 21f0efa commit 8b36076

7 files changed

Lines changed: 100 additions & 121 deletions

File tree

samples/ProtectedMCPServer/Program.cs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,13 @@
5656
})
5757
.AddMcp(options =>
5858
{
59-
options.ProtectedResourceMetadataProvider = context =>
59+
options.ResourceMetadata = new()
6060
{
61-
var metadata = new ProtectedResourceMetadata
62-
{
63-
Resource = new Uri(serverUrl),
64-
BearerMethodsSupported = { "header" },
65-
ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
66-
AuthorizationServers = { new Uri(inMemoryOAuthServerUrl) }
67-
};
68-
69-
metadata.ScopesSupported.AddRange([
70-
"mcp:tools"
71-
]);
72-
73-
return metadata;
61+
Resource = new Uri(serverUrl),
62+
BearerMethodsSupported = { "header" },
63+
ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
64+
AuthorizationServers = { new Uri(inMemoryOAuthServerUrl) },
65+
ScopesSupported = ["mcp:tools"],
7466
};
7567
});
7668

src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationEvents.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@
55
/// </summary>
66
public class McpAuthenticationEvents
77
{
8+
/// <summary>
9+
/// Gets or sets the function that is invoked when resource metadata is requested.
10+
/// </summary>
11+
/// <remarks>
12+
/// This function is called when a resource metadata request is made to the protected resource metadata endpoint.
13+
/// The implementer should set the <see cref="ResourceMetadataRequestContext.ResourceMetadata"/> property
14+
/// to provide the appropriate metadata for the current request.
15+
/// </remarks>
16+
public Func<ResourceMetadataRequestContext, Task> OnResourceMetadataRequest { get; set; } = context => Task.CompletedTask;
817
}

src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationHandler.cs

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.Extensions.Options;
55
using ModelContextProtocol.Authentication;
66
using System.Text.Encodings.Web;
7-
using System.Text.Json;
87

98
namespace ModelContextProtocol.AspNetCore.Authentication;
109

@@ -34,11 +33,11 @@ public async Task<bool> HandleRequestAsync()
3433
// Check if the request is for the resource metadata endpoint
3534
string requestPath = Request.Path.Value ?? string.Empty;
3635

37-
string expectedMetadataPath = this.Options.ResourceMetadataUri?.ToString() ?? string.Empty;
38-
if (this.Options.ResourceMetadataUri != null && !this.Options.ResourceMetadataUri.IsAbsoluteUri)
36+
string expectedMetadataPath = Options.ResourceMetadataUri?.ToString() ?? string.Empty;
37+
if (Options.ResourceMetadataUri != null && !Options.ResourceMetadataUri.IsAbsoluteUri)
3938
{
4039
// For relative URIs, it's just the path component.
41-
expectedMetadataPath = this.Options.ResourceMetadataUri.OriginalString;
40+
expectedMetadataPath = Options.ResourceMetadataUri.OriginalString;
4241
}
4342

4443
// If the path doesn't match, let the request continue through the pipeline
@@ -62,8 +61,7 @@ public async Task<bool> HandleRequestAsync()
6261
/// </summary>
6362
private string GetAbsoluteResourceMetadataUri()
6463
{
65-
var options = this.Options;
66-
var resourceMetadataUri = options.ResourceMetadataUri;
64+
var resourceMetadataUri = Options.ResourceMetadataUri;
6765

6866
string currentPath = resourceMetadataUri?.ToString() ?? string.Empty;
6967

@@ -88,47 +86,33 @@ private string GetAbsoluteResourceMetadataUri()
8886
/// Handles the resource metadata request.
8987
/// </summary>
9088
/// <param name="cancellationToken">A token to cancel the operation.</param>
91-
private Task HandleResourceMetadataRequestAsync(CancellationToken cancellationToken = default)
89+
private async Task HandleResourceMetadataRequestAsync(CancellationToken cancellationToken = default)
9290
{
93-
var options = this.Options;
94-
var resourceMetadata = options.GetResourceMetadata(Request.HttpContext);
91+
var resourceMetadata = Options.ResourceMetadata;
9592

96-
// Create a copy to avoid modifying the original
97-
var metadata = new ProtectedResourceMetadata
93+
if (Options.Events.OnResourceMetadataRequest is not null)
9894
{
99-
Resource = resourceMetadata.Resource ?? new Uri(GetBaseUrl()),
100-
AuthorizationServers = [.. resourceMetadata.AuthorizationServers],
101-
BearerMethodsSupported = [.. resourceMetadata.BearerMethodsSupported],
102-
ScopesSupported = [.. resourceMetadata.ScopesSupported],
103-
ResourceDocumentation = resourceMetadata.ResourceDocumentation
104-
};
105-
106-
Response.StatusCode = StatusCodes.Status200OK;
107-
Response.ContentType = "application/json";
95+
var context = new ResourceMetadataRequestContext(Request.HttpContext, Scheme, Options)
96+
{
97+
ResourceMetadata = CloneResourceMetadata(resourceMetadata),
98+
};
10899

109-
var json = JsonSerializer.Serialize(
110-
metadata,
111-
McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ProtectedResourceMetadata)));
100+
await Options.Events.OnResourceMetadataRequest(context);
101+
}
112102

113-
return Response.WriteAsync(json, cancellationToken);
114-
}
115103

116-
/// <inheritdoc />
117-
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
118-
{
119-
// If ForwardAuthenticate is set, forward the authentication to the specified scheme
120-
if (!string.IsNullOrEmpty(Options.ForwardAuthenticate) &&
121-
Options.ForwardAuthenticate != Scheme.Name)
104+
if (resourceMetadata == null)
122105
{
123-
// Simply forward the authentication request to the specified scheme and return its result
124-
// This ensures we don't interfere with the authentication process
125-
return await Context.AuthenticateAsync(Options.ForwardAuthenticate);
106+
throw new InvalidOperationException("ResourceMetadata has not been configured. Please set McpAuthenticationOptions.ResourceMetadata.");
126107
}
127108

128-
// If no forwarding is configured, this handler doesn't perform authentication
129-
return AuthenticateResult.NoResult();
109+
await Results.Json(resourceMetadata, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ProtectedResourceMetadata))).ExecuteAsync(Context);
130110
}
131111

112+
/// <inheritdoc />
113+
// If no forwarding is configured, this handler doesn't perform authentication
114+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() => AuthenticateResult.NoResult();
115+
132116
/// <inheritdoc />
133117
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
134118
{
@@ -146,4 +130,31 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
146130

147131
return base.HandleChallengeAsync(properties);
148132
}
133+
134+
internal ProtectedResourceMetadata? CloneResourceMetadata(ProtectedResourceMetadata? resourceMetadata)
135+
{
136+
if (resourceMetadata is null)
137+
{
138+
return null;
139+
}
140+
141+
return new ProtectedResourceMetadata
142+
{
143+
Resource = resourceMetadata.Resource,
144+
AuthorizationServers = [.. resourceMetadata.AuthorizationServers],
145+
BearerMethodsSupported = [.. resourceMetadata.BearerMethodsSupported],
146+
ScopesSupported = [.. resourceMetadata.ScopesSupported],
147+
JwksUri = resourceMetadata.JwksUri,
148+
ResourceSigningAlgValuesSupported = resourceMetadata.ResourceSigningAlgValuesSupported is not null ? [.. resourceMetadata.ResourceSigningAlgValuesSupported] : null,
149+
ResourceName = resourceMetadata.ResourceName,
150+
ResourceDocumentation = resourceMetadata.ResourceDocumentation,
151+
ResourcePolicyUri = resourceMetadata.ResourcePolicyUri,
152+
ResourceTosUri = resourceMetadata.ResourceTosUri,
153+
TlsClientCertificateBoundAccessTokens = resourceMetadata.TlsClientCertificateBoundAccessTokens,
154+
AuthorizationDetailsTypesSupported = resourceMetadata.AuthorizationDetailsTypesSupported is not null ? [.. resourceMetadata.AuthorizationDetailsTypesSupported] : null,
155+
DpopSigningAlgValuesSupported = resourceMetadata.DpopSigningAlgValuesSupported is not null ? [.. resourceMetadata.DpopSigningAlgValuesSupported] : null,
156+
DpopBoundAccessTokensRequired = resourceMetadata.DpopBoundAccessTokensRequired
157+
};
158+
}
159+
149160
}
Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Microsoft.AspNetCore.Authentication;
2-
using Microsoft.AspNetCore.Http;
32
using ModelContextProtocol.Authentication;
43

54
namespace ModelContextProtocol.AspNetCore.Authentication;
@@ -11,10 +10,6 @@ public class McpAuthenticationOptions : AuthenticationSchemeOptions
1110
{
1211
private static readonly Uri DefaultResourceMetadataUri = new("/.well-known/oauth-protected-resource", UriKind.Relative);
1312

14-
private Func<HttpContext, ProtectedResourceMetadata>? _resourceMetadataProvider;
15-
16-
private ProtectedResourceMetadata? _resourceMetadata;
17-
1813
/// <summary>
1914
/// Initializes a new instance of the <see cref="McpAuthenticationOptions"/> class.
2015
/// </summary>
@@ -44,61 +39,11 @@ public McpAuthenticationOptions()
4439
public Uri ResourceMetadataUri { get; set; }
4540

4641
/// <summary>
47-
/// Gets or sets the static protected resource metadata.
42+
/// Gets or sets the protected resource metadata.
4843
/// </summary>
4944
/// <remarks>
5045
/// This contains the OAuth metadata for the protected resource, including authorization servers,
5146
/// supported scopes, and other information needed for clients to authenticate.
52-
/// Setting this property will automatically update the <see cref="ProtectedResourceMetadataProvider"/>
53-
/// to return this static instance.
54-
/// </remarks>
55-
/// <exception cref="ArgumentNullException">Thrown when trying to set a null value.</exception>
56-
/// <exception cref="ArgumentException">Thrown when the Resource property of the metadata is null.</exception>
57-
public ProtectedResourceMetadata ResourceMetadata
58-
{
59-
get => _resourceMetadata ?? throw new InvalidOperationException(
60-
"ResourceMetadata has not been configured.");
61-
set
62-
{
63-
ArgumentNullException.ThrowIfNull(value);
64-
if (value.Resource == null)
65-
{
66-
throw new ArgumentException("The Resource property of the metadata cannot be null. A valid resource URI is required.", nameof(value));
67-
}
68-
69-
_resourceMetadata = value;
70-
// When static metadata is set, update the provider to use it
71-
_resourceMetadataProvider = _ => _resourceMetadata;
72-
}
73-
}
74-
75-
/// <summary>
76-
/// Gets or sets a delegate that dynamically provides resource metadata based on the HTTP context.
77-
/// </summary>
78-
/// <remarks>
79-
/// When set, this delegate will be called to generate resource metadata for each request,
80-
/// allowing dynamic customization based on the caller or other contextual information.
81-
/// This takes precedence over the static <see cref="ResourceMetadata"/> property.
8247
/// </remarks>
83-
public Func<HttpContext, ProtectedResourceMetadata>? ProtectedResourceMetadataProvider
84-
{
85-
get => _resourceMetadataProvider;
86-
set => _resourceMetadataProvider = value;
87-
}
88-
89-
/// <summary>
90-
/// Gets the resource metadata for the current request.
91-
/// </summary>
92-
/// <param name="context">The HTTP context for the current request.</param>
93-
/// <returns>The resource metadata to use for the current request.</returns>
94-
/// <exception cref="InvalidOperationException">Thrown when no resource metadata has been configured.</exception>
95-
internal ProtectedResourceMetadata GetResourceMetadata(HttpContext context)
96-
{
97-
var provider = _resourceMetadataProvider;
98-
99-
return provider != null
100-
? provider(context)
101-
: _resourceMetadata ?? throw new InvalidOperationException(
102-
"ResourceMetadata has not been configured.");
103-
}
48+
public ProtectedResourceMetadata? ResourceMetadata { get; set; }
10449
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.AspNetCore.Authentication;
2+
using Microsoft.AspNetCore.Http;
3+
using ModelContextProtocol.Authentication;
4+
5+
namespace ModelContextProtocol.AspNetCore.Authentication;
6+
7+
/// <summary>
8+
/// Context for resource metadata request events.
9+
/// </summary>
10+
public class ResourceMetadataRequestContext : HandleRequestContext<McpAuthenticationOptions>
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="ResourceMetadataRequestContext"/> class.
14+
/// </summary>
15+
/// <param name="context">The HTTP context.</param>
16+
/// <param name="scheme">The authentication scheme.</param>
17+
/// <param name="options">The authentication options.</param>
18+
public ResourceMetadataRequestContext(
19+
HttpContext context,
20+
AuthenticationScheme scheme,
21+
McpAuthenticationOptions options)
22+
: base(context, scheme, options)
23+
{
24+
}
25+
26+
/// <summary>
27+
/// Gets or sets the protected resource metadata for the current request.
28+
/// </summary>
29+
public ProtectedResourceMetadata? ResourceMetadata { get; set; }
30+
}

src/ModelContextProtocol.Core/Authentication/ProtectedResourceMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public sealed class ProtectedResourceMetadata
1515
/// REQUIRED. The protected resource's resource identifier.
1616
/// </remarks>
1717
[JsonPropertyName("resource")]
18-
public required Uri Resource { get; init; }
18+
public required Uri Resource { get; set; }
1919

2020
/// <summary>
2121
/// The list of authorization server URIs.

tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,12 @@ public AuthTests(ITestOutputHelper outputHelper)
5555
})
5656
.AddMcp(options =>
5757
{
58-
options.ProtectedResourceMetadataProvider = context =>
58+
options.ResourceMetadata = new ProtectedResourceMetadata
5959
{
60-
var metadata = new ProtectedResourceMetadata
61-
{
62-
Resource = new Uri(McpServerUrl),
63-
BearerMethodsSupported = { "header" },
64-
AuthorizationServers = { new Uri(OAuthServerUrl) }
65-
};
66-
67-
metadata.ScopesSupported.AddRange([
68-
"mcp:tools"
69-
]);
70-
71-
return metadata;
60+
Resource = new Uri(McpServerUrl),
61+
AuthorizationServers = { new Uri(OAuthServerUrl) },
62+
BearerMethodsSupported = { "header" },
63+
ScopesSupported = ["mcp:tools"]
7264
};
7365
});
7466

0 commit comments

Comments
 (0)