C# Implementation (#2)

C# implementation of SEP-1763

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Peder Holdgaard Pedersen 2026-01-28 00:53:44 +01:00 • committed by GitHub
commit 45f9e0ef27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 13862 additions and 21 deletions

View file

@ -0,0 +1,166 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Specifies the types of members that are dynamically accessed.
///
/// This enumeration has a <see cref="FlagsAttribute"/> attribute that allows a
/// bitwise combination of its member values.
/// </summary>
[Flags]
internal enum DynamicallyAccessedMemberTypes
{
/// <summary>
/// Specifies no members.
/// </summary>
None = 0,
/// <summary>
/// Specifies the default, parameterless public constructor.
/// </summary>
PublicParameterlessConstructor = 0x0001,
/// <summary>
/// Specifies all public constructors.
/// </summary>
PublicConstructors = 0x0002 | PublicParameterlessConstructor,
/// <summary>
/// Specifies all non-public constructors.
/// </summary>
NonPublicConstructors = 0x0004,
/// <summary>
/// Specifies all public methods.
/// </summary>
PublicMethods = 0x0008,
/// <summary>
/// Specifies all non-public methods.
/// </summary>
NonPublicMethods = 0x0010,
/// <summary>
/// Specifies all public fields.
/// </summary>
PublicFields = 0x0020,
/// <summary>
/// Specifies all non-public fields.
/// </summary>
NonPublicFields = 0x0040,
/// <summary>
/// Specifies all public nested types.
/// </summary>
PublicNestedTypes = 0x0080,
/// <summary>
/// Specifies all non-public nested types.
/// </summary>
NonPublicNestedTypes = 0x0100,
/// <summary>
/// Specifies all public properties.
/// </summary>
PublicProperties = 0x0200,
/// <summary>
/// Specifies all non-public properties.
/// </summary>
NonPublicProperties = 0x0400,
/// <summary>
/// Specifies all public events.
/// </summary>
PublicEvents = 0x0800,
/// <summary>
/// Specifies all non-public events.
/// </summary>
NonPublicEvents = 0x1000,
/// <summary>
/// Specifies all interfaces implemented by the type.
/// </summary>
Interfaces = 0x2000,
/// <summary>
/// Specifies all non-public constructors, including those inherited from base classes.
/// </summary>
NonPublicConstructorsWithInherited = NonPublicConstructors | 0x4000,
/// <summary>
/// Specifies all non-public methods, including those inherited from base classes.
/// </summary>
NonPublicMethodsWithInherited = NonPublicMethods | 0x8000,
/// <summary>
/// Specifies all non-public fields, including those inherited from base classes.
/// </summary>
NonPublicFieldsWithInherited = NonPublicFields | 0x10000,
/// <summary>
/// Specifies all non-public nested types, including those inherited from base classes.
/// </summary>
NonPublicNestedTypesWithInherited = NonPublicNestedTypes | 0x20000,
/// <summary>
/// Specifies all non-public properties, including those inherited from base classes.
/// </summary>
NonPublicPropertiesWithInherited = NonPublicProperties | 0x40000,
/// <summary>
/// Specifies all non-public events, including those inherited from base classes.
/// </summary>
NonPublicEventsWithInherited = NonPublicEvents | 0x80000,
/// <summary>
/// Specifies all public constructors, including those inherited from base classes.
/// </summary>
PublicConstructorsWithInherited = PublicConstructors | 0x100000,
/// <summary>
/// Specifies all public nested types, including those inherited from base classes.
/// </summary>
PublicNestedTypesWithInherited = PublicNestedTypes | 0x200000,
/// <summary>
/// Specifies all constructors, including those inherited from base classes.
/// </summary>
AllConstructors = PublicConstructorsWithInherited | NonPublicConstructorsWithInherited,
/// <summary>
/// Specifies all methods, including those inherited from base classes.
/// </summary>
AllMethods = PublicMethods | NonPublicMethodsWithInherited,
/// <summary>
/// Specifies all fields, including those inherited from base classes.
/// </summary>
AllFields = PublicFields | NonPublicFieldsWithInherited,
/// <summary>
/// Specifies all nested types, including those inherited from base classes.
/// </summary>
AllNestedTypes = PublicNestedTypesWithInherited | NonPublicNestedTypesWithInherited,
/// <summary>
/// Specifies all properties, including those inherited from base classes.
/// </summary>
AllProperties = PublicProperties | NonPublicPropertiesWithInherited,
/// <summary>
/// Specifies all events, including those inherited from base classes.
/// </summary>
AllEvents = PublicEvents | NonPublicEventsWithInherited,
/// <summary>
/// Specifies all members.
/// </summary>
All = ~None
}
#endif

View file

@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that certain members on a specified <see cref="Type"/> are accessed dynamically,
/// for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which members are being accessed during the execution
/// of a program.
///
/// This attribute is valid on members whose type is <see cref="Type"/> or <see cref="string"/>.
///
/// When this attribute is applied to a location of type <see cref="string"/>, the assumption is
/// that the string represents a fully qualified type name.
///
/// When this attribute is applied to a class, interface, or struct, the members specified
/// can be accessed dynamically on <see cref="Type"/> instances returned from calling
/// <see cref="object.GetType"/> on instances of that class, interface, or struct.
///
/// If the attribute is applied to a method it's treated as a special case and it implies
/// the attribute should be applied to the "this" parameter of the method. As such the attribute
/// should only be used on instance methods of types assignable to System.Type (or string, but no methods
/// will use it there).
/// </remarks>
[AttributeUsage(
AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter |
AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method |
AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
Inherited = false)]
internal sealed class DynamicallyAccessedMembersAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DynamicallyAccessedMembersAttribute"/> class
/// with the specified member types.
/// </summary>
/// <param name="memberTypes">The types of members dynamically accessed.</param>
public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
{
MemberTypes = memberTypes;
}
/// <summary>
/// Gets the <see cref="DynamicallyAccessedMemberTypes"/> which specifies the type
/// of members dynamically accessed.
/// </summary>
public DynamicallyAccessedMemberTypes MemberTypes { get; }
}
#endif

View file

@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
namespace System.Diagnostics.CodeAnalysis
{
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class AllowNullAttribute : Attribute;
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class DisallowNullAttribute : Attribute;
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class MaybeNullAttribute : Attribute;
/// <summary>Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class NotNullAttribute : Attribute;
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter may be null.
/// </param>
public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
internal sealed class NotNullIfNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with the associated parameter name.</summary>
/// <param name="parameterName">
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
/// </param>
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
/// <summary>Gets the associated parameter name.</summary>
public string ParameterName { get; }
}
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute;
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified parameter value.</summary>
/// <param name="parameterValue">
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
/// the associated parameter matches this value.
/// </param>
public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
/// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; }
}
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullAttribute(string member) => Members = [member];
/// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullAttribute(params string[] members) => Members = members;
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.</summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated field or property member will not be null.
/// </param>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = [member];
}
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated field and property members will not be null.
/// </param>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
}
#endif

View file

@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that the specified method requires dynamic access to code that is not referenced
/// statically, for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when removing unreferenced
/// code from an application.
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
internal sealed class RequiresUnreferencedCodeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresUnreferencedCodeAttribute"/> class
/// with the specified message.
/// </summary>
/// <param name="message">
/// A message that contains information about the usage of unreferenced code.
/// </param>
public RequiresUnreferencedCodeAttribute(string message)
{
Message = message;
}
/// <summary>
/// Gets a message that contains information about the usage of unreferenced code.
/// </summary>
public string Message { get; }
/// <summary>
/// Gets or sets an optional URL that contains more information about the method,
/// why it requires unreferenced code, and what options a consumer has to deal with it.
/// </summary>
public string? Url { get; set; }
}
#endif

View file

@ -0,0 +1,7 @@
#if !NET
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
internal sealed class SetsRequiredMembersAttribute : Attribute;
}
#endif

View file

@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}
public string ParameterName { get; }
}
#endif

View file

@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied.
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
internal sealed class CompilerFeatureRequiredAttribute : Attribute
{
public CompilerFeatureRequiredAttribute(string featureName)
{
FeatureName = featureName;
}
/// <summary>
/// The name of the compiler feature.
/// </summary>
public string FeatureName { get; }
/// <summary>
/// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand <see cref="FeatureName"/>.
/// </summary>
public bool IsOptional { get; init; }
/// <summary>
/// The <see cref="FeatureName"/> used for the required members C# feature.
/// </summary>
public const string RequiredMembers = nameof(RequiredMembers);
}
}
#endif

View file

@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit;
}
#else
// The compiler emits a reference to the internal copy of this type in the non-.NET builds,
// so we must include a forward to be compatible.
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
#endif

View file

@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if !NET
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <summary>Specifies that a type has required members or that a member is required.</summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
[EditorBrowsable(EditorBrowsableState.Never)]
internal sealed class RequiredMemberAttribute : Attribute;
}
#endif

View file

@ -0,0 +1,70 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ModelContextProtocol.Interceptors;
/// <summary>Provides helper methods for throwing exceptions.</summary>
internal static class Throw
{
/// <summary>
/// Throws an <see cref="ArgumentNullException"/> if <paramref name="arg"/> is <see langword="null"/>.
/// </summary>
/// <param name="arg">The argument to validate.</param>
/// <param name="parameterName">The name of the parameter (automatically populated by the compiler).</param>
/// <exception cref="ArgumentNullException"><paramref name="arg"/> is <see langword="null"/>.</exception>
public static void IfNull([NotNull] object? arg, [CallerArgumentExpression(nameof(arg))] string? parameterName = null)
{
if (arg is null)
{
ThrowArgumentNullException(parameterName);
}
}
/// <summary>
/// Throws an <see cref="ArgumentException"/> if <paramref name="arg"/> is <see langword="null"/>, empty, or whitespace.
/// </summary>
/// <param name="arg">The string argument to validate.</param>
/// <param name="parameterName">The name of the parameter (automatically populated by the compiler).</param>
/// <exception cref="ArgumentNullException"><paramref name="arg"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="arg"/> is empty or whitespace.</exception>
public static void IfNullOrWhiteSpace([NotNull] string? arg, [CallerArgumentExpression(nameof(arg))] string? parameterName = null)
{
if (string.IsNullOrWhiteSpace(arg))
{
ThrowArgumentNullOrWhiteSpaceException(arg, parameterName);
}
}
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> if <paramref name="arg"/> is negative.
/// </summary>
/// <param name="arg">The value to validate.</param>
/// <param name="parameterName">The name of the parameter (automatically populated by the compiler).</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="arg"/> is negative.</exception>
public static void IfNegative(int arg, [CallerArgumentExpression(nameof(arg))] string? parameterName = null)
{
if (arg < 0)
{
ThrowArgumentOutOfRangeException(parameterName);
}
}
[DoesNotReturn]
private static void ThrowArgumentNullException(string? parameterName) =>
throw new ArgumentNullException(parameterName);
[DoesNotReturn]
private static void ThrowArgumentNullOrWhiteSpaceException(string? arg, string? parameterName)
{
if (arg is null)
{
throw new ArgumentNullException(parameterName);
}
throw new ArgumentException("Value cannot be empty or composed entirely of whitespace.", parameterName);
}
[DoesNotReturn]
private static void ThrowArgumentOutOfRangeException(string? parameterName) =>
throw new ArgumentOutOfRangeException(parameterName, "Value must not be negative.");
}

View file

@ -0,0 +1,66 @@
using ModelContextProtocol.Client;
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Provides a context container for client-side interceptor invocations.
/// </summary>
/// <typeparam name="TParams">Type of the request parameters.</typeparam>
/// <remarks>
/// <para>
/// The <see cref="ClientInterceptorContext{TParams}"/> encapsulates all contextual information for
/// invoking a client-side interceptor. Unlike server-side <c>RequestContext</c>, this context is
/// designed for intercepting outgoing requests and incoming responses at the client level.
/// </para>
/// <para>
/// Client interceptors operate at trust boundaries when:
/// <list type="bullet">
/// <item>Sending requests to servers (request phase)</item>
/// <item>Receiving responses from servers (response phase)</item>
/// </list>
/// </para>
/// </remarks>
public sealed class ClientInterceptorContext<TParams>
{
/// <summary>
/// Initializes a new instance of the <see cref="ClientInterceptorContext{TParams}"/> class.
/// </summary>
/// <param name="client">The MCP client associated with this context, if any.</param>
public ClientInterceptorContext(McpClient? client = null)
{
Client = client;
}
/// <summary>
/// Gets or sets the MCP client associated with this context.
/// </summary>
/// <remarks>
/// May be null for interceptors that operate independently of a specific client session.
/// </remarks>
public McpClient? Client { get; set; }
/// <summary>
/// Gets or sets the services associated with this invocation.
/// </summary>
public IServiceProvider? Services { get; set; }
/// <summary>
/// Gets or sets the parameters for this interceptor invocation.
/// </summary>
public TParams? Params { get; set; }
/// <summary>
/// Gets or sets a key/value collection for sharing data within the scope of this invocation.
/// </summary>
public IDictionary<string, object?> Items
{
get => _items ??= new Dictionary<string, object?>();
set => _items = value;
}
private IDictionary<string, object?>? _items;
/// <summary>
/// Gets or sets the matched interceptor primitive, if any.
/// </summary>
public McpClientInterceptor? MatchedInterceptor { get; set; }
}

View file

@ -0,0 +1,516 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using ModelContextProtocol.Client;
using ModelContextProtocol.Interceptors.Client;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Interceptors;
/// <summary>
/// Wraps an <see cref="McpClient"/> and automatically executes interceptor chains for tool operations.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="InterceptingMcpClient"/> provides a decorator pattern implementation that wraps an
/// existing <see cref="McpClient"/> and intercepts tool-related operations (CallToolAsync
/// and <see cref="ListToolsAsync"/>) to execute validation, mutation, and observability interceptors
/// according to the SEP-1763 specification.
/// </para>
/// <para>
/// <b>Execution Model (SEP-1763):</b>
/// </para>
/// <list type="bullet">
/// <item><description><b>Sending (outgoing request):</b> Mutations execute sequentially by priority, then validations and observability execute in parallel.</description></item>
/// <item><description><b>Receiving (incoming response):</b> Validations and observability execute in parallel, then mutations execute sequentially by priority.</description></item>
/// </list>
/// <para>
/// Only validation interceptors with <see cref="ValidationSeverity.Error"/> severity can block execution.
/// Info and warning severities are recorded but do not prevent the operation from proceeding.
/// </para>
/// </remarks>
/// <example>
/// Using InterceptingMcpClient with interceptors:
/// <code>
/// // Create MCP client normally
/// await using var client = await McpClient.CreateAsync(transport);
///
/// // Wrap with interceptors using extension method
/// var interceptedClient = client.WithInterceptors(new InterceptingMcpClientOptions
/// {
/// Interceptors =
/// [
/// McpClientInterceptor.Create(
/// name: "pii-validator",
/// events: [InterceptorEvents.ToolsCall],
/// type: InterceptorType.Validation,
/// handler: (ctx, ct) =&gt;
/// {
/// // Validate no PII in arguments
/// return ValueTask.FromResult(ValidationInterceptorResult.Success());
/// })
/// ]
/// });
///
/// // Use intercepted client - interceptors run automatically
/// try
/// {
/// var result = await interceptedClient.CallToolAsync("my-tool", args);
/// }
/// catch (McpInterceptorValidationException ex)
/// {
/// Console.WriteLine($"Blocked by: {ex.AbortedAt?.Interceptor}");
/// }
/// </code>
/// </example>
public sealed class InterceptingMcpClient : IAsyncDisposable
{
private readonly McpClient _inner;
private readonly InterceptorChainExecutor _executor;
private readonly InterceptingMcpClientOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="InterceptingMcpClient"/> class.
/// </summary>
/// <param name="inner">The underlying <see cref="McpClient"/> to wrap.</param>
/// <param name="options">Configuration options including interceptors and settings.</param>
/// <exception cref="ArgumentNullException"><paramref name="inner"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public InterceptingMcpClient(McpClient inner, InterceptingMcpClientOptions options)
{
Throw.IfNull(inner);
Throw.IfNull(options);
_inner = inner;
_options = options;
_executor = new InterceptorChainExecutor(
options.Interceptors,
options.Services);
}
/// <summary>
/// Gets the underlying <see cref="McpClient"/> instance.
/// </summary>
/// <remarks>
/// Use this property to access non-intercepted operations or properties from the underlying client,
/// such as prompts, resources, or other MCP features not currently supported by interception.
/// </remarks>
public McpClient Inner => _inner;
/// <summary>
/// Gets the capabilities supported by the connected server.
/// </summary>
public ServerCapabilities ServerCapabilities => _inner.ServerCapabilities;
/// <summary>
/// Gets the implementation information of the connected server.
/// </summary>
public Implementation ServerInfo => _inner.ServerInfo;
/// <summary>
/// Gets any instructions describing how to use the connected server and its features.
/// </summary>
public string? ServerInstructions => _inner.ServerInstructions;
/// <summary>
/// Gets the configuration options for this intercepting client.
/// </summary>
public InterceptingMcpClientOptions Options => _options;
#region CallToolAsync
/// <summary>
/// Invokes a tool on the server with interceptor chain execution.
/// </summary>
/// <param name="toolName">The name of the tool to call on the server.</param>
/// <param name="arguments">An optional dictionary of arguments to pass to the tool.</param>
/// <param name="progress">An optional progress reporter for server notifications.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>The <see cref="CallToolResult"/> from the tool execution.</returns>
/// <exception cref="ArgumentNullException"><paramref name="toolName"/> is <see langword="null"/>.</exception>
/// <exception cref="McpInterceptorValidationException">A validation interceptor returned error severity and <see cref="InterceptingMcpClientOptions.ThrowOnValidationError"/> is <see langword="true"/>.</exception>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// <para>
/// This method executes the interceptor chain in two phases:
/// </para>
/// <list type="number">
/// <item><description><b>Request interception:</b> Before sending the request to the server, interceptors run according to the sending order (mutations → validations/observability in parallel).</description></item>
/// <item><description><b>Response interception:</b> After receiving the response, interceptors run according to the receiving order (validations/observability in parallel → mutations).</description></item>
/// </list>
/// <para>
/// If a validation interceptor fails with error severity during request interception, the request
/// is not sent to the server and <see cref="McpInterceptorValidationException"/> is thrown.
/// </para>
/// </remarks>
public async ValueTask<CallToolResult> CallToolAsync(
string toolName,
IReadOnlyDictionary<string, object?>? arguments = null,
IProgress<ProgressNotificationValue>? progress = null,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
Throw.IfNull(toolName);
// Phase 1: Intercept outgoing request
var requestPayload = PayloadConverter.ToCallToolRequestPayload(toolName, arguments);
var sendingResult = await _executor.ExecuteForSendingAsync(
InterceptorEvents.ToolsCall,
requestPayload,
_options.DefaultConfig,
_options.DefaultTimeoutMs,
cancellationToken).ConfigureAwait(false);
// Check if validation failed
if (sendingResult.Status == InterceptorChainStatus.ValidationFailed)
{
if (_options.ThrowOnValidationError)
{
throw new McpInterceptorValidationException(
$"Interceptor validation failed for tools/call '{toolName}': {sendingResult.AbortedAt?.Reason ?? "Unknown reason"}",
sendingResult);
}
// Return an error result if not throwing
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = sendingResult.AbortedAt?.Reason ?? "Validation failed" }]
};
}
// Check for timeout or mutation failure
if (sendingResult.Status == InterceptorChainStatus.Timeout)
{
throw new McpInterceptorValidationException(
$"Interceptor chain timed out for tools/call '{toolName}'",
sendingResult);
}
if (sendingResult.Status == InterceptorChainStatus.MutationFailed)
{
throw new McpInterceptorValidationException(
$"Interceptor mutation failed for tools/call '{toolName}': {sendingResult.AbortedAt?.Reason ?? "Unknown reason"}",
sendingResult);
}
// Extract potentially mutated request parameters
var (mutatedToolName, mutatedArguments) = PayloadConverter.FromCallToolRequestPayload(sendingResult.FinalPayload);
// Phase 2: Call the underlying client
CallToolResult result;
if (progress is not null)
{
// Use the high-level overload which handles progress registration
result = await _inner.CallToolAsync(
mutatedToolName,
ConvertToObjectDictionary(mutatedArguments),
progress,
options,
cancellationToken).ConfigureAwait(false);
}
else
{
// Use the low-level overload for better performance
var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
result = await _inner.CallToolAsync(
new CallToolRequestParams
{
Name = mutatedToolName,
Arguments = mutatedArguments,
Meta = options?.GetMetaForRequest(),
},
cancellationToken).ConfigureAwait(false);
}
// Phase 3: Intercept incoming response (if enabled)
if (_options.InterceptResponses)
{
var responsePayload = PayloadConverter.ToCallToolResultPayload(result);
var receivingResult = await _executor.ExecuteForReceivingAsync(
InterceptorEvents.ToolsCall,
responsePayload,
_options.DefaultConfig,
_options.DefaultTimeoutMs,
cancellationToken).ConfigureAwait(false);
// Check if validation failed on response
if (receivingResult.Status == InterceptorChainStatus.ValidationFailed)
{
if (_options.ThrowOnValidationError)
{
throw new McpInterceptorValidationException(
$"Interceptor validation failed for tools/call response from '{toolName}': {receivingResult.AbortedAt?.Reason ?? "Unknown reason"}",
receivingResult);
}
// Return an error result if not throwing
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = receivingResult.AbortedAt?.Reason ?? "Response validation failed" }]
};
}
// Extract potentially mutated response
var mutatedResult = PayloadConverter.FromCallToolResultPayload(receivingResult.FinalPayload);
if (mutatedResult is not null)
{
result = mutatedResult;
}
}
return result;
}
/// <summary>
/// Invokes a tool on the server with interceptor chain execution.
/// </summary>
/// <param name="requestParams">The request parameters to send in the request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>The result of the request.</returns>
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
/// <exception cref="McpInterceptorValidationException">A validation interceptor returned error severity.</exception>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
public async ValueTask<CallToolResult> CallToolAsync(
CallToolRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
// Phase 1: Intercept outgoing request
var requestPayload = PayloadConverter.ToCallToolRequestParamsPayload(requestParams);
var sendingResult = await _executor.ExecuteForSendingAsync(
InterceptorEvents.ToolsCall,
requestPayload,
_options.DefaultConfig,
_options.DefaultTimeoutMs,
cancellationToken).ConfigureAwait(false);
// Check if validation failed
if (sendingResult.Status == InterceptorChainStatus.ValidationFailed)
{
if (_options.ThrowOnValidationError)
{
throw new McpInterceptorValidationException(
$"Interceptor validation failed for tools/call '{requestParams.Name}': {sendingResult.AbortedAt?.Reason ?? "Unknown reason"}",
sendingResult);
}
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = sendingResult.AbortedAt?.Reason ?? "Validation failed" }]
};
}
if (sendingResult.Status != InterceptorChainStatus.Success)
{
throw new McpInterceptorValidationException(
$"Interceptor chain failed for tools/call '{requestParams.Name}': {sendingResult.AbortedAt?.Reason ?? sendingResult.Status.ToString()}",
sendingResult);
}
// Extract potentially mutated request parameters
var mutatedParams = PayloadConverter.FromCallToolRequestParamsPayload(sendingResult.FinalPayload)
?? requestParams;
// Phase 2: Call the underlying client
var result = await _inner.CallToolAsync(mutatedParams, cancellationToken).ConfigureAwait(false);
// Phase 3: Intercept incoming response (if enabled)
if (_options.InterceptResponses)
{
var responsePayload = PayloadConverter.ToCallToolResultPayload(result);
var receivingResult = await _executor.ExecuteForReceivingAsync(
InterceptorEvents.ToolsCall,
responsePayload,
_options.DefaultConfig,
_options.DefaultTimeoutMs,
cancellationToken).ConfigureAwait(false);
if (receivingResult.Status == InterceptorChainStatus.ValidationFailed)
{
if (_options.ThrowOnValidationError)
{
throw new McpInterceptorValidationException(
$"Interceptor validation failed for tools/call response: {receivingResult.AbortedAt?.Reason ?? "Unknown reason"}",
receivingResult);
}
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = receivingResult.AbortedAt?.Reason ?? "Response validation failed" }]
};
}
var mutatedResult = PayloadConverter.FromCallToolResultPayload(receivingResult.FinalPayload);
if (mutatedResult is not null)
{
result = mutatedResult;
}
}
return result;
}
#endregion
#region ListToolsAsync
/// <summary>
/// Retrieves a list of available tools from the server with interceptor chain execution.
/// </summary>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A list of all available tools as <see cref="McpClientTool"/> instances.</returns>
/// <exception cref="McpInterceptorValidationException">A validation interceptor returned error severity.</exception>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// <para>
/// This method handles pagination automatically, executing interceptors for each page of results.
/// The returned tools are associated with this <see cref="InterceptingMcpClient"/> instance (via the inner client),
/// so invoking them through their InvokeAsync method will NOT execute interceptors.
/// </para>
/// <para>
/// To ensure interceptors are executed for tool calls, use <see cref="CallToolAsync(string, IReadOnlyDictionary{string, object?}?, IProgress{ProgressNotificationValue}?, RequestOptions?, CancellationToken)"/>
/// directly on this <see cref="InterceptingMcpClient"/> instance instead of calling tools through <see cref="McpClientTool"/>.
/// </para>
/// </remarks>
public async ValueTask<IList<McpClientTool>> ListToolsAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
List<McpClientTool>? tools = null;
string? cursor = null;
do
{
// Phase 1: Intercept outgoing request
var requestPayload = PayloadConverter.ToListToolsRequestPayload(cursor);
var sendingResult = await _executor.ExecuteForSendingAsync(
InterceptorEvents.ToolsList,
requestPayload,
_options.DefaultConfig,
_options.DefaultTimeoutMs,
cancellationToken).ConfigureAwait(false);
if (sendingResult.Status == InterceptorChainStatus.ValidationFailed)
{
if (_options.ThrowOnValidationError)
{
throw new McpInterceptorValidationException(
$"Interceptor validation failed for tools/list: {sendingResult.AbortedAt?.Reason ?? "Unknown reason"}",
sendingResult);
}
// Return empty list if not throwing
return [];
}
if (sendingResult.Status != InterceptorChainStatus.Success)
{
throw new McpInterceptorValidationException(
$"Interceptor chain failed for tools/list: {sendingResult.AbortedAt?.Reason ?? sendingResult.Status.ToString()}",
sendingResult);
}
// Extract potentially mutated cursor
var mutatedCursor = PayloadConverter.FromListToolsRequestPayload(sendingResult.FinalPayload);
// Phase 2: Call the underlying client
var requestParams = new ListToolsRequestParams
{
Cursor = mutatedCursor,
Meta = options?.GetMetaForRequest()
};
var toolResults = await _inner.ListToolsAsync(requestParams, cancellationToken).ConfigureAwait(false);
// Phase 3: Intercept incoming response (if enabled)
if (_options.InterceptResponses)
{
var responsePayload = PayloadConverter.ToListToolsResultPayload(toolResults);
var receivingResult = await _executor.ExecuteForReceivingAsync(
InterceptorEvents.ToolsList,
responsePayload,
_options.DefaultConfig,
_options.DefaultTimeoutMs,
cancellationToken).ConfigureAwait(false);
if (receivingResult.Status == InterceptorChainStatus.ValidationFailed)
{
if (_options.ThrowOnValidationError)
{
throw new McpInterceptorValidationException(
$"Interceptor validation failed for tools/list response: {receivingResult.AbortedAt?.Reason ?? "Unknown reason"}",
receivingResult);
}
return tools ?? [];
}
var mutatedResults = PayloadConverter.FromListToolsResultPayload(receivingResult.FinalPayload);
if (mutatedResults is not null)
{
toolResults = mutatedResults;
}
}
// Add tools to the result list
tools ??= new(toolResults.Tools.Count);
foreach (var tool in toolResults.Tools)
{
// Note: Tools are associated with the inner client, not this intercepting wrapper
// This means calling tool.InvokeAsync() will bypass interceptors
tools.Add(new McpClientTool(_inner, tool, options?.JsonSerializerOptions));
}
cursor = toolResults.NextCursor;
}
while (cursor is not null);
return tools ?? [];
}
#endregion
#region IAsyncDisposable
/// <summary>
/// Disposes the underlying <see cref="McpClient"/> instance.
/// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public ValueTask DisposeAsync() => _inner.DisposeAsync();
#endregion
#region Helper Methods
/// <summary>
/// Converts a dictionary of JsonElement values to object values for compatibility with the high-level CallToolAsync overload.
/// </summary>
private static IReadOnlyDictionary<string, object?>? ConvertToObjectDictionary(Dictionary<string, JsonElement>? arguments)
{
if (arguments is null || arguments.Count == 0)
{
return null;
}
var result = new Dictionary<string, object?>(arguments.Count);
foreach (var kvp in arguments)
{
result[kvp.Key] = kvp.Value;
}
return result;
}
#endregion
}

View file

@ -0,0 +1,128 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Interceptors.Client;
namespace ModelContextProtocol.Interceptors;
/// <summary>
/// Provides extension methods for wrapping <see cref="McpClient"/> with interceptor support.
/// </summary>
public static class InterceptingMcpClientExtensions
{
/// <summary>
/// Wraps an <see cref="McpClient"/> with interceptor chain execution for tool operations.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> to wrap.</param>
/// <param name="options">Configuration options including interceptors and settings.</param>
/// <returns>An <see cref="InterceptingMcpClient"/> that executes interceptor chains for tool operations.</returns>
/// <exception cref="ArgumentNullException"><paramref name="client"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This extension method creates an <see cref="InterceptingMcpClient"/> that wraps the provided
/// <see cref="McpClient"/> and automatically executes interceptor chains for <c>tools/call</c>
/// and <c>tools/list</c> operations according to SEP-1763.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// await using var client = await McpClient.CreateAsync(transport);
///
/// var interceptedClient = client.WithInterceptors(new InterceptingMcpClientOptions
/// {
/// Interceptors =
/// [
/// McpClientInterceptor.Create(
/// name: "audit-logger",
/// events: [InterceptorEvents.ToolsCall],
/// type: InterceptorType.Observability,
/// handler: async (ctx, ct) =&gt;
/// {
/// await LogToolCallAsync(ctx.Params);
/// return ObservabilityInterceptorResult.Success();
/// })
/// ],
/// DefaultTimeoutMs = 5000
/// });
///
/// var result = await interceptedClient.CallToolAsync("my-tool", args);
/// </code>
/// </example>
public static InterceptingMcpClient WithInterceptors(
this McpClient client,
InterceptingMcpClientOptions options)
{
Throw.IfNull(client);
Throw.IfNull(options);
return new InterceptingMcpClient(client, options);
}
/// <summary>
/// Wraps an <see cref="McpClient"/> with interceptor chain execution using the specified interceptors.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> to wrap.</param>
/// <param name="interceptors">The interceptors to register.</param>
/// <returns>An <see cref="InterceptingMcpClient"/> that executes interceptor chains for tool operations.</returns>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This is a convenience overload that creates an <see cref="InterceptingMcpClient"/> with the
/// specified interceptors using default options (no timeout, throw on validation error, intercept responses).
/// </para>
/// </remarks>
/// <example>
/// <code>
/// await using var client = await McpClient.CreateAsync(transport);
///
/// var interceptedClient = client.WithInterceptors(
/// McpClientInterceptor.Create(
/// name: "rate-limiter",
/// events: [InterceptorEvents.ToolsCall],
/// type: InterceptorType.Validation,
/// handler: (ctx, ct) =&gt; ValueTask.FromResult(ValidationInterceptorResult.Success())),
/// McpClientInterceptor.Create(
/// name: "pii-filter",
/// events: [InterceptorEvents.ToolsCall],
/// type: InterceptorType.Validation,
/// handler: (ctx, ct) =&gt; ValueTask.FromResult(ValidatePii(ctx))));
/// </code>
/// </example>
public static InterceptingMcpClient WithInterceptors(
this McpClient client,
params McpClientInterceptor[] interceptors)
{
Throw.IfNull(client);
return new InterceptingMcpClient(client, new InterceptingMcpClientOptions
{
Interceptors = interceptors ?? []
});
}
/// <summary>
/// Wraps an <see cref="McpClient"/> with interceptor chain execution using the specified interceptors and service provider.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> to wrap.</param>
/// <param name="interceptors">The interceptors to register.</param>
/// <param name="services">The service provider for dependency injection in interceptors.</param>
/// <returns>An <see cref="InterceptingMcpClient"/> that executes interceptor chains for tool operations.</returns>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This overload allows you to provide a service provider for dependency injection support
/// in interceptors that need to resolve services.
/// </para>
/// </remarks>
public static InterceptingMcpClient WithInterceptors(
this McpClient client,
IEnumerable<McpClientInterceptor> interceptors,
IServiceProvider? services = null)
{
Throw.IfNull(client);
return new InterceptingMcpClient(client, new InterceptingMcpClientOptions
{
Interceptors = interceptors?.ToList() ?? [],
Services = services
});
}
}

View file

@ -0,0 +1,138 @@
using System.Text.Json.Nodes;
using ModelContextProtocol.Interceptors.Client;
namespace ModelContextProtocol.Interceptors;
/// <summary>
/// Configuration options for <see cref="InterceptingMcpClient"/>.
/// </summary>
/// <remarks>
/// <para>
/// Use these options to configure how the <see cref="InterceptingMcpClient"/> executes interceptor
/// chains for MCP tool operations. You can register interceptors, configure timeouts, and control
/// error handling behavior.
/// </para>
/// </remarks>
/// <example>
/// Creating options with interceptors:
/// <code>
/// var options = new InterceptingMcpClientOptions
/// {
/// Interceptors =
/// [
/// McpClientInterceptor.Create(
/// name: "logging",
/// events: [InterceptorEvents.ToolsCall],
/// type: InterceptorType.Observability,
/// handler: (ctx, ct) => { /* log */ })
/// ],
/// DefaultTimeoutMs = 5000,
/// ThrowOnValidationError = true
/// };
/// </code>
/// </example>
public sealed class InterceptingMcpClientOptions
{
/// <summary>
/// Gets or sets the collection of interceptors to execute for MCP operations.
/// </summary>
/// <remarks>
/// <para>
/// Interceptors are executed according to SEP-1763 ordering rules:
/// </para>
/// <list type="bullet">
/// <item><description><b>Sending (outgoing):</b> Mutations run sequentially by priority, then validations and observability run in parallel.</description></item>
/// <item><description><b>Receiving (incoming):</b> Validations and observability run in parallel, then mutations run sequentially by priority.</description></item>
/// </list>
/// <para>
/// Only interceptors whose <see cref="Interceptor.Events"/> match the current operation
/// (e.g., "tools/call") will be executed.
/// </para>
/// </remarks>
public IList<McpClientInterceptor> Interceptors { get; set; } = [];
/// <summary>
/// Gets or sets the service provider for dependency injection.
/// </summary>
/// <remarks>
/// If provided, this service provider will be passed to interceptors via the
/// <see cref="ClientInterceptorContext{TParams}.Services"/> property, allowing
/// interceptors to resolve dependencies.
/// </remarks>
public IServiceProvider? Services { get; set; }
/// <summary>
/// Gets or sets the default timeout in milliseconds for interceptor chain execution.
/// </summary>
/// <remarks>
/// <para>
/// If the interceptor chain takes longer than this timeout, execution will be aborted
/// and the chain result will have status <see cref="InterceptorChainStatus.Timeout"/>.
/// </para>
/// <para>
/// Set to <c>null</c> for no timeout (default). A reasonable production value might be
/// 5000-30000ms depending on interceptor complexity.
/// </para>
/// </remarks>
public int? DefaultTimeoutMs { get; set; }
/// <summary>
/// Gets or sets the default configuration passed to interceptors.
/// </summary>
/// <remarks>
/// <para>
/// This configuration dictionary is merged with any per-call configuration and passed
/// to interceptors. Keys should match interceptor names, and values should be JSON
/// objects containing interceptor-specific configuration.
/// </para>
/// </remarks>
/// <example>
/// Setting default interceptor configuration:
/// <code>
/// options.DefaultConfig = new Dictionary&lt;string, JsonNode&gt;
/// {
/// ["pii-filter"] = JsonNode.Parse("""{"sensitivity": "high", "regions": ["US", "EU"]}"""),
/// ["rate-limiter"] = JsonNode.Parse("""{"maxRequestsPerMinute": 100}""")
/// };
/// </code>
/// </example>
public IDictionary<string, JsonNode>? DefaultConfig { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to throw <see cref="McpInterceptorValidationException"/>
/// when a validation interceptor fails with error severity.
/// </summary>
/// <remarks>
/// <para>
/// When <c>true</c> (default), the <see cref="InterceptingMcpClient"/> will throw
/// <see cref="McpInterceptorValidationException"/> if any validation interceptor returns
/// <see cref="ValidationSeverity.Error"/>. The exception contains the full
/// <see cref="InterceptorChainResult"/> for inspection.
/// </para>
/// <para>
/// When <c>false</c>, validation errors will not throw exceptions. Instead, the operation
/// will proceed without calling the underlying MCP client, and the caller is responsible
/// for checking results. This mode is primarily useful for testing or scenarios where
/// you want to handle validation failures differently.
/// </para>
/// </remarks>
/// <value><c>true</c> to throw on validation errors; <c>false</c> to continue silently. Default is <c>true</c>.</value>
public bool ThrowOnValidationError { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether to execute interceptors for response/result payloads.
/// </summary>
/// <remarks>
/// <para>
/// When <c>true</c> (default), interceptors will be executed both when sending requests
/// (via <see cref="InterceptorChainExecutor.ExecuteForSendingAsync"/>) and when receiving
/// responses (via <see cref="InterceptorChainExecutor.ExecuteForReceivingAsync"/>).
/// </para>
/// <para>
/// When <c>false</c>, only request interception is performed. This can be useful when you
/// only need to validate/transform outgoing requests and want to minimize overhead on responses.
/// </para>
/// </remarks>
/// <value><c>true</c> to intercept responses; <c>false</c> to only intercept requests. Default is <c>true</c>.</value>
public bool InterceptResponses { get; set; } = true;
}

View file

@ -0,0 +1,397 @@
using System.Diagnostics;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Executes interceptor chains following the SEP-1763 execution model.
/// </summary>
/// <remarks>
/// <para>
/// The chain executor handles the ordering and execution of interceptors based on their type:
/// <list type="bullet">
/// <item><b>Mutations</b>: Executed sequentially by priority (lower first), alphabetically for ties</item>
/// <item><b>Validations</b>: Executed in parallel, errors block execution</item>
/// <item><b>Observability</b>: Fire-and-forget, executed in parallel, never block</item>
/// </list>
/// </para>
/// <para>
/// Execution order depends on data flow direction:
/// <list type="bullet">
/// <item><b>Sending</b>: Mutate → Validate &amp; Observe → Send</item>
/// <item><b>Receiving</b>: Receive → Validate &amp; Observe → Mutate</item>
/// </list>
/// </para>
/// </remarks>
public class InterceptorChainExecutor
{
private readonly IReadOnlyList<McpClientInterceptor> _interceptors;
private readonly IServiceProvider? _services;
/// <summary>
/// Initializes a new instance of the <see cref="InterceptorChainExecutor"/> class.
/// </summary>
/// <param name="interceptors">The interceptors to execute.</param>
/// <param name="services">Optional service provider for dependency injection.</param>
public InterceptorChainExecutor(IEnumerable<McpClientInterceptor> interceptors, IServiceProvider? services = null)
{
Throw.IfNull(interceptors);
_interceptors = interceptors.ToList();
_services = services;
}
/// <summary>
/// Executes the interceptor chain for outgoing data (sending across trust boundary).
/// </summary>
/// <param name="event">The event type being intercepted.</param>
/// <param name="payload">The payload to process.</param>
/// <param name="config">Optional per-interceptor configuration.</param>
/// <param name="timeoutMs">Optional timeout for the entire chain.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The chain execution result.</returns>
/// <remarks>
/// Execution order for sending: Mutate (sequential) → Validate &amp; Observe (parallel) → Return
/// </remarks>
public Task<InterceptorChainResult> ExecuteForSendingAsync(
string @event,
JsonNode? payload,
IDictionary<string, JsonNode>? config = null,
int? timeoutMs = null,
CancellationToken cancellationToken = default)
{
return ExecuteChainAsync(@event, InterceptorPhase.Request, payload, config, timeoutMs, isSending: true, cancellationToken);
}
/// <summary>
/// Executes the interceptor chain for incoming data (receiving from trust boundary).
/// </summary>
/// <param name="event">The event type being intercepted.</param>
/// <param name="payload">The payload to process.</param>
/// <param name="config">Optional per-interceptor configuration.</param>
/// <param name="timeoutMs">Optional timeout for the entire chain.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The chain execution result.</returns>
/// <remarks>
/// Execution order for receiving: Validate &amp; Observe (parallel) → Mutate (sequential) → Return
/// </remarks>
public Task<InterceptorChainResult> ExecuteForReceivingAsync(
string @event,
JsonNode? payload,
IDictionary<string, JsonNode>? config = null,
int? timeoutMs = null,
CancellationToken cancellationToken = default)
{
return ExecuteChainAsync(@event, InterceptorPhase.Response, payload, config, timeoutMs, isSending: false, cancellationToken);
}
private async Task<InterceptorChainResult> ExecuteChainAsync(
string @event,
InterceptorPhase phase,
JsonNode? payload,
IDictionary<string, JsonNode>? config,
int? timeoutMs,
bool isSending,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var result = new InterceptorChainResult
{
Event = @event,
Phase = phase,
Status = InterceptorChainStatus.Success
};
using var timeoutCts = timeoutMs.HasValue
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
: null;
if (timeoutCts is not null)
{
timeoutCts.CancelAfter(timeoutMs!.Value);
}
var effectiveCt = timeoutCts?.Token ?? cancellationToken;
try
{
// Get interceptors that handle this event and phase
var applicableInterceptors = GetApplicableInterceptors(@event, phase);
// Separate by type
var mutations = applicableInterceptors
.Where(i => i.ProtocolInterceptor.Type == InterceptorType.Mutation)
.OrderBy(i => GetPriority(i, phase))
.ThenBy(i => i.ProtocolInterceptor.Name)
.ToList();
var validations = applicableInterceptors
.Where(i => i.ProtocolInterceptor.Type == InterceptorType.Validation)
.ToList();
var observability = applicableInterceptors
.Where(i => i.ProtocolInterceptor.Type == InterceptorType.Observability)
.ToList();
JsonNode? currentPayload = payload;
if (isSending)
{
// Sending: Mutate → Validate & Observe
currentPayload = await ExecuteMutationsAsync(mutations, @event, phase, currentPayload, config, result, effectiveCt);
if (result.Status != InterceptorChainStatus.Success)
{
result.TotalDurationMs = stopwatch.ElapsedMilliseconds;
return result;
}
await ExecuteValidationsAndObservabilityAsync(validations, observability, @event, phase, currentPayload, config, result, effectiveCt);
}
else
{
// Receiving: Validate & Observe → Mutate
await ExecuteValidationsAndObservabilityAsync(validations, observability, @event, phase, currentPayload, config, result, effectiveCt);
if (result.Status != InterceptorChainStatus.Success)
{
result.TotalDurationMs = stopwatch.ElapsedMilliseconds;
return result;
}
currentPayload = await ExecuteMutationsAsync(mutations, @event, phase, currentPayload, config, result, effectiveCt);
}
result.FinalPayload = currentPayload;
}
catch (OperationCanceledException) when (timeoutCts?.IsCancellationRequested == true)
{
result.Status = InterceptorChainStatus.Timeout;
result.AbortedAt = new ChainAbortInfo
{
Interceptor = "chain",
Reason = "Chain execution timed out",
Type = "timeout"
};
}
result.TotalDurationMs = stopwatch.ElapsedMilliseconds;
return result;
}
private IEnumerable<McpClientInterceptor> GetApplicableInterceptors(string @event, InterceptorPhase phase)
{
return _interceptors.Where(i =>
{
var proto = i.ProtocolInterceptor;
// Check phase
if (proto.Phase != InterceptorPhase.Both && proto.Phase != phase)
{
return false;
}
// Check event
if (proto.Events.Count == 0)
{
return true; // No events specified means all events
}
return proto.Events.Any(e =>
e == @event ||
e == "*" ||
(e == "*/request" && phase == InterceptorPhase.Request) ||
(e == "*/response" && phase == InterceptorPhase.Response));
});
}
private static int GetPriority(McpClientInterceptor interceptor, InterceptorPhase phase)
{
var hint = interceptor.ProtocolInterceptor.PriorityHint;
if (hint is null)
{
return 0;
}
return hint.Value.GetPriorityForPhase(phase);
}
private async Task<JsonNode?> ExecuteMutationsAsync(
List<McpClientInterceptor> mutations,
string @event,
InterceptorPhase phase,
JsonNode? payload,
IDictionary<string, JsonNode>? config,
InterceptorChainResult chainResult,
CancellationToken cancellationToken)
{
var currentPayload = payload;
foreach (var interceptor in mutations)
{
cancellationToken.ThrowIfCancellationRequested();
var context = CreateContext(interceptor, @event, phase, currentPayload, config);
try
{
var result = await interceptor.InvokeAsync(context, cancellationToken);
chainResult.Results.Add(result);
if (result is MutationInterceptorResult mutationResult)
{
if (mutationResult.Modified)
{
currentPayload = mutationResult.Payload;
}
}
}
catch (Exception ex)
{
chainResult.Status = InterceptorChainStatus.MutationFailed;
chainResult.AbortedAt = new ChainAbortInfo
{
Interceptor = interceptor.ProtocolInterceptor.Name,
Reason = ex.Message,
Type = "mutation"
};
chainResult.FinalPayload = currentPayload; // Return last valid state
return currentPayload;
}
}
return currentPayload;
}
private async Task ExecuteValidationsAndObservabilityAsync(
List<McpClientInterceptor> validations,
List<McpClientInterceptor> observability,
string @event,
InterceptorPhase phase,
JsonNode? payload,
IDictionary<string, JsonNode>? config,
InterceptorChainResult chainResult,
CancellationToken cancellationToken)
{
// Execute validations and observability in parallel
var allTasks = new List<Task<(McpClientInterceptor Interceptor, InterceptorResult Result, bool IsObservability)>>();
foreach (var interceptor in validations)
{
allTasks.Add(ExecuteInterceptorAsync(interceptor, @event, phase, payload, config, isObservability: false, cancellationToken));
}
foreach (var interceptor in observability)
{
allTasks.Add(ExecuteInterceptorAsync(interceptor, @event, phase, payload, config, isObservability: true, cancellationToken));
}
var results = await Task.WhenAll(allTasks);
foreach (var (interceptor, result, isObservability) in results)
{
chainResult.Results.Add(result);
if (result is ValidationInterceptorResult validationResult)
{
// Update validation summary
if (validationResult.Messages is not null)
{
foreach (var msg in validationResult.Messages)
{
switch (msg.Severity)
{
case ValidationSeverity.Error:
chainResult.ValidationSummary.Errors++;
break;
case ValidationSeverity.Warn:
chainResult.ValidationSummary.Warnings++;
break;
case ValidationSeverity.Info:
chainResult.ValidationSummary.Infos++;
break;
}
}
}
// Check for blocking errors
if (!validationResult.Valid && validationResult.Severity == ValidationSeverity.Error)
{
chainResult.Status = InterceptorChainStatus.ValidationFailed;
chainResult.AbortedAt = new ChainAbortInfo
{
Interceptor = interceptor.ProtocolInterceptor.Name,
Reason = validationResult.Messages?.FirstOrDefault()?.Message ?? "Validation failed",
Type = "validation"
};
}
}
// Observability failures are logged but never block (fire-and-forget behavior)
}
}
private async Task<(McpClientInterceptor Interceptor, InterceptorResult Result, bool IsObservability)> ExecuteInterceptorAsync(
McpClientInterceptor interceptor,
string @event,
InterceptorPhase phase,
JsonNode? payload,
IDictionary<string, JsonNode>? config,
bool isObservability,
CancellationToken cancellationToken)
{
var context = CreateContext(interceptor, @event, phase, payload, config);
try
{
var result = await interceptor.InvokeAsync(context, cancellationToken);
return (interceptor, result, isObservability);
}
catch (Exception ex)
{
// For observability, failures are logged but don't affect the result
if (isObservability)
{
return (interceptor, new ObservabilityInterceptorResult
{
Interceptor = interceptor.ProtocolInterceptor.Name,
Phase = phase,
Observed = false,
Info = new JsonObject { ["error"] = ex.Message }
}, true);
}
// For validations, return an error result
return (interceptor, new ValidationInterceptorResult
{
Interceptor = interceptor.ProtocolInterceptor.Name,
Phase = phase,
Valid = false,
Severity = ValidationSeverity.Error,
Messages = [new() { Message = ex.Message, Severity = ValidationSeverity.Error }]
}, false);
}
}
private ClientInterceptorContext<InvokeInterceptorRequestParams> CreateContext(
McpClientInterceptor interceptor,
string @event,
InterceptorPhase phase,
JsonNode? payload,
IDictionary<string, JsonNode>? config)
{
return new ClientInterceptorContext<InvokeInterceptorRequestParams>
{
Services = _services,
MatchedInterceptor = interceptor,
Params = new InvokeInterceptorRequestParams
{
Name = interceptor.ProtocolInterceptor.Name,
Event = @event,
Phase = phase,
Payload = payload!,
Config = config?.TryGetValue(interceptor.ProtocolInterceptor.Name, out var interceptorConfig) == true
? interceptorConfig
: null
}
};
}
}

View file

@ -0,0 +1,27 @@
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Contains filter delegates for client-side interceptor operations.
/// </summary>
/// <remarks>
/// <para>
/// Filters provide a middleware-like mechanism to wrap interceptor handler invocations,
/// allowing for cross-cutting concerns like logging, timing, or additional validation.
/// </para>
/// <para>
/// Filters are applied in the order they are added, with each filter wrapping the next
/// handler in the chain.
/// </para>
/// </remarks>
public class InterceptorClientFilters
{
/// <summary>
/// Gets the list of filters for the list interceptors handler.
/// </summary>
public List<Func<ListInterceptorsRequestParams?, Func<ListInterceptorsRequestParams?, CancellationToken, ValueTask<ListInterceptorsResult>>, CancellationToken, ValueTask<ListInterceptorsResult>>> ListInterceptorsFilters { get; } = [];
/// <summary>
/// Gets the list of filters for the invoke interceptor handler.
/// </summary>
public List<Func<InvokeInterceptorRequestParams, Func<InvokeInterceptorRequestParams, CancellationToken, ValueTask<InterceptorResult>>, CancellationToken, ValueTask<InterceptorResult>>> InvokeInterceptorFilters { get; } = [];
}

View file

@ -0,0 +1,27 @@
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Contains handler delegates for client-side interceptor operations.
/// </summary>
/// <remarks>
/// <para>
/// This class stores the handler functions that are invoked when clients need to
/// list available interceptors or invoke specific interceptors.
/// </para>
/// <para>
/// Handlers can be configured through the McpClientInterceptorExtensions
/// extension methods in the <c>Microsoft.Extensions.DependencyInjection</c> namespace.
/// </para>
/// </remarks>
public class InterceptorClientHandlers
{
/// <summary>
/// Gets or sets the handler for listing available interceptors.
/// </summary>
public Func<ListInterceptorsRequestParams?, CancellationToken, ValueTask<ListInterceptorsResult>>? ListInterceptorsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for invoking an interceptor.
/// </summary>
public Func<InvokeInterceptorRequestParams, CancellationToken, ValueTask<InterceptorResult>>? InvokeInterceptorHandler { get; set; }
}

View file

@ -0,0 +1,135 @@
using System.Reflection;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Represents an invocable interceptor used by Model Context Protocol clients.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="McpClientInterceptor"/> is an abstract base class that represents an MCP interceptor for use in the client
/// (as opposed to <see cref="Interceptor"/>, which provides the protocol representation of an interceptor).
/// Client interceptors are invoked when processing outgoing requests or incoming responses.
/// </para>
/// <para>
/// Most commonly, <see cref="McpClientInterceptor"/> instances are created using the static <see cref="M:McpClientInterceptor.Create"/> methods.
/// These methods enable creating an <see cref="McpClientInterceptor"/> for a method, specified via a <see cref="Delegate"/> or
/// <see cref="MethodInfo"/>.
/// </para>
/// <para>
/// By default, parameters are bound from the <see cref="InvokeInterceptorRequestParams"/>:
/// <list type="bullet">
/// <item>
/// <description>
/// <see cref="JsonNode"/> parameters named "payload" are bound to <see cref="InvokeInterceptorRequestParams.Payload"/>.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="InvokeInterceptorContext"/> parameters are bound to <see cref="InvokeInterceptorRequestParams.Context"/>.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="JsonNode"/> parameters named "config" are bound to <see cref="InvokeInterceptorRequestParams.Config"/>.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the caller.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="IServiceProvider"/> parameters are bound from the <see cref="ClientInterceptorContext{InvokeInterceptorRequestParams}"/> for this request.
/// </description>
/// </item>
/// </list>
/// </para>
/// <para>
/// Return values from a method should be an interceptor result type appropriate for the interceptor's type:
/// <list type="bullet">
/// <item><see cref="ValidationInterceptorResult"/> for validation interceptors</item>
/// <item><see cref="MutationInterceptorResult"/> for mutation interceptors</item>
/// <item><see cref="ObservabilityInterceptorResult"/> for observability interceptors</item>
/// </list>
/// </para>
/// </remarks>
public abstract class McpClientInterceptor
{
/// <summary>Initializes a new instance of the <see cref="McpClientInterceptor"/> class.</summary>
protected McpClientInterceptor()
{
}
/// <summary>Gets the protocol <see cref="Interceptor"/> type for this instance.</summary>
public abstract Interceptor ProtocolInterceptor { get; }
/// <summary>
/// Gets the metadata for this interceptor instance.
/// </summary>
/// <remarks>
/// Contains attributes from the associated MethodInfo and declaring class (if any),
/// with class-level attributes appearing before method-level attributes.
/// </remarks>
public abstract IReadOnlyList<object> Metadata { get; }
/// <summary>Invokes the <see cref="McpClientInterceptor"/>.</summary>
/// <param name="context">The context information for this interceptor invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The result from invoking the interceptor.</returns>
/// <exception cref="ArgumentNullException"><paramref name="context"/> is <see langword="null"/>.</exception>
public abstract ValueTask<InterceptorResult> InvokeAsync(
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates an <see cref="McpClientInterceptor"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="McpClientInterceptor"/>.</param>
/// <param name="options">Optional options used in the creation of the <see cref="McpClientInterceptor"/> to control its behavior.</param>
/// <returns>The created <see cref="McpClientInterceptor"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
public static McpClientInterceptor Create(
Delegate method,
McpClientInterceptorCreateOptions? options = null) =>
ReflectionMcpClientInterceptor.Create(method, options);
/// <summary>
/// Creates an <see cref="McpClientInterceptor"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="McpClientInterceptor"/>.</param>
/// <param name="target">The instance if <paramref name="method"/> is an instance method; otherwise, <see langword="null"/>.</param>
/// <param name="options">Optional options used in the creation of the <see cref="McpClientInterceptor"/> to control its behavior.</param>
/// <returns>The created <see cref="McpClientInterceptor"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> is an instance method but <paramref name="target"/> is <see langword="null"/>.</exception>
public static McpClientInterceptor Create(
MethodInfo method,
object? target = null,
McpClientInterceptorCreateOptions? options = null) =>
ReflectionMcpClientInterceptor.Create(method, target, options);
/// <summary>
/// Creates an <see cref="McpClientInterceptor"/> instance for a method, specified via an <see cref="MethodInfo"/> for
/// an instance method, along with a factory function to create the target object.
/// </summary>
/// <param name="method">The instance method to be represented via the created <see cref="McpClientInterceptor"/>.</param>
/// <param name="createTargetFunc">
/// Callback used on each invocation to create an instance of the type on which the instance method <paramref name="method"/>
/// will be invoked. If the returned instance is <see cref="IAsyncDisposable"/> or <see cref="IDisposable"/>, it will
/// be disposed of after the method completes its invocation.
/// </param>
/// <param name="options">Optional options used in the creation of the <see cref="McpClientInterceptor"/> to control its behavior.</param>
/// <returns>The created <see cref="McpClientInterceptor"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> or <paramref name="createTargetFunc"/> is <see langword="null"/>.</exception>
public static McpClientInterceptor Create(
MethodInfo method,
Func<ClientInterceptorContext<InvokeInterceptorRequestParams>, object> createTargetFunc,
McpClientInterceptorCreateOptions? options = null) =>
ReflectionMcpClientInterceptor.Create(method, createTargetFunc, options);
/// <inheritdoc />
public override string ToString() => ProtocolInterceptor.Name;
}

View file

@ -0,0 +1,63 @@
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Attribute used to mark a method as an MCP client interceptor.
/// </summary>
/// <remarks>
/// <para>
/// When applied to a method, this attribute indicates that the method should be exposed as an
/// MCP interceptor that can validate, mutate, or observe messages on the client side.
/// </para>
/// <para>
/// Client interceptors are invoked when the client sends requests to a server or receives responses,
/// enabling validation and transformation at trust boundaries.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class McpClientInterceptorAttribute : Attribute
{
/// <summary>
/// Gets or sets the name of the interceptor.
/// </summary>
/// <remarks>
/// If not specified, a name will be derived from the method name.
/// </remarks>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the version of the interceptor.
/// </summary>
public string? Version { get; set; }
/// <summary>
/// Gets or sets the description of the interceptor.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the events this interceptor handles.
/// </summary>
/// <remarks>
/// Use constants from <see cref="InterceptorEvents"/> for event names.
/// This is a required property when using the attribute.
/// </remarks>
public string[] Events { get; set; } = [];
/// <summary>
/// Gets or sets the interceptor type.
/// </summary>
public InterceptorType Type { get; set; } = InterceptorType.Validation;
/// <summary>
/// Gets or sets the execution phase for this interceptor.
/// </summary>
public InterceptorPhase Phase { get; set; } = InterceptorPhase.Request;
/// <summary>
/// Gets or sets the priority hint for mutation interceptor ordering.
/// </summary>
/// <remarks>
/// Lower values execute first. Default is 0 if not specified.
/// </remarks>
public int PriorityHint { get; set; }
}

View file

@ -0,0 +1,78 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Interceptors.Client;
/// <summary>
/// Options for creating an <see cref="McpClientInterceptor"/>.
/// </summary>
public sealed class McpClientInterceptorCreateOptions
{
/// <summary>
/// Gets or sets the name of the interceptor.
/// </summary>
/// <remarks>
/// If not provided, the name will be derived from the method name.
/// </remarks>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the version of the interceptor.
/// </summary>
public string? Version { get; set; }
/// <summary>
/// Gets or sets the description of the interceptor.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the events this interceptor handles.
/// </summary>
public string[]? Events { get; set; }
/// <summary>
/// Gets or sets the interceptor type.
/// </summary>
public InterceptorType? Type { get; set; }
/// <summary>
/// Gets or sets the execution phase.
/// </summary>
public InterceptorPhase? Phase { get; set; }
/// <summary>
/// Gets or sets the priority hint for mutation ordering.
/// </summary>
public int? PriorityHint { get; set; }
/// <summary>
/// Gets or sets the JSON schema for the interceptor's configuration.
/// </summary>
public JsonElement? ConfigSchema { get; set; }
/// <summary>
/// Gets or sets protocol-level metadata.
/// </summary>
public JsonObject? Meta { get; set; }
/// <summary>
/// Gets or sets the service provider for dependency injection.
/// </summary>
public IServiceProvider? Services { get; set; }
/// <summary>
/// Gets or sets the JSON serializer options.
/// </summary>
public JsonSerializerOptions? SerializerOptions { get; set; }
/// <summary>
/// Gets or sets the metadata for the interceptor.
/// </summary>
public IReadOnlyList<object>? Metadata { get; set; }
/// <summary>
/// Creates a shallow copy of this options instance.
/// </summary>
public McpClientInterceptorCreateOptions Clone() => (McpClientInterceptorCreateOptions)MemberwiseClone();
}

Some files were not shown because too many files have changed in this diff Show more