C# Implementation (#2)
C# implementation of SEP-1763 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
46d96779d6
commit
45f9e0ef27
68 changed files with 13862 additions and 21 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#if !NET
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
|
||||
internal sealed class SetsRequiredMembersAttribute : Attribute;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
70
csharp/sdk/src/Common/Throw.cs
Normal file
70
csharp/sdk/src/Common/Throw.cs
Normal 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.");
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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) =>
|
||||
/// {
|
||||
/// // 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
|
||||
}
|
||||
|
|
@ -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) =>
|
||||
/// {
|
||||
/// 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) => ValueTask.FromResult(ValidationInterceptorResult.Success())),
|
||||
/// McpClientInterceptor.Create(
|
||||
/// name: "pii-filter",
|
||||
/// events: [InterceptorEvents.ToolsCall],
|
||||
/// type: InterceptorType.Validation,
|
||||
/// handler: (ctx, ct) => 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, JsonNode>
|
||||
/// {
|
||||
/// ["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;
|
||||
}
|
||||
|
|
@ -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 & Observe → Send</item>
|
||||
/// <item><b>Receiving</b>: Receive → Validate & 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 & 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 & 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue