Skip to content

Commit e6c1995

Browse files
committed
Updated logc
1 parent b0d9932 commit e6c1995

File tree

2 files changed

+119
-105
lines changed

2 files changed

+119
-105
lines changed

samples/AuthorizationExample/Program.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ public static async Task Main(string[] args)
4747
},
4848
hostname: hostname,
4949
listenPort: port,
50-
redirectPath: callbackPath,
51-
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+
redirectPath: callbackPath
5251
)
5352
}
5453
};

src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs

Lines changed: 118 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Microsoft.Extensions.Logging;
22
using ModelContextProtocol.Protocol.Auth;
33
using ModelContextProtocol.Utils;
4+
using System.Net;
5+
using System.Text;
46

57
namespace ModelContextProtocol.Protocol.Transport;
68

@@ -61,147 +63,160 @@ public SseClientTransport(SseClientTransportOptions transportOptions, HttpClient
6163
public string Name { get; }
6264

6365
/// <summary>
64-
/// Creates a delegate that can handle the OAuth 2.0 authorization code flow using an HTTP listener.
66+
/// Creates a callback function for handling OAuth 2.0 authorization flows using an HTTP listener.
6567
/// </summary>
66-
/// <param name="openBrowser">A function that opens a URL in the browser.</param>
67-
/// <param name="hostname">The hostname to listen on for the redirect URI. Default is "localhost".</param>
68-
/// <param name="listenPort">The port to listen on for the redirect URI. Default is 8888.</param>
69-
/// <param name="redirectPath">The path for the redirect URI. Default is "/callback".</param>
70-
/// <param name="successHtml">The HTML content to display on successful authorization. If null, a default message is shown.</param>
71-
/// <param name="errorHtml">The HTML template to display on failed authorization. If null, a default message is shown. Use {0} as a placeholder for the error message.</param>
72-
/// <returns>A delegate that can be used for the <see cref="AuthorizationOptions.AuthorizeCallback"/> property.</returns>
73-
/// <remarks>
74-
/// <para>
75-
/// This method creates a delegate that implements a complete OAuth 2.0 authorization code flow using an HTTP listener.
76-
/// When called, it will:
77-
/// </para>
78-
/// <list type="bullet">
79-
/// <item><description>Open the authorization URL in the browser</description></item>
80-
/// <item><description>Start an HTTP listener to receive the authorization code</description></item>
81-
/// <item><description>Return the redirect URI and authorization code when received</description></item>
82-
/// </list>
83-
/// <para>
84-
/// You can customize the hostname, port, and path for the redirect URI to match your OAuth client configuration.
85-
/// </para>
86-
/// </remarks>
68+
/// <param name="openBrowser">A function to open the browser to the authorization URL.</param>
69+
/// <param name="hostname">The hostname for the HTTP listener. Defaults to "localhost".</param>
70+
/// <param name="listenPort">The port for the HTTP listener. Defaults to 8888.</param>
71+
/// <param name="redirectPath">The redirect path for the HTTP listener. Defaults to "/callback".</param>
72+
/// <returns>
73+
/// A function that takes <see cref="ClientMetadata"/> and returns a task that resolves to a tuple containing
74+
/// the redirect URI and the authorization code.
75+
/// </returns>
8776
public static Func<ClientMetadata, Task<(string RedirectUri, string Code)>> CreateHttpListenerAuthorizeCallback(
8877
Func<string, Task> openBrowser,
8978
string hostname = "localhost",
9079
int listenPort = 8888,
91-
string redirectPath = "/callback",
92-
string? successHtml = null,
93-
string? errorHtml = null)
80+
string redirectPath = "/callback")
9481
{
9582
return async (ClientMetadata clientMetadata) =>
9683
{
97-
// Default redirect URI based on parameters
98-
var defaultRedirectUri = $"http://{hostname}:{listenPort}{redirectPath}";
99-
100-
// First, try to find a matching redirect URI from the client metadata
101-
var redirectUri = defaultRedirectUri;
102-
var hostPrefix = $"http://{hostname}";
103-
84+
string redirectUri = $"http://{hostname}:{listenPort}{redirectPath}";
85+
10486
foreach (var uri in clientMetadata.RedirectUris)
10587
{
106-
if (uri.StartsWith(hostPrefix, StringComparison.OrdinalIgnoreCase))
88+
if (uri.StartsWith($"http://{hostname}", StringComparison.OrdinalIgnoreCase) &&
89+
Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
10790
{
10891
redirectUri = uri;
109-
110-
// Parse the port and path from the selected URI to ensure we listen on the correct endpoint
111-
if (Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
112-
{
113-
listenPort = parsedUri.IsDefaultPort ? 80 : parsedUri.Port;
114-
redirectPath = parsedUri.AbsolutePath;
115-
}
116-
92+
listenPort = parsedUri.IsDefaultPort ? 80 : parsedUri.Port;
93+
redirectPath = parsedUri.AbsolutePath;
11794
break;
11895
}
11996
}
120-
121-
// Use a TaskCompletionSource to wait for the authorization code
97+
12298
var authCodeTcs = new TaskCompletionSource<string>();
123-
124-
// Start an HTTP listener to listen for the authorization code
125-
using var listener = new System.Net.HttpListener();
126-
127-
// Ensure the URI format is correct for HttpListener
128-
var listenerPrefix = $"http://{hostname}:{listenPort}/";
129-
if (redirectPath.Length > 1)
99+
// Ensure the path has a trailing slash for the HttpListener prefix
100+
string listenerPrefix = $"http://{hostname}:{listenPort}{redirectPath}";
101+
if (!listenerPrefix.EndsWith("/"))
130102
{
131-
// If path is something like "/callback", we need to listen on all paths that start with it
132-
var basePath = redirectPath.TrimEnd('/').TrimStart('/');
133-
listenerPrefix = $"http://{hostname}:{listenPort}/{basePath}/";
103+
listenerPrefix += "/";
134104
}
135-
105+
106+
using var listener = new HttpListener();
136107
listener.Prefixes.Add(listenerPrefix);
137-
listener.Start();
138108

139-
// Default HTML responses
140-
var defaultSuccessHtml = "<html><body><h1>Authorization Successful</h1><p>You can now close this window and return to the application.</p></body></html>";
141-
var defaultErrorHtml = "<html><body><h1>Authorization Failed</h1><p>Error: {0}</p></body></html>";
109+
// Start the listener BEFORE opening the browser
110+
try
111+
{
112+
listener.Start();
113+
}
114+
catch (HttpListenerException ex)
115+
{
116+
throw new McpException($"Failed to start HTTP listener on {listenerPrefix}: {ex.Message}", McpErrorCode.InvalidRequest);
117+
}
118+
119+
// Create a cancellation token source with a timeout
120+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
142121

143-
// Start listening for the callback asynchronously
144-
var listenerTask = Task.Run(async () =>
122+
_ = Task.Run(async () =>
145123
{
146124
try
147125
{
148-
var context = await listener.GetContextAsync();
149-
var request = context.Request;
150-
151-
// Get the authorization code from the query string
152-
var code = request.QueryString["code"];
153-
var error = request.QueryString["error"];
126+
// GetContextAsync doesn't accept a cancellation token, so we need to handle cancellation manually
127+
var contextTask = listener.GetContextAsync();
128+
var completedTask = await Task.WhenAny(contextTask, Task.Delay(Timeout.Infinite, cts.Token));
154129

155-
// Send a response to the browser
156-
var response = context.Response;
157-
response.ContentType = "text/html";
158-
string responseHtml;
159-
160-
if (!string.IsNullOrEmpty(error))
130+
if (completedTask == contextTask)
161131
{
162-
responseHtml = string.Format(errorHtml ?? defaultErrorHtml, error);
163-
authCodeTcs.SetException(new McpException($"Authorization failed: {error}", McpErrorCode.InvalidRequest));
164-
}
165-
else if (string.IsNullOrEmpty(code))
166-
{
167-
responseHtml = string.Format(errorHtml ?? defaultErrorHtml, "No authorization code received");
168-
authCodeTcs.SetException(new McpException("No authorization code received", McpErrorCode.InvalidRequest));
169-
}
170-
else
171-
{
172-
responseHtml = successHtml ?? defaultSuccessHtml;
173-
authCodeTcs.SetResult(code);
132+
var context = await contextTask;
133+
var request = context.Request;
134+
var response = context.Response;
135+
136+
string? code = request.QueryString["code"];
137+
string? error = request.QueryString["error"];
138+
string html;
139+
string? resultCode = null;
140+
141+
if (!string.IsNullOrEmpty(error))
142+
{
143+
html = $"<html><body><h1>Authorization Failed</h1><p>Error: {WebUtility.HtmlEncode(error)}</p></body></html>";
144+
}
145+
else if (string.IsNullOrEmpty(code))
146+
{
147+
html = "<html><body><h1>Authorization Failed</h1><p>No authorization code received.</p></body></html>";
148+
}
149+
else
150+
{
151+
html = "<html><body><h1>Authorization Successful</h1><p>You may now close this window.</p></body></html>";
152+
resultCode = code;
153+
}
154+
155+
try
156+
{
157+
// Send response to browser
158+
byte[] buffer = Encoding.UTF8.GetBytes(html);
159+
response.ContentType = "text/html";
160+
response.ContentLength64 = buffer.Length;
161+
response.OutputStream.Write(buffer, 0, buffer.Length);
162+
163+
// IMPORTANT: Explicitly close the response to ensure it's fully sent
164+
response.Close();
165+
166+
// Now that we've finished processing the browser response,
167+
// we can safely signal completion or failure with the auth code
168+
if (resultCode != null)
169+
{
170+
authCodeTcs.TrySetResult(resultCode);
171+
}
172+
else if (!string.IsNullOrEmpty(error))
173+
{
174+
authCodeTcs.TrySetException(new McpException($"Authorization failed: {error}", McpErrorCode.InvalidRequest));
175+
}
176+
else
177+
{
178+
authCodeTcs.TrySetException(new McpException("No authorization code received", McpErrorCode.InvalidRequest));
179+
}
180+
}
181+
catch (Exception ex)
182+
{
183+
authCodeTcs.TrySetException(new McpException($"Error processing browser response: {ex.Message}", McpErrorCode.InvalidRequest));
184+
}
174185
}
175-
176-
var buffer = System.Text.Encoding.UTF8.GetBytes(responseHtml);
177-
response.ContentLength64 = buffer.Length;
178-
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
179-
response.Close();
180186
}
181187
catch (Exception ex)
182188
{
183189
authCodeTcs.TrySetException(ex);
184190
}
185-
finally
186-
{
187-
listener.Close();
188-
}
189191
});
190-
191-
// Open the authorization URL in the browser
192-
if (clientMetadata.ClientUri != null)
192+
193+
// Now open the browser AFTER the listener is started
194+
if (!string.IsNullOrEmpty(clientMetadata.ClientUri))
193195
{
194-
await openBrowser(clientMetadata.ClientUri);
196+
await openBrowser(clientMetadata.ClientUri!);
195197
}
196198
else
197199
{
198-
authCodeTcs.SetException(new McpException("No authorization URL provided in client metadata", McpErrorCode.InvalidRequest));
200+
// Stop the listener before throwing
201+
listener.Stop();
202+
throw new McpException("Client URI is missing in metadata.", McpErrorCode.InvalidRequest);
203+
}
204+
205+
try
206+
{
207+
// Use a timeout to avoid hanging indefinitely
208+
string authCode = await authCodeTcs.Task.WaitAsync(cts.Token);
209+
return (redirectUri, authCode);
210+
}
211+
catch (OperationCanceledException)
212+
{
213+
throw new McpException("Authorization timed out after 5 minutes.", McpErrorCode.InvalidRequest);
214+
}
215+
finally
216+
{
217+
// Ensure the listener is stopped when we're done
218+
listener.Stop();
199219
}
200-
201-
// Wait for the authorization code
202-
var code = await authCodeTcs.Task;
203-
204-
return (redirectUri, code);
205220
};
206221
}
207222

0 commit comments

Comments
 (0)