Skip to content

Commit 4c7a578

Browse files
committed
Iterating on the changes
1 parent ecc40ab commit 4c7a578

File tree

6 files changed

+312
-84
lines changed

6 files changed

+312
-84
lines changed
Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using ModelContextProtocol.Client;
3+
using ModelContextProtocol.Protocol.Auth;
34
using ModelContextProtocol.Protocol.Transport;
45

56
namespace AuthorizationExample;
@@ -14,35 +15,76 @@ public static async Task Main(string[] args)
1415
// Define the MCP server endpoint that requires OAuth authentication
1516
var serverEndpoint = new Uri("http://localhost:7071/sse");
1617

18+
// Configuration values for OAuth redirect
19+
string hostname = "localhost";
20+
int port = 8888;
21+
string callbackPath = "/oauth/callback";
22+
1723
// Set up the SSE transport with authorization support
1824
var transportOptions = new SseClientTransportOptions
1925
{
2026
Endpoint = serverEndpoint,
21-
AuthorizeCallback = SseClientTransport.CreateLocalServerAuthorizeCallback(
22-
openBrowser: async (url) =>
23-
{
24-
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
25-
}
26-
)
27+
AuthorizationOptions = new McpAuthorizationOptions
28+
{
29+
// Pre-registered client credentials (if applicable)
30+
ClientId = "my-registered-client-id",
31+
ClientSecret = "optional-client-secret",
32+
33+
// Specify the exact same redirect URIs that are registered with the OAuth server
34+
RedirectUris = new[]
35+
{
36+
$"http://{hostname}:{port}{callbackPath}"
37+
},
38+
39+
// Configure the authorize callback with the same hostname, port, and path
40+
AuthorizeCallback = SseClientTransport.CreateHttpListenerAuthorizeCallback(
41+
openBrowser: async (url) =>
42+
{
43+
Console.WriteLine($"Opening browser to authorize at: {url}");
44+
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
45+
},
46+
hostname: hostname,
47+
listenPort: port,
48+
redirectPath: callbackPath,
49+
successHtml: "<html><body><h1>Authorization Successful</h1><p>You have successfully authorized the application. You can close this window and return to the app.</p><script>window.close();</script></body></html>"
50+
)
51+
}
2752
};
2853

29-
// Create the client with authorization-enabled transport
30-
var transport = new SseClientTransport(transportOptions);
31-
var client = await McpClientFactory.CreateAsync(transport);
32-
33-
// Print the list of tools available from the server.
34-
foreach (var tool in await client.ListToolsAsync())
54+
Console.WriteLine("Connecting to MCP server...");
55+
56+
try
3557
{
36-
Console.WriteLine($"{tool.Name} ({tool.Description})");
37-
}
58+
// Create the client with authorization-enabled transport
59+
var transport = new SseClientTransport(transportOptions);
60+
var client = await McpClientFactory.CreateAsync(transport);
3861

39-
// Execute a tool (this would normally be driven by LLM tool invocations).
40-
var result = await client.CallToolAsync(
41-
"echo",
42-
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
43-
cancellationToken: CancellationToken.None);
62+
Console.WriteLine("Successfully connected and authorized!");
63+
64+
// Print the list of tools available from the server.
65+
Console.WriteLine("\nAvailable tools:");
66+
foreach (var tool in await client.ListToolsAsync())
67+
{
68+
Console.WriteLine($" - {tool.Name}: {tool.Description}");
69+
}
4470

45-
// echo always returns one and only one text content object
46-
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
71+
// Execute a tool (this would normally be driven by LLM tool invocations).
72+
Console.WriteLine("\nCalling 'echo' tool...");
73+
var result = await client.CallToolAsync(
74+
"echo",
75+
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
76+
cancellationToken: CancellationToken.None);
77+
78+
// echo always returns one and only one text content object
79+
Console.WriteLine($"Tool response: {result.Content.First(c => c.Type == "text").Text}");
80+
}
81+
catch (Exception ex)
82+
{
83+
Console.WriteLine($"Error: {ex.Message}");
84+
if (ex.InnerException != null)
85+
{
86+
Console.WriteLine($"Inner Error: {ex.InnerException.Message}");
87+
}
88+
}
4789
}
4890
}

src/ModelContextProtocol/Protocol/Auth/DefaultAuthorizationHandler.cs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,40 @@ internal class DefaultAuthorizationHandler : IAuthorizationHandler
1515
private readonly ILogger _logger;
1616
private readonly SynchronizedValue<AuthorizationContext> _authContext = new(new AuthorizationContext());
1717
private readonly Func<ClientMetadata, Task<(string RedirectUri, string Code)>>? _authorizeCallback;
18+
private readonly string? _clientId;
19+
private readonly string? _clientSecret;
20+
private readonly ICollection<string>? _redirectUris;
21+
private readonly ICollection<string>? _scopes;
1822

1923
/// <summary>
2024
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
2125
/// </summary>
2226
/// <param name="loggerFactory">The logger factory.</param>
23-
/// <param name="authorizeCallback">A callback function that handles the authorization code flow.</param>
24-
public DefaultAuthorizationHandler(ILoggerFactory? loggerFactory = null, Func<ClientMetadata, Task<(string RedirectUri, string Code)>>? authorizeCallback = null)
27+
/// <param name="options">The authorization options.</param>
28+
public DefaultAuthorizationHandler(ILoggerFactory? loggerFactory = null, McpAuthorizationOptions? options = null)
2529
{
2630
_logger = loggerFactory != null
2731
? loggerFactory.CreateLogger<DefaultAuthorizationHandler>()
2832
: NullLogger<DefaultAuthorizationHandler>.Instance;
29-
_authorizeCallback = authorizeCallback;
33+
34+
if (options != null)
35+
{
36+
_authorizeCallback = options.AuthorizeCallback;
37+
_clientId = options.ClientId;
38+
_clientSecret = options.ClientSecret;
39+
_redirectUris = options.RedirectUris;
40+
_scopes = options.Scopes;
41+
}
42+
}
43+
44+
/// <summary>
45+
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
46+
/// </summary>
47+
/// <param name="loggerFactory">The logger factory.</param>
48+
/// <param name="authorizeCallback">A callback function that handles the authorization code flow.</param>
49+
public DefaultAuthorizationHandler(ILoggerFactory? loggerFactory = null, Func<ClientMetadata, Task<(string RedirectUri, string Code)>>? authorizeCallback = null)
50+
: this(loggerFactory, new McpAuthorizationOptions { AuthorizeCallback = authorizeCallback })
51+
{
3052
}
3153

3254
/// <inheritdoc />
@@ -110,26 +132,43 @@ public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage resp
110132
_logger.LogDebug("Successfully retrieved authorization server metadata");
111133

112134
// Create client metadata
135+
string[] redirectUris = _redirectUris?.ToArray() ?? new[] { "http://localhost:8888/callback" };
113136
var clientMetadata = new ClientMetadata
114137
{
115-
RedirectUris = new[] { "http://localhost:8888/callback" }, // Default redirect URI
138+
RedirectUris = redirectUris,
116139
ClientName = "MCP C# SDK Client",
117-
Scope = string.Join(" ", resourceMetadata.ScopesSupported ?? Array.Empty<string>())
140+
Scope = string.Join(" ", _scopes ?? resourceMetadata.ScopesSupported ?? Array.Empty<string>())
118141
};
119-
120-
// Register client if the server supports it
121-
if (authServerMetadata.RegistrationEndpoint != null)
142+
143+
// Register client if needed, or use pre-configured client ID
144+
if (!string.IsNullOrEmpty(_clientId))
145+
{
146+
_logger.LogDebug("Using pre-configured client ID: {ClientId}", _clientId);
147+
148+
// Create a client registration response to store in the context
149+
var clientRegistration = new ClientRegistrationResponse
150+
{
151+
ClientId = _clientId!, // Using null-forgiving operator since we've already checked it's not null
152+
ClientSecret = _clientSecret,
153+
};
154+
155+
authContext.Value.ClientRegistration = clientRegistration;
156+
}
157+
else if (authServerMetadata.RegistrationEndpoint != null)
122158
{
159+
// Register client dynamically
123160
_logger.LogDebug("Registering client with authorization server");
124161
var clientRegistration = await AuthorizationService.RegisterClientAsync(authServerMetadata, clientMetadata);
125162
authContext.Value.ClientRegistration = clientRegistration;
126163
_logger.LogDebug("Client registered successfully with ID: {ClientId}", clientRegistration.ClientId);
127164
}
128165
else
129166
{
130-
_logger.LogWarning("Authorization server does not support dynamic client registration");
167+
_logger.LogWarning("Authorization server does not support dynamic client registration and no client ID was provided");
131168

132-
var exception = new McpAuthorizationException("Authorization server does not support dynamic client registration");
169+
var exception = new McpAuthorizationException(
170+
"Authorization server does not support dynamic client registration and no client ID was provided. " +
171+
"Use McpAuthorizationOptions.ClientId to provide a pre-registered client ID.");
133172
exception.ResourceUri = resourceMetadata.Resource;
134173
exception.AuthorizationServerUri = authServerUrl;
135174
throw exception;
@@ -142,7 +181,7 @@ public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage resp
142181

143182
var exception = new McpAuthorizationException(
144183
"Authentication is required but no authorization callback was provided. " +
145-
"Use SseClientTransportOptions.AuthorizeCallback to provide a callback function.");
184+
"Use McpAuthorizationOptions.AuthorizeCallback to provide a callback function.");
146185
exception.ResourceUri = resourceMetadata.Resource;
147186
exception.AuthorizationServerUri = authServerUrl;
148187
throw exception;
@@ -155,18 +194,18 @@ public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage resp
155194
// Initiate authorization code flow
156195
_logger.LogDebug("Initiating authorization code flow");
157196

158-
// Get the registered client ID
159-
var clientId = authContext.Value.ClientRegistration!.ClientId;
160-
161197
// Get the authorization URL that the user needs to visit
162198
var authUrl = AuthorizationService.CreateAuthorizationUrl(
163199
authServerMetadata,
164-
clientId,
200+
authContext.Value.ClientRegistration.ClientId,
165201
clientMetadata.RedirectUris[0],
166202
codeChallenge,
167-
resourceMetadata.ScopesSupported);
203+
_scopes?.ToArray() ?? resourceMetadata.ScopesSupported);
168204

169205
_logger.LogDebug("Authorization URL: {AuthUrl}", authUrl);
206+
207+
// Set the authorization URL in the client metadata
208+
clientMetadata.ClientUri = authUrl;
170209

171210
// Let the callback handle the user authorization
172211
var (redirectUri, code) = await _authorizeCallback(clientMetadata);
@@ -176,7 +215,7 @@ public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage resp
176215
_logger.LogDebug("Exchanging authorization code for tokens");
177216
var tokenResponse = await AuthorizationService.ExchangeCodeForTokensAsync(
178217
authServerMetadata,
179-
clientId,
218+
authContext.Value.ClientRegistration.ClientId,
180219
authContext.Value.ClientRegistration.ClientSecret,
181220
redirectUri,
182221
code,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace ModelContextProtocol.Protocol.Auth;
5+
6+
/// <summary>
7+
/// Provides authorization options for MCP clients.
8+
/// </summary>
9+
public class McpAuthorizationOptions
10+
{
11+
/// <summary>
12+
/// Gets or sets a delegate that handles the OAuth 2.0 authorization code flow.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// This delegate is called when the server requires OAuth 2.0 authorization. It receives the client metadata
17+
/// and should return the redirect URI and authorization code received from the authorization server.
18+
/// </para>
19+
/// <para>
20+
/// If not provided, the client will not be able to authenticate with servers that require OAuth authentication.
21+
/// </para>
22+
/// </remarks>
23+
public Func<ClientMetadata, Task<(string RedirectUri, string Code)>>? AuthorizeCallback { get; init; }
24+
25+
/// <summary>
26+
/// Gets or sets the client ID to use for OAuth authorization.
27+
/// </summary>
28+
/// <remarks>
29+
/// <para>
30+
/// If specified, this client ID will be used during the OAuth flow instead of performing dynamic client registration.
31+
/// This is useful when connecting to servers that have pre-registered clients.
32+
/// </para>
33+
/// </remarks>
34+
public string? ClientId { get; init; }
35+
36+
/// <summary>
37+
/// Gets or sets the client secret associated with the client ID.
38+
/// </summary>
39+
/// <remarks>
40+
/// This is only required if the client was registered as a confidential client with the authorization server.
41+
/// Public clients don't require a client secret.
42+
/// </remarks>
43+
public string? ClientSecret { get; init; }
44+
45+
/// <summary>
46+
/// Gets or sets the redirect URIs that can be used during the OAuth authorization flow.
47+
/// </summary>
48+
/// <remarks>
49+
/// <para>
50+
/// These URIs must match the redirect URIs registered with the authorization server for the client.
51+
/// </para>
52+
/// <para>
53+
/// If not specified and <see cref="ClientId"/> is set, a default value of
54+
/// "http://localhost:8888/callback" will be used.
55+
/// </para>
56+
/// </remarks>
57+
public ICollection<string>? RedirectUris { get; init; }
58+
59+
/// <summary>
60+
/// Gets or sets the scopes to request during OAuth authorization.
61+
/// </summary>
62+
/// <remarks>
63+
/// <para>
64+
/// If not specified, the scopes will be determined from the server's resource metadata.
65+
/// </para>
66+
/// </remarks>
67+
public ICollection<string>? Scopes { get; init; }
68+
69+
/// <summary>
70+
/// Gets or sets a custom authorization handler.
71+
/// </summary>
72+
/// <remarks>
73+
/// <para>
74+
/// If specified, this handler will be used to manage authorization with the server.
75+
/// </para>
76+
/// <para>
77+
/// If not provided, a default handler will be created using the other options.
78+
/// </para>
79+
/// </remarks>
80+
public IAuthorizationHandler? AuthorizationHandler { get; init; }
81+
}

src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,16 @@ public SseClientSessionTransport(SseClientTransportOptions transportOptions, Htt
5050
_connectionEstablished = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
5151

5252
// Initialize the authorization handler
53-
_authorizationHandler = transportOptions.AuthorizationHandler ??
54-
new DefaultAuthorizationHandler(loggerFactory, transportOptions.AuthorizeCallback);
53+
if (transportOptions.AuthorizationOptions?.AuthorizationHandler != null)
54+
{
55+
// Use explicitly provided handler
56+
_authorizationHandler = transportOptions.AuthorizationOptions.AuthorizationHandler;
57+
}
58+
else
59+
{
60+
// Create default handler with auth options
61+
_authorizationHandler = new DefaultAuthorizationHandler(loggerFactory, transportOptions.AuthorizationOptions);
62+
}
5563
}
5664

5765
/// <inheritdoc/>

0 commit comments

Comments
 (0)