Authorization Support (Using ASP.NET Core Native AuthN/AuthZ Integration) (#377)
This commit is contained in:
parent
cd8e5d5f7f
commit
cdf88e7f5d
6 changed files with 4207 additions and 126 deletions
|
|
@ -13,13 +13,13 @@ namespace ModelContextProtocol.Client;
|
|||
internal sealed partial class AutoDetectingClientSessionTransport : ITransport
|
||||
{
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly McpHttpClient _httpClient;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _name;
|
||||
private readonly Channel<JsonRpcMessage> _messageChannel;
|
||||
|
||||
public AutoDetectingClientSessionTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory, string endpointName)
|
||||
public AutoDetectingClientSessionTransport(string endpointName, SseClientTransportOptions transportOptions, McpHttpClient httpClient, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
Throw.IfNull(transportOptions);
|
||||
Throw.IfNull(httpClient);
|
||||
|
|
|
|||
42
src/ModelContextProtocol.Core/Client/McpHttpClient.cs
Normal file
42
src/ModelContextProtocol.Core/Client/McpHttpClient.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
using System.Diagnostics;
|
||||
|
||||
#if NET
|
||||
using System.Net.Http.Json;
|
||||
#else
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
#endif
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
internal class McpHttpClient(HttpClient httpClient)
|
||||
{
|
||||
internal virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, JsonRpcMessage? message, CancellationToken cancellationToken)
|
||||
{
|
||||
Debug.Assert(request.Content is null, "The request body should only be supplied as a JsonRpcMessage");
|
||||
Debug.Assert(message is null || request.Method == HttpMethod.Post, "All messages should be sent in POST requests.");
|
||||
|
||||
using var content = CreatePostBodyContent(message);
|
||||
request.Content = content;
|
||||
return await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private HttpContent? CreatePostBodyContent(JsonRpcMessage? message)
|
||||
{
|
||||
if (message is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#if NET
|
||||
return JsonContent.Create(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
|
||||
#else
|
||||
return new StringContent(
|
||||
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ using ModelContextProtocol.Protocol;
|
|||
using System.Diagnostics;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Channels;
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ namespace ModelContextProtocol.Client;
|
|||
/// </summary>
|
||||
internal sealed partial class SseClientSessionTransport : TransportBase
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly McpHttpClient _httpClient;
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly Uri _sseEndpoint;
|
||||
private Uri? _messageEndpoint;
|
||||
|
|
@ -31,7 +30,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
public SseClientSessionTransport(
|
||||
string endpointName,
|
||||
SseClientTransportOptions transportOptions,
|
||||
HttpClient httpClient,
|
||||
McpHttpClient httpClient,
|
||||
Channel<JsonRpcMessage>? messageChannel,
|
||||
ILoggerFactory? loggerFactory)
|
||||
: base(endpointName, messageChannel, loggerFactory)
|
||||
|
|
@ -74,12 +73,6 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
if (_messageEndpoint == null)
|
||||
throw new InvalidOperationException("Transport not connected");
|
||||
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
string messageId = "(no id)";
|
||||
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
|
|
@ -87,12 +80,9 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint)
|
||||
{
|
||||
Content = content,
|
||||
};
|
||||
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint);
|
||||
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null);
|
||||
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
|
||||
var response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
|
|
@ -154,11 +144,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null);
|
||||
|
||||
using var response = await _httpClient.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
using var response = await _httpClient.SendAsync(request, message: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Authentication;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
|
@ -15,9 +16,10 @@ namespace ModelContextProtocol.Client;
|
|||
public sealed class SseClientTransport : IClientTransport, IAsyncDisposable
|
||||
{
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly McpHttpClient _mcpHttpClient;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
private readonly bool _ownsHttpClient;
|
||||
|
||||
private readonly HttpClient? _ownedHttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SseClientTransport"/> class.
|
||||
|
|
@ -45,10 +47,23 @@ public sealed class SseClientTransport : IClientTransport, IAsyncDisposable
|
|||
Throw.IfNull(httpClient);
|
||||
|
||||
_options = transportOptions;
|
||||
_httpClient = httpClient;
|
||||
_loggerFactory = loggerFactory;
|
||||
_ownsHttpClient = ownsHttpClient;
|
||||
Name = transportOptions.Name ?? transportOptions.Endpoint.ToString();
|
||||
|
||||
if (transportOptions.OAuth is { } clientOAuthOptions)
|
||||
{
|
||||
var oAuthProvider = new ClientOAuthProvider(_options.Endpoint, clientOAuthOptions, httpClient, loggerFactory);
|
||||
_mcpHttpClient = new AuthenticatingMcpHttpClient(httpClient, oAuthProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mcpHttpClient = new(httpClient);
|
||||
}
|
||||
|
||||
if (ownsHttpClient)
|
||||
{
|
||||
_ownedHttpClient = httpClient;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -59,8 +74,8 @@ public sealed class SseClientTransport : IClientTransport, IAsyncDisposable
|
|||
{
|
||||
return _options.TransportMode switch
|
||||
{
|
||||
HttpTransportMode.AutoDetect => new AutoDetectingClientSessionTransport(_options, _httpClient, _loggerFactory, Name),
|
||||
HttpTransportMode.StreamableHttp => new StreamableHttpClientSessionTransport(Name, _options, _httpClient, messageChannel: null, _loggerFactory),
|
||||
HttpTransportMode.AutoDetect => new AutoDetectingClientSessionTransport(Name, _options, _mcpHttpClient, _loggerFactory),
|
||||
HttpTransportMode.StreamableHttp => new StreamableHttpClientSessionTransport(Name, _options, _mcpHttpClient, messageChannel: null, _loggerFactory),
|
||||
HttpTransportMode.Sse => await ConnectSseTransportAsync(cancellationToken).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported transport mode: {_options.TransportMode}"),
|
||||
};
|
||||
|
|
@ -68,7 +83,7 @@ public sealed class SseClientTransport : IClientTransport, IAsyncDisposable
|
|||
|
||||
private async Task<ITransport> ConnectSseTransportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var sessionTransport = new SseClientSessionTransport(Name, _options, _httpClient, messageChannel: null, _loggerFactory);
|
||||
var sessionTransport = new SseClientSessionTransport(Name, _options, _mcpHttpClient, messageChannel: null, _loggerFactory);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -85,11 +100,7 @@ public sealed class SseClientTransport : IClientTransport, IAsyncDisposable
|
|||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (_ownsHttpClient)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
_ownedHttpClient?.Dispose();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using ModelContextProtocol.Authentication;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -46,7 +48,7 @@ public sealed class SseClientTransportOptions
|
|||
public HttpTransportMode TransportMode { get; set; } = HttpTransportMode.AutoDetect;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a transport identifier used for logging purposes.
|
||||
/// Gets or sets a transport identifier used for logging purposes.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
|
|
@ -70,4 +72,9 @@ public sealed class SseClientTransportOptions
|
|||
/// Use this property to specify custom HTTP headers that should be sent with each request to the server.
|
||||
/// </remarks>
|
||||
public IDictionary<string, string>? AdditionalHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets sor sets the authorization provider to use for authentication.
|
||||
/// </summary>
|
||||
public ClientOAuthOptions? OAuth { get; set; }
|
||||
}
|
||||
|
|
@ -6,12 +6,6 @@ using System.Text.Json;
|
|||
using ModelContextProtocol.Protocol;
|
||||
using System.Threading.Channels;
|
||||
|
||||
#if NET
|
||||
using System.Net.Http.Json;
|
||||
#else
|
||||
using System.Text;
|
||||
#endif
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -22,7 +16,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
private static readonly MediaTypeWithQualityHeaderValue s_applicationJsonMediaType = new("application/json");
|
||||
private static readonly MediaTypeWithQualityHeaderValue s_textEventStreamMediaType = new("text/event-stream");
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly McpHttpClient _httpClient;
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly CancellationTokenSource _connectionCts;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -36,7 +30,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
public StreamableHttpClientSessionTransport(
|
||||
string endpointName,
|
||||
SseClientTransportOptions transportOptions,
|
||||
HttpClient httpClient,
|
||||
McpHttpClient httpClient,
|
||||
Channel<JsonRpcMessage>? messageChannel,
|
||||
ILoggerFactory? loggerFactory)
|
||||
: base(endpointName, messageChannel, loggerFactory)
|
||||
|
|
@ -69,19 +63,8 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionCts.Token);
|
||||
cancellationToken = sendCts.Token;
|
||||
|
||||
#if NET
|
||||
using var content = JsonContent.Create(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
|
||||
#else
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
#endif
|
||||
|
||||
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _options.Endpoint)
|
||||
{
|
||||
Content = content,
|
||||
Headers =
|
||||
{
|
||||
Accept = { s_applicationJsonMediaType, s_textEventStreamMediaType },
|
||||
|
|
@ -90,7 +73,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
|
||||
CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion);
|
||||
|
||||
var response = await _httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
var response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// We'll let the caller decide whether to throw or fall back given an unsuccessful response.
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
|
@ -192,7 +175,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
request.Headers.Accept.Add(s_textEventStreamMediaType);
|
||||
CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _connectionCts.Token).ConfigureAwait(false);
|
||||
using var response = await _httpClient.SendAsync(request, message: null, _connectionCts.Token).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
|
|
@ -261,7 +244,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
try
|
||||
{
|
||||
// Do not validate we get a successful status code, because server support for the DELETE request is optional
|
||||
(await _httpClient.SendAsync(deleteRequest, CancellationToken.None).ConfigureAwait(false)).Dispose();
|
||||
(await _httpClient.SendAsync(deleteRequest, message: null, CancellationToken.None).ConfigureAwait(false)).Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue