Skip to content

Commit 03d0fda

Browse files
committed
Add ClientOAuthOptions
1 parent 3cc5c7f commit 03d0fda

File tree

9 files changed

+197
-152
lines changed

9 files changed

+197
-152
lines changed

samples/ProtectedMCPClient/Program.cs

Lines changed: 31 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,68 @@
11
using Microsoft.Extensions.Logging;
2-
using ModelContextProtocol.Authentication;
32
using ModelContextProtocol.Client;
43
using ModelContextProtocol.Protocol;
54
using System.Diagnostics;
65
using System.Net;
76
using System.Text;
87
using System.Web;
98

9+
var serverUrl = "http://localhost:7071/";
10+
1011
Console.WriteLine("Protected MCP Client");
12+
Console.WriteLine($"Connecting to weather server at {serverUrl}...");
1113
Console.WriteLine();
1214

13-
var serverUrl = "http://localhost:7071/";
14-
var clientId = Environment.GetEnvironmentVariable("CLIENT_ID") ?? throw new Exception("The CLIENT_ID environment variable is not set.");
15-
1615
// We can customize a shared HttpClient with a custom handler if desired
1716
var sharedHandler = new SocketsHttpHandler
1817
{
1918
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
2019
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
2120
};
21+
var httpClient = new HttpClient(sharedHandler);
2222

2323
var consoleLoggerFactory = LoggerFactory.Create(builder =>
2424
{
2525
builder.AddConsole();
2626
});
2727

28-
var httpClient = new HttpClient(sharedHandler);
29-
// Create the token provider with our custom HttpClient and authorization URL handler
30-
var tokenProvider = new GenericOAuthProvider(
31-
new Uri(serverUrl),
32-
httpClient,
33-
//clientId: clientId,
34-
clientId: "demo-client",
35-
clientSecret: "demo-secret",
36-
redirectUri: new Uri("http://localhost:1179/callback"),
37-
authorizationRedirectDelegate: HandleAuthorizationUrlAsync,
38-
loggerFactory: consoleLoggerFactory);
39-
40-
Console.WriteLine();
41-
Console.WriteLine($"Connecting to weather server at {serverUrl}...");
42-
43-
try
28+
var transport = new SseClientTransport(new()
4429
{
45-
var transport = new SseClientTransport(new()
46-
{
47-
Endpoint = new Uri(serverUrl),
48-
Name = "Secure Weather Client",
49-
CredentialProvider = tokenProvider,
50-
}, httpClient, consoleLoggerFactory);
51-
52-
var client = await McpClientFactory.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
53-
54-
var tools = await client.ListToolsAsync();
55-
if (tools.Count == 0)
30+
Endpoint = new Uri(serverUrl),
31+
Name = "Secure Weather Client",
32+
OAuth = new()
5633
{
57-
Console.WriteLine("No tools available on the server.");
58-
return;
34+
ClientId = "demo-client",
35+
ClientSecret = "demo-secret",
36+
RedirectUri = new Uri("http://localhost:1179/callback"),
37+
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
5938
}
39+
}, httpClient, consoleLoggerFactory);
6040

61-
Console.WriteLine($"Found {tools.Count} tools on the server.");
62-
Console.WriteLine();
41+
var client = await McpClientFactory.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
6342

64-
if (tools.Any(t => t.Name == "GetAlerts"))
65-
{
66-
Console.WriteLine("Calling GetAlerts tool...");
43+
var tools = await client.ListToolsAsync();
44+
if (tools.Count == 0)
45+
{
46+
Console.WriteLine("No tools available on the server.");
47+
return;
48+
}
6749

68-
var result = await client.CallToolAsync(
69-
"GetAlerts",
70-
new Dictionary<string, object?> { { "state", "WA" } }
71-
);
50+
Console.WriteLine($"Found {tools.Count} tools on the server.");
51+
Console.WriteLine();
7252

73-
Console.WriteLine("Result: " + ((TextContentBlock)result.Content[0]).Text);
74-
Console.WriteLine();
75-
}
76-
}
77-
catch (Exception ex)
53+
if (tools.Any(t => t.Name == "GetAlerts"))
7854
{
79-
Console.WriteLine($"Error: {ex.Message}");
80-
if (ex.InnerException != null)
81-
{
82-
Console.WriteLine($"Inner error: {ex.InnerException.Message}");
83-
}
55+
Console.WriteLine("Calling GetAlerts tool...");
8456

85-
#if DEBUG
86-
Console.WriteLine($"Stack trace: {ex.StackTrace}");
87-
#endif
57+
var result = await client.CallToolAsync(
58+
"GetAlerts",
59+
new Dictionary<string, object?> { { "state", "WA" } }
60+
);
61+
62+
Console.WriteLine("Result: " + ((TextContentBlock)result.Content[0]).Text);
63+
Console.WriteLine();
8864
}
89-
Console.WriteLine("Press any key to exit...");
90-
Console.ReadKey();
9165

92-
/// <summary>
9366
/// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
9467
/// This implementation demonstrates how SDK consumers can provide their own authorization flow.
9568
/// </summary>

src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace ModelContextProtocol.Authentication;
77
/// <summary>
88
/// A delegating handler that adds authentication tokens to requests and handles 401 responses.
99
/// </summary>
10-
internal sealed class AuthenticatingMcpHttpClient(HttpClient httpClient, IMcpCredentialProvider credentialProvider) : McpHttpClient(httpClient)
10+
internal sealed class AuthenticatingMcpHttpClient(HttpClient httpClient, ClientOAuthProvider credentialProvider) : McpHttpClient(httpClient)
1111
{
1212
// Select first supported scheme as the default
1313
private string _currentScheme = credentialProvider.SupportedSchemes.FirstOrDefault() ??
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace ModelContextProtocol.Authentication;
2+
3+
/// <summary>
4+
/// Provides configuration options for the <see cref="ClientOAuthProvider"/>.
5+
/// </summary>
6+
public sealed class ClientOAuthOptions
7+
{
8+
/// <summary>
9+
/// Gets or sets the OAuth client ID.
10+
/// </summary>
11+
public required string ClientId { get; set; }
12+
13+
/// <summary>
14+
/// Gets or sets the OAuth redirect URI.
15+
/// </summary>
16+
public required Uri RedirectUri { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets the OAuth client secret.
20+
/// </summary>
21+
/// <remarks>
22+
/// This is optional for public clients or when using PKCE without client authentication.
23+
/// </remarks>
24+
public string? ClientSecret { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the OAuth scopes to request.
28+
/// </summary>
29+
/// <remarks>
30+
/// <para>
31+
/// When specified, these scopes will be used instead of the scopes advertised by the protected resource.
32+
/// If not specified, the provider will use the scopes from the protected resource metadata.
33+
/// </para>
34+
/// <para>
35+
/// Common OAuth scopes include "openid", "profile", "email", etc.
36+
/// </para>
37+
/// </remarks>
38+
public IEnumerable<string>? Scopes { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the authorization redirect delegate for handling the OAuth authorization flow.
42+
/// </summary>
43+
/// <remarks>
44+
/// <para>
45+
/// This delegate is responsible for handling the OAuth authorization URL and obtaining the authorization code.
46+
/// If not specified, a default implementation will be used that prompts the user to enter the code manually.
47+
/// </para>
48+
/// <para>
49+
/// Custom implementations might open a browser, start an HTTP listener, or use other mechanisms to capture
50+
/// the authorization code from the OAuth redirect.
51+
/// </para>
52+
/// </remarks>
53+
public AuthorizationRedirectDelegate? AuthorizationRedirectDelegate { get; set; }
54+
55+
/// <summary>
56+
/// Gets or sets the authorization server selector function.
57+
/// </summary>
58+
/// <remarks>
59+
/// <para>
60+
/// This function is used to select which authorization server to use when multiple servers are available.
61+
/// If not specified, the first available server will be selected.
62+
/// </para>
63+
/// <para>
64+
/// The function receives a list of available authorization server URIs and should return the selected server,
65+
/// or null if no suitable server is found.
66+
/// </para>
67+
/// </remarks>
68+
public Func<IReadOnlyList<Uri>, Uri?>? AuthServerSelector { get; set; }
69+
}

src/ModelContextProtocol.Core/Authentication/GenericOAuthProvider.cs renamed to src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace ModelContextProtocol.Authentication;
1414
/// protection or caching - it acquires a token and server metadata and holds it in memory.
1515
/// This is suitable for demonstration and development purposes.
1616
/// </summary>
17-
public sealed class GenericOAuthProvider : IMcpCredentialProvider
17+
internal sealed class ClientOAuthProvider
1818
{
1919
/// <summary>
2020
/// The Bearer authentication scheme.
@@ -23,55 +23,51 @@ public sealed class GenericOAuthProvider : IMcpCredentialProvider
2323

2424
private readonly Uri _serverUrl;
2525
private readonly Uri _redirectUri;
26-
private readonly IList<string>? _scopes;
26+
private readonly string[]? _scopes;
2727
private readonly string _clientId;
2828
private readonly string? _clientSecret;
29-
private readonly HttpClient _httpClient;
30-
private readonly ILogger _logger;
3129
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
3230
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
3331

32+
private readonly HttpClient _httpClient;
33+
private readonly ILogger _logger;
34+
3435
private TokenContainer? _token;
3536
private AuthorizationServerMetadata? _authServerMetadata;
3637

3738
/// <summary>
38-
/// Initializes a new instance of the <see cref="GenericOAuthProvider"/> class with explicit authorization server selection.
39+
/// Initializes a new instance of the <see cref="ClientOAuthProvider"/> class using the specified options.
3940
/// </summary>
4041
/// <param name="serverUrl">The MCP server URL.</param>
42+
/// <param name="options">The OAuth provider configuration options.</param>
4143
/// <param name="httpClient">The HTTP client to use for OAuth requests. If null, a default HttpClient will be used.</param>
42-
/// <param name="clientId">OAuth client ID.</param>
43-
/// <param name="clientSecret">OAuth client secret.</param>
44-
/// <param name="redirectUri">OAuth redirect URI.</param>
45-
/// <param name="authorizationRedirectDelegate">Custom handler for processing the OAuth authorization URL. If null, uses the default HTTP listener approach.</param>
46-
/// <param name="scopes">Additional OAuth scopes to request instead of those specified in the scopes_supported specified in the .well-known/oauth-protected-resource response.</param>
4744
/// <param name="loggerFactory">A logger factory to handle diagnostic messages.</param>
48-
/// <param name="authServerSelector">Function to select which authorization server to use from available servers. If null, uses default selection strategy.</param>
49-
/// <exception cref="ArgumentNullException">Thrown when serverUrl is null.</exception>
50-
public GenericOAuthProvider(
45+
/// <exception cref="ArgumentNullException">Thrown when serverUrl or options are null.</exception>
46+
public ClientOAuthProvider(
5147
Uri serverUrl,
52-
HttpClient? httpClient,
53-
string clientId,
54-
Uri redirectUri,
55-
AuthorizationRedirectDelegate? authorizationRedirectDelegate = null,
56-
string? clientSecret = null,
57-
IList<string>? scopes = null,
58-
Func<IReadOnlyList<Uri>, Uri?>? authServerSelector = null,
48+
ClientOAuthOptions options,
49+
HttpClient? httpClient = null,
5950
ILoggerFactory? loggerFactory = null)
6051
{
6152
_serverUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));
6253
_httpClient = httpClient ?? new HttpClient();
63-
_logger = (ILogger?)loggerFactory?.CreateLogger<GenericOAuthProvider>() ?? NullLogger.Instance;
54+
_logger = (ILogger?)loggerFactory?.CreateLogger<ClientOAuthProvider>() ?? NullLogger.Instance;
55+
56+
if (options is null)
57+
{
58+
throw new ArgumentNullException(nameof(options));
59+
}
6460

65-
_redirectUri = redirectUri;
66-
_scopes = scopes;
67-
_clientId = clientId;
68-
_clientSecret = clientSecret;
61+
_clientId = options.ClientId;
62+
_redirectUri = options.RedirectUri;
63+
_clientSecret = options.ClientSecret;
64+
_scopes = options.Scopes?.ToArray();
6965

7066
// Set up authorization server selection strategy
71-
_authServerSelector = authServerSelector ?? DefaultAuthServerSelector;
67+
_authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector;
7268

7369
// Set up authorization URL handler (use default if not provided)
74-
_authorizationRedirectDelegate = authorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler;
70+
_authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler;
7571
}
7672

7773
/// <summary>
@@ -301,9 +297,9 @@ private Uri BuildAuthorizationUrl(
301297
queryParams["code_challenge_method"] = "S256";
302298

303299
var scopesSupported = protectedResourceMetadata.ScopesSupported;
304-
if (_scopes?.Count > 0 || scopesSupported.Count > 0)
300+
if (_scopes is not null || scopesSupported.Count > 0)
305301
{
306-
queryParams["scope"] = string.Join(" ", _scopes ?? scopesSupported);
302+
queryParams["scope"] = string.Join(" ", _scopes ?? scopesSupported.ToArray());
307303
}
308304

309305
var uriBuilder = new UriBuilder(authServerMetadata.AuthorizationEndpoint)

src/ModelContextProtocol.Core/Authentication/IMcpCredentialProvider.cs

Lines changed: 0 additions & 45 deletions
This file was deleted.

src/ModelContextProtocol.Core/Client/SseClientTransport.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ public SseClientTransport(SseClientTransportOptions transportOptions, HttpClient
4949
_loggerFactory = loggerFactory;
5050
Name = transportOptions.Name ?? transportOptions.Endpoint.ToString();
5151

52-
if (transportOptions.CredentialProvider is { } credentialProvider)
52+
if (transportOptions.OAuth is { } clientOAuthOptions)
5353
{
54-
_mcpHttpClient = new AuthenticatingMcpHttpClient(httpClient, credentialProvider);
54+
var oAuthProvider = new ClientOAuthProvider(_options.Endpoint, clientOAuthOptions, httpClient, loggerFactory);
55+
_mcpHttpClient = new AuthenticatingMcpHttpClient(httpClient, oAuthProvider);
5556
}
5657
else
5758
{

src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,5 @@ public required Uri Endpoint
7676
/// <summary>
7777
/// Gets sor sets the authorization provider to use for authentication.
7878
/// </summary>
79-
public IMcpCredentialProvider? CredentialProvider { get; set; }
79+
public ClientOAuthOptions? OAuth { get; set; }
8080
}

0 commit comments

Comments
 (0)