Skip to content

Commit 1a3d2c7

Browse files
committed
Simplify HandleUnauthorizedResponseAsync signature by using exceptions
- Use Streamable HTTP transport in samples
1 parent 1de26c7 commit 1a3d2c7

File tree

13 files changed

+376
-602
lines changed

13 files changed

+376
-602
lines changed

samples/ProtectedMCPClient/Program.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Console.WriteLine("Protected MCP Weather Server");
1010
Console.WriteLine();
1111

12-
var serverUrl = "http://localhost:7071/sse";
12+
var serverUrl = "http://localhost:7071/";
1313
var clientId = Environment.GetEnvironmentVariable("CLIENT_ID") ?? throw new Exception("The CLIENT_ID environment variable is not set.");
1414

1515
// We can customize a shared HttpClient with a custom handler if desired
@@ -24,14 +24,9 @@
2424
var tokenProvider = new GenericOAuthProvider(
2525
new Uri(serverUrl),
2626
httpClient,
27-
null, // AuthorizationHelpers will be created automatically
2827
clientId: clientId,
29-
clientSecret: "", // No secret needed for this client
3028
redirectUri: new Uri("http://localhost:1179/callback"),
31-
scopes: null, // Scopes listed in scopes_supported will be requested automatically
32-
logger: null,
33-
authorizationUrlHandler: HandleAuthorizationUrlAsync
34-
);
29+
authorizationRedirectDelegate: HandleAuthorizationUrlAsync);
3530

3631
Console.WriteLine();
3732
Console.WriteLine($"Connecting to weather server at {serverUrl}...");

samples/ProtectedMCPServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
{
6363
var metadata = new ProtectedResourceMetadata
6464
{
65-
Resource = new Uri("http://localhost:7071/sse"),
65+
Resource = new Uri("http://localhost:7071/"),
6666
BearerMethodsSupported = { "header" },
6767
ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
6868
AuthorizationServers = { new Uri($"{instance}{tenantId}/v2.0") }

src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs

Lines changed: 34 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ namespace ModelContextProtocol.Authentication;
99
/// </summary>
1010
internal sealed class AuthenticatingMcpHttpClient(HttpClient httpClient, IMcpCredentialProvider credentialProvider) : McpHttpClient(httpClient)
1111
{
12-
private static readonly char[] SchemeSplitDelimiters = [' ', ','];
13-
1412
// Select first supported scheme as the default
1513
private string _currentScheme = credentialProvider.SupportedSchemes.FirstOrDefault() ??
1614
throw new ArgumentException("Authorization provider must support at least one authentication scheme.", nameof(credentialProvider));
@@ -47,88 +45,41 @@ private async Task<HttpResponseMessage> HandleUnauthorizedResponseAsync(
4745
// Gather the schemes the server wants us to use from WWW-Authenticate headers
4846
var serverSchemes = ExtractServerSupportedSchemes(response);
4947

50-
// Find the intersection between what the server supports and what our provider supports
51-
string? bestSchemeMatch = null;
52-
53-
// First try to find a direct match with the current scheme if it's still valid
54-
string schemeUsed = originalRequest.Headers.Authorization?.Scheme ?? _currentScheme ?? string.Empty;
55-
if (!string.IsNullOrEmpty(schemeUsed) &&
56-
serverSchemes.Contains(schemeUsed) &&
57-
credentialProvider.SupportedSchemes.Contains(schemeUsed))
58-
{
59-
bestSchemeMatch = schemeUsed;
60-
}
61-
else
48+
if (!serverSchemes.Contains(_currentScheme))
6249
{
6350
// Find the first server scheme that's in our supported set
64-
bestSchemeMatch = serverSchemes.Intersect(credentialProvider.SupportedSchemes, StringComparer.OrdinalIgnoreCase).FirstOrDefault();
65-
66-
// If no match was found, either throw an exception or use default
67-
if (bestSchemeMatch is null)
68-
{
69-
if (serverSchemes.Count > 0)
70-
{
71-
throw new IOException(
72-
$"The server does not support any of the provided authentication schemes." +
73-
$"Server supports: [{string.Join(", ", serverSchemes)}], " +
74-
$"Provider supports: [{string.Join(", ", credentialProvider.SupportedSchemes)}].");
75-
}
76-
77-
// If the server didn't specify any schemes, use the provider's default
78-
bestSchemeMatch = credentialProvider.SupportedSchemes.FirstOrDefault();
79-
}
80-
}
51+
var bestSchemeMatch = serverSchemes.Intersect(credentialProvider.SupportedSchemes, StringComparer.OrdinalIgnoreCase).FirstOrDefault();
8152

82-
if (bestSchemeMatch != null)
83-
{
84-
try
53+
if (bestSchemeMatch is not null)
8554
{
86-
// Try to handle the 401 response with the selected scheme
87-
var (handled, recommendedScheme) = await credentialProvider.HandleUnauthorizedResponseAsync(
88-
response,
89-
bestSchemeMatch,
90-
cancellationToken).ConfigureAwait(false);
91-
92-
if (!handled)
93-
{
94-
throw new McpException(
95-
$"Failed to handle unauthorized response with scheme '{bestSchemeMatch}'. " +
96-
"The authentication provider was unable to process the authentication challenge.");
97-
}
98-
99-
_currentScheme = recommendedScheme ?? bestSchemeMatch;
55+
_currentScheme = bestSchemeMatch;
10056
}
101-
catch (Exception ex)
57+
else if (serverSchemes.Count > 0)
10258
{
59+
// If no match was found, either throw an exception or use default
10360
throw new McpException(
104-
$"Failed to handle unauthorized response with scheme '{bestSchemeMatch}'. " +
105-
"The authentication provider encountered an error while processing the authentication challenge.",
106-
ex);
61+
$"The server does not support any of the provided authentication schemes." +
62+
$"Server supports: [{string.Join(", ", serverSchemes)}], " +
63+
$"Provider supports: [{string.Join(", ", credentialProvider.SupportedSchemes)}].");
10764
}
65+
}
10866

109-
var retryRequest = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri)
110-
{
111-
Version = originalRequest.Version,
112-
#if NET
113-
VersionPolicy = originalRequest.VersionPolicy,
114-
#endif
115-
};
116-
117-
// Copy headers except Authorization which we'll set separately
118-
foreach (var header in originalRequest.Headers)
119-
{
120-
if (!header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
121-
{
122-
retryRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
123-
}
124-
}
67+
// Try to handle the 401 response with the selected scheme
68+
await credentialProvider.HandleUnauthorizedResponseAsync(_currentScheme, response, cancellationToken).ConfigureAwait(false);
12569

126-
await AddAuthorizationHeaderAsync(retryRequest, _currentScheme, cancellationToken).ConfigureAwait(false);
70+
using var retryRequest = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri);
12771

128-
return await base.SendAsync(retryRequest, originalJsonRpcMessage, cancellationToken).ConfigureAwait(false);
72+
// Copy headers except Authorization which we'll set separately
73+
foreach (var header in originalRequest.Headers)
74+
{
75+
if (!header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
76+
{
77+
retryRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
78+
}
12979
}
13080

131-
return response; // Return the original response if we couldn't handle it
81+
await AddAuthorizationHeaderAsync(retryRequest, _currentScheme, cancellationToken).ConfigureAwait(false);
82+
return await base.SendAsync(retryRequest, originalJsonRpcMessage, cancellationToken).ConfigureAwait(false);
13283
}
13384

13485
/// <summary>
@@ -138,15 +89,9 @@ private static HashSet<string> ExtractServerSupportedSchemes(HttpResponseMessage
13889
{
13990
var serverSchemes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
14091

141-
if (response.Headers.Contains("WWW-Authenticate"))
92+
foreach (var header in response.Headers.WwwAuthenticate)
14293
{
143-
foreach (var authHeader in response.Headers.GetValues("WWW-Authenticate"))
144-
{
145-
// Extract the scheme from the WWW-Authenticate header
146-
// Format is typically: "Scheme param1=value1, param2=value2"
147-
string scheme = authHeader.Split(SchemeSplitDelimiters, StringSplitOptions.RemoveEmptyEntries)[0];
148-
serverSchemes.Add(scheme);
149-
}
94+
serverSchemes.Add(header.Scheme);
15095
}
15196

15297
return serverSchemes;
@@ -157,13 +102,17 @@ private static HashSet<string> ExtractServerSupportedSchemes(HttpResponseMessage
157102
/// </summary>
158103
private async Task AddAuthorizationHeaderAsync(HttpRequestMessage request, string scheme, CancellationToken cancellationToken)
159104
{
160-
if (request.RequestUri != null)
105+
if (request.RequestUri is null)
161106
{
162-
var token = await credentialProvider.GetCredentialAsync(scheme, request.RequestUri, cancellationToken).ConfigureAwait(false);
163-
if (!string.IsNullOrEmpty(token))
164-
{
165-
request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token);
166-
}
107+
return;
108+
}
109+
110+
var token = await credentialProvider.GetCredentialAsync(scheme, request.RequestUri, cancellationToken).ConfigureAwait(false);
111+
if (string.IsNullOrEmpty(token))
112+
{
113+
return;
167114
}
115+
116+
request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token);
168117
}
169118
}

0 commit comments

Comments
 (0)