C# Re-implementation (#5)

Re-implementation of C# interceptors SDK with clearer focus on
server-side / gateway interceptors.
Fixes a lot of issues with the existing implementation.
Still more boilerplate than I'm happy with, but this is better than what
was there. Will refine further in later PRs.
This commit is contained in:
Peder Holdgaard Pedersen 2026-05-14 15:11:41 +02:00 • committed by GitHub
commit d01a626b76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,304 +1,200 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ModelContextProtocol;
using ModelContextProtocol.Interceptors;
using ModelContextProtocol.Interceptors.Server;
using ModelContextProtocol.Server;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Interceptors.Protocol;
using ModelContextProtocol.Interceptors.Server;
using ModelContextProtocol.Server;
namespace Microsoft.Extensions.DependencyInjection;
namespace ModelContextProtocol.Interceptors;
/// <summary>
/// Provides extension methods for configuring MCP interceptors via dependency injection.
/// Extension methods for <see cref="IMcpServerBuilder"/> to register interceptors.
/// </summary>
public static class McpServerInterceptorBuilderExtensions
{
private const string WithInterceptorsRequiresUnreferencedCodeMessage =
$"The non-generic {nameof(WithInterceptors)} and {nameof(WithInterceptorsFromAssembly)} methods require dynamic lookup of method metadata" +
$"and might not work in Native AOT. Use the generic {nameof(WithInterceptors)} method instead.";
/// <summary>Adds <see cref="McpServerInterceptor"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <typeparam name="TInterceptorType">The interceptor type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="serializerOptions">The serializer options governing interceptor parameter marshalling.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TInterceptorType"/>
/// type, where the methods are attributed as <see cref="McpServerInterceptorAttribute"/>, and adds an <see cref="McpServerInterceptor"/>
/// instance for each. For instance methods, an instance is constructed for each invocation of the interceptor.
/// </remarks>
public static IMcpServerBuilder WithInterceptors<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicConstructors)] TInterceptorType>(
this IMcpServerBuilder builder,
JsonSerializerOptions? serializerOptions = null)
/// <summary>
/// Registers interceptors from all methods on <typeparamref name="T"/> that are decorated
/// with <see cref="McpServerInterceptorAttribute"/>.
/// </summary>
public static IMcpServerBuilder WithInterceptors<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] T>(
this IMcpServerBuilder builder)
{
Throw.IfNull(builder);
ArgumentNullException.ThrowIfNull(builder);
foreach (var interceptorMethod in typeof(TInterceptorType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
EnsureSetupRegistered(builder.Services);
foreach (var method in typeof(T).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
{
if (interceptorMethod.GetCustomAttribute<McpServerInterceptorAttribute>() is not null)
if (method.GetCustomAttribute<McpServerInterceptorAttribute>() is not null)
{
builder.Services.AddSingleton((Func<IServiceProvider, McpServerInterceptor>)(interceptorMethod.IsStatic ?
services => McpServerInterceptor.Create(interceptorMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
services => McpServerInterceptor.Create(interceptorMethod, static r => CreateTarget(r.Services, typeof(TInterceptorType)), new() { Services = services, SerializerOptions = serializerOptions })));
McpServerInterceptor interceptor;
if (method.IsStatic)
{
interceptor = ReflectionMcpServerInterceptor.Create(method, target: null);
}
else
{
// For instance methods, we need a factory. Register the type in DI if not already.
builder.Services.TryAddTransient(typeof(T));
interceptor = ReflectionMcpServerInterceptor.Create(
method,
target: null,
new McpServerInterceptorCreateOptions { Services = null });
// Wrap with instance resolution
var instanceInterceptor = new DeferredInstanceInterceptor<T>(interceptor, method);
builder.Services.AddSingleton<McpServerInterceptor>(instanceInterceptor);
continue;
}
builder.Services.AddSingleton<McpServerInterceptor>(interceptor);
}
}
return builder;
}
/// <summary>Adds <see cref="McpServerInterceptor"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <typeparam name="TInterceptorType">The interceptor type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="target">The target instance from which the interceptors should be sourced.</param>
/// <param name="serializerOptions">The serializer options governing interceptor parameter marshalling.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="target"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This method discovers all methods (public and non-public) on the specified <typeparamref name="TInterceptorType"/>
/// type, where the methods are attributed as <see cref="McpServerInterceptorAttribute"/>, and adds an <see cref="McpServerInterceptor"/>
/// instance for each, using <paramref name="target"/> as the associated instance for instance methods.
/// </para>
/// <para>
/// However, if <typeparamref name="TInterceptorType"/> is itself an <see cref="IEnumerable{T}"/> of <see cref="McpServerInterceptor"/>,
/// this method registers those interceptors directly without scanning for methods on <typeparamref name="TInterceptorType"/>.
/// </para>
/// </remarks>
public static IMcpServerBuilder WithInterceptors<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods)] TInterceptorType>(
/// <summary>
/// Registers interceptors from all methods on <typeparamref name="T"/> on the given instance.
/// </summary>
public static IMcpServerBuilder WithInterceptors<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] T>(
this IMcpServerBuilder builder,
TInterceptorType target,
JsonSerializerOptions? serializerOptions = null)
T target) where T : class
{
Throw.IfNull(builder);
Throw.IfNull(target);
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(target);
if (target is IEnumerable<McpServerInterceptor> interceptors)
{
return builder.WithInterceptors(interceptors);
}
EnsureSetupRegistered(builder.Services);
foreach (var interceptorMethod in typeof(TInterceptorType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
foreach (var method in typeof(T).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
{
if (interceptorMethod.GetCustomAttribute<McpServerInterceptorAttribute>() is not null)
if (method.GetCustomAttribute<McpServerInterceptorAttribute>() is not null)
{
builder.Services.AddSingleton(services => McpServerInterceptor.Create(
interceptorMethod,
interceptorMethod.IsStatic ? null : target,
new() { Services = services, SerializerOptions = serializerOptions }));
var interceptor = ReflectionMcpServerInterceptor.Create(method, method.IsStatic ? null : target);
builder.Services.AddSingleton<McpServerInterceptor>(interceptor);
}
}
return builder;
}
/// <summary>Adds <see cref="McpServerInterceptor"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <param name="builder">The builder instance.</param>
/// <param name="interceptors">The <see cref="McpServerInterceptor"/> instances to add to the server.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="interceptors"/> is <see langword="null"/>.</exception>
public static IMcpServerBuilder WithInterceptors(this IMcpServerBuilder builder, IEnumerable<McpServerInterceptor> interceptors)
/// <summary>
/// Registers the specified interceptors directly.
/// </summary>
public static IMcpServerBuilder WithInterceptors(
this IMcpServerBuilder builder,
params McpServerInterceptor[] interceptors)
{
Throw.IfNull(builder);
Throw.IfNull(interceptors);
ArgumentNullException.ThrowIfNull(builder);
EnsureSetupRegistered(builder.Services);
foreach (var interceptor in interceptors)
{
if (interceptor is not null)
{
builder.Services.AddSingleton(interceptor);
}
builder.Services.AddSingleton(interceptor);
}
return builder;
}
/// <summary>Adds <see cref="McpServerInterceptor"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <param name="builder">The builder instance.</param>
/// <param name="interceptorTypes">Types with <see cref="McpServerInterceptorAttribute"/>-attributed methods to add as interceptors to the server.</param>
/// <param name="serializerOptions">The serializer options governing interceptor parameter marshalling.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="interceptorTypes"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This method discovers all instance and static methods (public and non-public) on the specified <paramref name="interceptorTypes"/>
/// types, where the methods are attributed as <see cref="McpServerInterceptorAttribute"/>, and adds an <see cref="McpServerInterceptor"/>
/// instance for each. For instance methods, an instance is constructed for each invocation of the interceptor.
/// </remarks>
[RequiresUnreferencedCode(WithInterceptorsRequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithInterceptors(this IMcpServerBuilder builder, IEnumerable<Type> interceptorTypes, JsonSerializerOptions? serializerOptions = null)
private static void EnsureSetupRegistered(IServiceCollection services)
{
Throw.IfNull(builder);
Throw.IfNull(interceptorTypes);
foreach (var interceptorType in interceptorTypes)
// Only register once
if (services.Any(d => d.ImplementationType == typeof(InterceptorServerOptionsSetup)))
{
if (interceptorType is not null)
return;
}
services.AddSingleton<IConfigureOptions<McpServerOptions>, InterceptorServerOptionsSetup>();
}
/// <summary>
/// Wraps an interceptor to resolve target instances from DI for instance methods.
/// </summary>
private sealed class DeferredInstanceInterceptor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TTarget> : McpServerInterceptor
{
private readonly McpServerInterceptor _inner;
private readonly MethodInfo _method;
internal DeferredInstanceInterceptor(McpServerInterceptor inner, MethodInfo method)
{
_inner = inner;
_method = method;
}
public override Interceptor ProtocolInterceptor => _inner.ProtocolInterceptor;
public override IReadOnlyList<object> Metadata => _inner.Metadata;
public override async ValueTask<InterceptorResult> InvokeAsync(
InvokeInterceptorRequestParams request,
McpServer server,
IServiceProvider? services,
CancellationToken cancellationToken = default)
{
if (services is null)
{
foreach (var interceptorMethod in interceptorType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
throw new InvalidOperationException($"Cannot resolve instance of type '{typeof(TTarget).Name}' without a service provider.");
}
var target = (TTarget)services.GetRequiredService(typeof(TTarget));
// Create a new interceptor bound to this instance and invoke it
var boundInterceptor = ReflectionMcpServerInterceptor.Create(_method, target);
return await boundInterceptor.InvokeAsync(request, server, services, cancellationToken);
}
}
}
/// <summary>
/// Configures <see cref="McpServerOptions"/> to register interceptor support.
/// </summary>
internal sealed class InterceptorServerOptionsSetup : IConfigureOptions<McpServerOptions>
{
private readonly IEnumerable<McpServerInterceptor> _interceptors;
public InterceptorServerOptionsSetup(IEnumerable<McpServerInterceptor> interceptors)
{
_interceptors = interceptors;
}
public void Configure(McpServerOptions options)
{
// Collect all interceptors into a primitive collection
var collection = new McpServerPrimitiveCollection<McpServerInterceptor>();
var allEvents = new HashSet<string>();
foreach (var interceptor in _interceptors)
{
collection.Add(interceptor);
foreach (var hook in interceptor.ProtocolInterceptor.Hooks)
{
foreach (var ev in hook.Events)
{
if (interceptorMethod.GetCustomAttribute<McpServerInterceptorAttribute>() is not null)
{
builder.Services.AddSingleton((Func<IServiceProvider, McpServerInterceptor>)(interceptorMethod.IsStatic ?
services => McpServerInterceptor.Create(interceptorMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
services => McpServerInterceptor.Create(interceptorMethod, r => CreateTarget(r.Services, interceptorType), new() { Services = services, SerializerOptions = serializerOptions })));
}
allEvents.Add(ev);
}
}
}
return builder;
// Register the message filter
var filter = new InterceptorMessageFilter(collection);
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
// Advertise interceptor capability via Extensions
options.Capabilities ??= new();
#pragma warning disable MCPEXP001 // We intentionally use the experimental Extensions API for protocol extensions
options.Capabilities.Extensions ??= new Dictionary<string, object>();
// Serialize to JsonElement so the SDK's own serializer (which doesn't know about
// InterceptorsCapability) can write it without type-info issues.
var capability = new InterceptorsCapability
{
SupportedEvents = allEvents.ToList(),
};
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
capability, InterceptorJsonUtilities.DefaultOptions);
#pragma warning restore MCPEXP001
}
/// <summary>
/// Adds types marked with the <see cref="McpServerInterceptorTypeAttribute"/> attribute from the given assembly as interceptors to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="serializerOptions">The serializer options governing interceptor parameter marshalling.</param>
/// <param name="interceptorAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly is used.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This method scans the specified assembly (or the calling assembly if none is provided) for classes
/// marked with the <see cref="McpServerInterceptorTypeAttribute"/>. It then discovers all methods within those
/// classes that are marked with the <see cref="McpServerInterceptorAttribute"/> and registers them as <see cref="McpServerInterceptor"/>s
/// in the <paramref name="builder"/>'s <see cref="IServiceCollection"/>.
/// </para>
/// <para>
/// The method automatically handles both static and instance methods. For instance methods, a new instance
/// of the containing class is constructed for each invocation of the interceptor.
/// </para>
/// <para>
/// Interceptors registered through this method can be discovered by clients using the <c>interceptors/list</c> request
/// and invoked using the <c>interceptor/invoke</c> request.
/// </para>
/// <para>
/// Note that this method performs reflection at runtime and might not work in Native AOT scenarios. For
/// Native AOT compatibility, consider using the generic <see cref="M:WithInterceptors"/> method instead.
/// </para>
/// </remarks>
[RequiresUnreferencedCode(WithInterceptorsRequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithInterceptorsFromAssembly(this IMcpServerBuilder builder, Assembly? interceptorAssembly = null, JsonSerializerOptions? serializerOptions = null)
{
Throw.IfNull(builder);
interceptorAssembly ??= Assembly.GetCallingAssembly();
return builder.WithInterceptors(
from t in interceptorAssembly.GetTypes()
where t.GetCustomAttribute<McpServerInterceptorTypeAttribute>() is not null
select t,
serializerOptions);
}
/// <summary>
/// Configures a handler for listing interceptors available from the Model Context Protocol server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler that processes list interceptors requests.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This handler is called when a client requests a list of available interceptors. It should return all interceptors
/// that can be invoked through the server, including their names, descriptions, and supported events.
/// </para>
/// <para>
/// When interceptors are also defined using <see cref="McpServerInterceptor"/> collection, both sets of interceptors
/// will be combined in the response to clients. This allows for a mix of programmatically defined
/// interceptors and dynamically generated interceptors.
/// </para>
/// <para>
/// This method is typically paired with <see cref="WithInvokeInterceptorHandler"/> to provide a complete interceptors implementation,
/// where <see cref="WithListInterceptorsHandler"/> advertises available interceptors and <see cref="WithInvokeInterceptorHandler"/>
/// executes them when invoked by clients.
/// </para>
/// </remarks>
public static IMcpServerBuilder WithListInterceptorsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListInterceptorsRequestParams>, CancellationToken, ValueTask<ListInterceptorsResult>> handler)
{
Throw.IfNull(builder);
builder.Services.Configure<InterceptorServerHandlers>(s => s.ListInterceptorsHandler = handler);
return builder;
}
/// <summary>
/// Configures a handler for invoking interceptors available from the Model Context Protocol server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler function that processes interceptor invocations.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The invoke interceptor handler is responsible for executing validation interceptors and returning their results to clients.
/// This method is typically paired with <see cref="WithListInterceptorsHandler"/> to provide a complete interceptors implementation,
/// where <see cref="WithListInterceptorsHandler"/> advertises available interceptors and this handler executes them.
/// </remarks>
public static IMcpServerBuilder WithInvokeInterceptorHandler(this IMcpServerBuilder builder, Func<RequestContext<InvokeInterceptorRequestParams>, CancellationToken, ValueTask<ValidationInterceptorResult>> handler)
{
Throw.IfNull(builder);
builder.Services.Configure<InterceptorServerHandlers>(s => s.InvokeInterceptorHandler = handler);
return builder;
}
/// <summary>
/// Adds a filter to the list interceptors handler pipeline.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This filter wraps handlers that return a list of available interceptors when requested by a client.
/// The filter can modify, log, or perform additional operations on requests and responses.
/// </para>
/// <para>
/// This filter works alongside any interceptors defined in the <see cref="McpServerInterceptor"/> collection.
/// Interceptors from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddListInterceptorsFilter(this IMcpServerBuilder builder, Func<RequestContext<ListInterceptorsRequestParams>, Func<RequestContext<ListInterceptorsRequestParams>, CancellationToken, ValueTask<ListInterceptorsResult>>, CancellationToken, ValueTask<ListInterceptorsResult>> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<InterceptorServerFilters>(options => options.ListInterceptorsFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the invoke interceptor handler pipeline.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This filter wraps handlers that are invoked when a client calls an interceptor.
/// The filter can modify, log, or perform additional operations on requests and responses.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddInvokeInterceptorFilter(this IMcpServerBuilder builder, Func<RequestContext<InvokeInterceptorRequestParams>, Func<RequestContext<InvokeInterceptorRequestParams>, CancellationToken, ValueTask<ValidationInterceptorResult>>, CancellationToken, ValueTask<ValidationInterceptorResult>> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<InterceptorServerFilters>(options => options.InvokeInterceptorFilters.Add(filter));
return builder;
}
/// <summary>Creates an instance of the target object.</summary>
private static object CreateTarget(
IServiceProvider? services,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) =>
services is not null ? ActivatorUtilities.CreateInstance(services, type) :
Activator.CreateInstance(type)!;
}