Add support for message-level filters to McpServer (#1207)
This commit is contained in:
parent
6224f30a33
commit
7d48d3922c
12 changed files with 1333 additions and 105 deletions
|
|
@ -260,9 +260,6 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
// Get auth server metadata
|
||||
var authServerMetadata = await GetAuthServerMetadataAsync(selectedAuthServer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Store auth server metadata for future refresh operations
|
||||
_authServerMetadata = authServerMetadata;
|
||||
|
||||
// The existing access token must be invalid to have resulted in a 401 response, but refresh might still work.
|
||||
var resourceUri = GetRequiredResourceUri(protectedResourceMetadata);
|
||||
|
||||
|
|
@ -296,6 +293,9 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
}
|
||||
}
|
||||
|
||||
// Store auth server metadata for future refresh operations
|
||||
_authServerMetadata = authServerMetadata;
|
||||
|
||||
// Perform the OAuth flow
|
||||
return await InitiateAuthorizationCodeFlowAsync(protectedResourceMetadata, authServerMetadata, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,15 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
RegisterHandlers(options, notificationHandlers, requestHandlers);
|
||||
|
||||
_sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger);
|
||||
_sessionHandler = new McpSessionHandler(
|
||||
isServer: false,
|
||||
transport,
|
||||
endpointName,
|
||||
requestHandlers,
|
||||
notificationHandlers,
|
||||
incomingMessageFilter: null,
|
||||
outgoingMessageFilter: null,
|
||||
_logger);
|
||||
}
|
||||
|
||||
private void RegisterHandlers(McpClientOptions options, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
|
||||
|
|
|
|||
10
src/ModelContextProtocol.Core/JsonRpcMessageFilter.cs
Normal file
10
src/ModelContextProtocol.Core/JsonRpcMessageFilter.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a filter that wraps the processing of incoming JSON-RPC messages.
|
||||
/// </summary>
|
||||
/// <param name="next">The next handler in the pipeline.</param>
|
||||
/// <returns>A wrapped handler that processes messages and optionally delegates to the next handler.</returns>
|
||||
internal delegate Func<JsonRpcMessage, CancellationToken, Task> JsonRpcMessageFilter(Func<JsonRpcMessage, CancellationToken, Task> next);
|
||||
|
|
@ -66,6 +66,8 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
private readonly ITransport _transport;
|
||||
private readonly RequestHandlers _requestHandlers;
|
||||
private readonly NotificationHandlers _notificationHandlers;
|
||||
private readonly JsonRpcMessageFilter _incomingMessageFilter;
|
||||
private readonly JsonRpcMessageFilter _outgoingMessageFilter;
|
||||
private readonly long _sessionStartingTimestamp = Stopwatch.GetTimestamp();
|
||||
|
||||
private readonly DistributedContextPropagator _propagator = DistributedContextPropagator.Current;
|
||||
|
|
@ -95,6 +97,8 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
/// <param name="endpointName">The name of the endpoint for logging and debug purposes.</param>
|
||||
/// <param name="requestHandlers">A collection of request handlers.</param>
|
||||
/// <param name="notificationHandlers">A collection of notification handlers.</param>
|
||||
/// <param name="incomingMessageFilter">A filter that wraps incoming message processing. Takes the next handler and returns a wrapped handler. If null, a passthrough filter is used.</param>
|
||||
/// <param name="outgoingMessageFilter">A filter that wraps outgoing message processing. Takes the next handler and returns a wrapped handler. If null, a passthrough filter is used.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public McpSessionHandler(
|
||||
bool isServer,
|
||||
|
|
@ -102,6 +106,8 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
string endpointName,
|
||||
RequestHandlers requestHandlers,
|
||||
NotificationHandlers notificationHandlers,
|
||||
JsonRpcMessageFilter? incomingMessageFilter,
|
||||
JsonRpcMessageFilter? outgoingMessageFilter,
|
||||
ILogger logger)
|
||||
{
|
||||
Throw.IfNull(transport);
|
||||
|
|
@ -120,7 +126,9 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
EndpointName = endpointName;
|
||||
_requestHandlers = requestHandlers;
|
||||
_notificationHandlers = notificationHandlers;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
_incomingMessageFilter = incomingMessageFilter ?? (next => next);
|
||||
_outgoingMessageFilter = outgoingMessageFilter ?? (next => next);
|
||||
_logger = logger;
|
||||
LogSessionCreated(EndpointName, _sessionId, _transportKind);
|
||||
}
|
||||
|
||||
|
|
@ -309,36 +317,14 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
AddTags(ref tags, activity, message, method, target);
|
||||
}
|
||||
|
||||
switch (message)
|
||||
await _incomingMessageFilter(async (msg, ct) =>
|
||||
{
|
||||
case JsonRpcRequest request:
|
||||
LogRequestHandlerCalled(EndpointName, request.Method);
|
||||
long requestStartingTimestamp = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
var result = await HandleRequest(request, cancellationToken).ConfigureAwait(false);
|
||||
LogRequestHandlerCompleted(EndpointName, request.Method, GetElapsed(requestStartingTimestamp).TotalMilliseconds);
|
||||
AddResponseTags(ref tags, activity, result, method);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogRequestHandlerException(EndpointName, request.Method, GetElapsed(requestStartingTimestamp).TotalMilliseconds, ex);
|
||||
throw;
|
||||
}
|
||||
break;
|
||||
|
||||
case JsonRpcNotification notification:
|
||||
await HandleNotification(notification, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case JsonRpcMessageWithId messageWithId:
|
||||
HandleMessageWithId(message, messageWithId);
|
||||
break;
|
||||
|
||||
default:
|
||||
LogEndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
|
||||
break;
|
||||
}
|
||||
var result = await HandleMessageCoreAsync(msg, ct).ConfigureAwait(false);
|
||||
if (addTags && result is not null)
|
||||
{
|
||||
AddResponseTags(ref tags, activity, result, method);
|
||||
}
|
||||
})(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (addTags)
|
||||
{
|
||||
|
|
@ -351,7 +337,40 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private async Task HandleNotification(JsonRpcNotification notification, CancellationToken cancellationToken)
|
||||
private async Task<JsonNode?> HandleMessageCoreAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case JsonRpcRequest request:
|
||||
LogRequestHandlerCalled(EndpointName, request.Method);
|
||||
long requestStartingTimestamp = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
var result = await HandleRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
LogRequestHandlerCompleted(EndpointName, request.Method, GetElapsed(requestStartingTimestamp).TotalMilliseconds);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogRequestHandlerException(EndpointName, request.Method, GetElapsed(requestStartingTimestamp).TotalMilliseconds, ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
case JsonRpcNotification notification:
|
||||
await HandleNotificationAsync(notification, cancellationToken).ConfigureAwait(false);
|
||||
return null;
|
||||
|
||||
case JsonRpcMessageWithId messageWithId:
|
||||
HandleMessageWithId(message, messageWithId);
|
||||
return null;
|
||||
|
||||
default:
|
||||
LogEndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleNotificationAsync(JsonRpcNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
// Special-case cancellation to cancel a pending operation. (We'll still subsequently invoke a user-specified handler if one exists.)
|
||||
if (notification.Method == NotificationMethods.CancelledNotification)
|
||||
|
|
@ -387,7 +406,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<JsonNode?> HandleRequest(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
private async Task<JsonNode?> HandleRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_requestHandlers.TryGetValue(request.Method, out var handler))
|
||||
{
|
||||
|
|
@ -586,26 +605,29 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
AddTags(ref tags, activity, message, method, target);
|
||||
}
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
await _outgoingMessageFilter(async (msg, ct) =>
|
||||
{
|
||||
LogSendingMessageSensitive(EndpointName, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage));
|
||||
}
|
||||
else
|
||||
{
|
||||
LogSendingMessage(EndpointName);
|
||||
}
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
LogSendingMessageSensitive(EndpointName, JsonSerializer.Serialize(msg, McpJsonUtilities.JsonContext.Default.JsonRpcMessage));
|
||||
}
|
||||
else
|
||||
{
|
||||
LogSendingMessage(EndpointName);
|
||||
}
|
||||
|
||||
await SendToRelatedTransportAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
await SendToRelatedTransportAsync(msg, ct).ConfigureAwait(false);
|
||||
|
||||
// If the sent notification was a cancellation notification, cancel the pending request's await, as either the
|
||||
// server won't be sending a response, or per the specification, the response should be ignored. There are inherent
|
||||
// race conditions here, so it's possible and allowed for the operation to complete before we get to this point.
|
||||
if (message is JsonRpcNotification { Method: NotificationMethods.CancelledNotification } notification &&
|
||||
GetCancelledNotificationParams(notification.Params) is CancelledNotificationParams cn &&
|
||||
_pendingRequests.TryRemove(cn.RequestId, out var tcs))
|
||||
{
|
||||
tcs.TrySetCanceled(default);
|
||||
}
|
||||
// If the sent notification was a cancellation notification, cancel the pending request's await, as either the
|
||||
// server won't be sending a response, or per the specification, the response should be ignored. There are inherent
|
||||
// race conditions here, so it's possible and allowed for the operation to complete before we get to this point.
|
||||
if (msg is JsonRpcNotification { Method: NotificationMethods.CancelledNotification } notification &&
|
||||
GetCancelledNotificationParams(notification.Params) is CancelledNotificationParams cn &&
|
||||
_pendingRequests.TryRemove(cn.RequestId, out var tcs))
|
||||
{
|
||||
tcs.TrySetCanceled(default);
|
||||
}
|
||||
})(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (addTags)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,4 +58,20 @@ public class JsonRpcMessageContext
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
public ClaimsPrincipal? User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property allows data to be flowed throughout the message processing pipeline,
|
||||
/// including from incoming message filters to request-specific filters and handlers.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When creating a <see cref="MessageContext"/> or <see cref="RequestContext{TParams}"/> for server-side
|
||||
/// processing, the Items dictionary from this context will be used, ensuring data set in message filters
|
||||
/// is available in request filters and handlers.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IDictionary<string, object?>? Items { get; set; }
|
||||
}
|
||||
|
|
|
|||
30
src/ModelContextProtocol.Core/Server/McpMessageFilter.cs
Normal file
30
src/ModelContextProtocol.Core/Server/McpMessageFilter.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate type for applying filters to JSON-RPC messages.
|
||||
/// </summary>
|
||||
/// <param name="next">The next message handler in the pipeline.</param>
|
||||
/// <returns>The next message handler wrapped with the filter.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Message filters allow you to intercept and process JSON-RPC messages before they reach
|
||||
/// their respective handlers (incoming) or before they are sent (outgoing). This is useful for implementing
|
||||
/// cross-cutting concerns that need to apply to all message types, such as logging, authentication, rate limiting,
|
||||
/// redaction, or request tracing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Filters are applied in the order they are registered, with the first registered filter being the outermost.
|
||||
/// Each filter receives the next handler in the pipeline and can choose to:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Call the next handler to continue processing (await next(context, cancellationToken))</description></item>
|
||||
/// <item><description>Skip the default handlers entirely by not calling next</description></item>
|
||||
/// <item><description>Perform operations before and/or after calling next</description></item>
|
||||
/// <item><description>Catch and handle exceptions from inner handlers</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For request-specific filters, use <see cref="McpRequestFilter{TParams, TResult}"/> instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public delegate McpMessageHandler McpMessageFilter(
|
||||
McpMessageHandler next);
|
||||
21
src/ModelContextProtocol.Core/Server/McpMessageHandler.cs
Normal file
21
src/ModelContextProtocol.Core/Server/McpMessageHandler.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate type for handling incoming JSON-RPC messages.
|
||||
/// </summary>
|
||||
/// <param name="context">The message context containing the JSON-RPC message and other metadata.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This delegate can handle any type of JSON-RPC message, including requests, notifications, responses, and errors.
|
||||
/// Use this for implementing cross-cutting concerns that need to intercept all message types,
|
||||
/// such as logging, authentication, rate limiting, or request tracing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For request-specific handling, use <see cref="McpRequestHandler{TParams, TResult}"/> instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public delegate Task McpMessageHandler(
|
||||
MessageContext context,
|
||||
CancellationToken cancellationToken);
|
||||
|
|
@ -11,6 +11,39 @@ namespace ModelContextProtocol.Server;
|
|||
/// </remarks>
|
||||
public sealed class McpServerFilters
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the filters for all incoming JSON-RPC messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These filters intercept all incoming JSON-RPC messages before they are processed by the server,
|
||||
/// including requests, notifications, responses, and errors. The filters can perform logging,
|
||||
/// authentication, rate limiting, or other cross-cutting concerns that apply to all message types.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Message filters are applied before request-specific filters. If a message filter does not call
|
||||
/// the next handler in the pipeline, the default handlers will not be executed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public List<McpMessageFilter> IncomingMessageFilters { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filters for all outgoing JSON-RPC messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These filters intercept all outgoing JSON-RPC messages before they are sent to the client,
|
||||
/// including responses, notifications, and errors. The filters can perform logging,
|
||||
/// redaction, auditing, or other cross-cutting concerns that apply to all message types.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If a message filter does not call the next handler in the pipeline, the message will not be sent.
|
||||
/// Filters may also call the next handler multiple times with different messages to emit additional
|
||||
/// server-to-client messages.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public List<McpMessageFilter> OutgoingMessageFilters { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filters for the list-tools handler pipeline.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -118,7 +118,17 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
}
|
||||
|
||||
// And initialize the session.
|
||||
_sessionHandler = new McpSessionHandler(isServer: true, _sessionTransport, _endpointName!, _requestHandlers, _notificationHandlers, _logger);
|
||||
var incomingMessageFilter = BuildMessageFilterPipeline(options.Filters.IncomingMessageFilters);
|
||||
var outgoingMessageFilter = BuildMessageFilterPipeline(options.Filters.OutgoingMessageFilters);
|
||||
_sessionHandler = new McpSessionHandler(
|
||||
isServer: true,
|
||||
_sessionTransport,
|
||||
_endpointName!,
|
||||
_requestHandlers,
|
||||
_notificationHandlers,
|
||||
incomingMessageFilter,
|
||||
outgoingMessageFilter,
|
||||
_logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -875,6 +885,39 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
return current;
|
||||
}
|
||||
|
||||
private JsonRpcMessageFilter BuildMessageFilterPipeline(List<McpMessageFilter> filters)
|
||||
{
|
||||
if (filters.Count == 0)
|
||||
{
|
||||
return next => next;
|
||||
}
|
||||
|
||||
return next =>
|
||||
{
|
||||
// Build the handler chain from the filters.
|
||||
// The innermost handler calls the provided 'next' delegate with the message from the context.
|
||||
McpMessageHandler baseHandler = async (context, cancellationToken) =>
|
||||
{
|
||||
await next(context.JsonRpcMessage, cancellationToken).ConfigureAwait(false);
|
||||
};
|
||||
|
||||
var current = baseHandler;
|
||||
for (int i = filters.Count - 1; i >= 0; i--)
|
||||
{
|
||||
current = filters[i](current);
|
||||
}
|
||||
|
||||
// Return the handler that creates a MessageContext and invokes the pipeline.
|
||||
return async (message, cancellationToken) =>
|
||||
{
|
||||
// Ensure message has a Context so Items can be shared through the pipeline
|
||||
message.Context ??= new();
|
||||
var context = new MessageContext(new DestinationBoundMcpServer(this, message.Context.RelatedTransport), message);
|
||||
await current(context, cancellationToken).ConfigureAwait(false);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateEndpointNameWithClientInfo()
|
||||
{
|
||||
if (ClientInfo is null)
|
||||
|
|
|
|||
108
src/ModelContextProtocol.Core/Server/MessageContext.cs
Normal file
108
src/ModelContextProtocol.Core/Server/MessageContext.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System.Security.Claims;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a context container that provides access to the server and resources for processing a JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="MessageContext"/> encapsulates contextual information for handling any JSON-RPC message,
|
||||
/// including requests, responses, notifications, and errors. This is the base class for
|
||||
/// <see cref="RequestContext{TParams}"/>, which adds request-specific properties.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This type is typically received as a parameter in message filter delegates registered via
|
||||
/// <see cref="McpServerFilters.IncomingMessageFilters"/> or <see cref="McpServerFilters.OutgoingMessageFilters"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class MessageContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageContext"/> class with the specified server and JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <param name="server">The server with which this instance is associated.</param>
|
||||
/// <param name="jsonRpcMessage">The JSON-RPC message associated with this context.</param>
|
||||
public MessageContext(McpServer server, JsonRpcMessage jsonRpcMessage)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
Throw.IfNull(jsonRpcMessage);
|
||||
|
||||
Server = server;
|
||||
JsonRpcMessage = jsonRpcMessage;
|
||||
Services = server.Services;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the server with which this instance is associated.</summary>
|
||||
public McpServer Server
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
Throw.IfNull(value);
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This dictionary is shared with the <see cref="Protocol.JsonRpcMessageContext.Items"/> property
|
||||
/// on the underlying <see cref="JsonRpcMessage"/>, ensuring that data set in message filters
|
||||
/// flows through to request-specific filters and handlers.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IDictionary<string, object?> Items
|
||||
{
|
||||
get
|
||||
{
|
||||
JsonRpcMessage.Context ??= new();
|
||||
return JsonRpcMessage.Context.Items ??= new Dictionary<string, object?>();
|
||||
}
|
||||
set
|
||||
{
|
||||
JsonRpcMessage.Context ??= new();
|
||||
JsonRpcMessage.Context.Items = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the services associated with this message.</summary>
|
||||
/// <remarks>
|
||||
/// This provider might not be the same instance stored in <see cref="McpServer.Services"/>
|
||||
/// if <see cref="McpServerOptions.ScopeRequests"/> was true, in which case this
|
||||
/// might be a scoped <see cref="IServiceProvider"/> derived from the server's
|
||||
/// <see cref="McpServer.Services"/>.
|
||||
/// </remarks>
|
||||
public IServiceProvider? Services { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the user associated with this message.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property is backed by the <see cref="Protocol.JsonRpcMessageContext.User"/> property
|
||||
/// on the underlying <see cref="JsonRpcMessage"/>, ensuring that user information set in message filters
|
||||
/// flows through to request-specific filters and handlers.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ClaimsPrincipal? User
|
||||
{
|
||||
get => JsonRpcMessage.Context?.User;
|
||||
set
|
||||
{
|
||||
JsonRpcMessage.Context ??= new();
|
||||
JsonRpcMessage.Context.User = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON-RPC message associated with this context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property provides access to the complete JSON-RPC message,
|
||||
/// including the method name (for requests/notifications), request ID (for requests/responses),
|
||||
/// and associated transport and user information.
|
||||
/// </remarks>
|
||||
public JsonRpcMessage JsonRpcMessage { get; set; }
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Security.Claims;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
|
@ -12,11 +11,8 @@ namespace ModelContextProtocol.Server;
|
|||
/// This type is typically received as a parameter in handler delegates registered with IMcpServerBuilder,
|
||||
/// and can be injected as parameters into <see cref="McpServerTool"/>s.
|
||||
/// </remarks>
|
||||
public sealed class RequestContext<TParams>
|
||||
public sealed class RequestContext<TParams> : MessageContext
|
||||
{
|
||||
/// <summary>The server with which this instance is associated.</summary>
|
||||
private McpServer _server;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequestContext{TParams}"/> class with the specified server and JSON-RPC request.
|
||||
/// </summary>
|
||||
|
|
@ -24,48 +20,10 @@ public sealed class RequestContext<TParams>
|
|||
/// <param name="jsonRpcRequest">The JSON-RPC request associated with this context.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> or <paramref name="jsonRpcRequest"/> is <see langword="null"/>.</exception>
|
||||
public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest)
|
||||
: base(server, jsonRpcRequest)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
Throw.IfNull(jsonRpcRequest);
|
||||
|
||||
_server = server;
|
||||
JsonRpcRequest = jsonRpcRequest;
|
||||
Services = server.Services;
|
||||
User = jsonRpcRequest.Context?.User;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the server with which this instance is associated.</summary>
|
||||
public McpServer Server
|
||||
{
|
||||
get => _server;
|
||||
set
|
||||
{
|
||||
Throw.IfNull(value);
|
||||
_server = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this request.
|
||||
/// </summary>
|
||||
public IDictionary<string, object?> Items
|
||||
{
|
||||
get => field ??= new Dictionary<string, object?>();
|
||||
set => field = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the services associated with this request.</summary>
|
||||
/// <remarks>
|
||||
/// This provider might not be the same instance stored in <see cref="McpServer.Services"/>
|
||||
/// if <see cref="McpServerOptions.ScopeRequests"/> was true, in which case this
|
||||
/// might be a scoped <see cref="IServiceProvider"/> derived from the server's
|
||||
/// <see cref="McpServer.Services"/>.
|
||||
/// </remarks>
|
||||
public IServiceProvider? Services { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the user associated with this request.</summary>
|
||||
public ClaimsPrincipal? User { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the parameters associated with this request.</summary>
|
||||
public TParams? Params { get; set; }
|
||||
|
||||
|
|
@ -81,7 +39,11 @@ public sealed class RequestContext<TParams>
|
|||
/// This property provides access to the complete JSON-RPC request that initiated this handler invocation,
|
||||
/// including the method name, parameters, request ID, and associated transport and user information.
|
||||
/// </remarks>
|
||||
public JsonRpcRequest JsonRpcRequest { get; }
|
||||
public JsonRpcRequest JsonRpcRequest
|
||||
{
|
||||
get => (JsonRpcRequest)JsonRpcMessage;
|
||||
set => JsonRpcMessage = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the current response and enables polling for updates from the server.
|
||||
|
|
|
|||
|
|
@ -41,9 +41,12 @@ internal sealed partial class StreamableHttpPostTransport(
|
|||
{
|
||||
Debug.Assert(_pendingRequest.Id is null);
|
||||
|
||||
message.Context ??= new JsonRpcMessageContext();
|
||||
|
||||
if (message is JsonRpcRequest request)
|
||||
{
|
||||
_pendingRequest = request.Id;
|
||||
message.Context.RelatedTransport = this;
|
||||
|
||||
// Invoke the initialize request handler if applicable.
|
||||
if (request.Method == RequestMethods.Initialize)
|
||||
|
|
@ -53,9 +56,6 @@ internal sealed partial class StreamableHttpPostTransport(
|
|||
}
|
||||
}
|
||||
|
||||
message.Context ??= new JsonRpcMessageContext();
|
||||
message.Context.RelatedTransport = this;
|
||||
|
||||
if (parentTransport.FlowExecutionContextFromRequests)
|
||||
{
|
||||
message.Context.ExecutionContext = ExecutionContext.Capture();
|
||||
|
|
|
|||
Loading…
Reference in a new issue