diff --git a/src/ModelContextProtocol.Core/Server/AlternateResultFilterCollection.cs b/src/ModelContextProtocol.Core/Server/AlternateResultFilterCollection.cs
new file mode 100644
index 000000000..85268ff80
--- /dev/null
+++ b/src/ModelContextProtocol.Core/Server/AlternateResultFilterCollection.cs
@@ -0,0 +1,131 @@
+using ModelContextProtocol.Protocol;
+using System.Diagnostics.CodeAnalysis;
+
+namespace ModelContextProtocol.Server;
+
+#pragma warning disable MCPEXP002 // this collection is itself part of the experimental ResultOrAlternate seam
+
+///
+/// Stores alternate-result filters keyed by JSON-RPC method name, so that augmenting a Core-dispatched
+/// method to return an alternate subtype does not require a new
+/// <Method>WithAlternateHandler/<Method>WithAlternateFilters pair per method.
+///
+///
+///
+/// Each method may only be registered with a single (TParams, TResult) pair, matching the
+/// types Core dispatches that method with. Registering the same method with different types throws, because
+/// the mismatched filters could never be invoked.
+///
+///
+/// is a typed view over the
+/// entry of this collection.
+///
+///
+[Experimental(Experimentals.Extensibility_DiagnosticId, UrlFormat = Experimentals.Extensibility_Url)]
+public sealed class AlternateResultFilterCollection
+{
+ private readonly Dictionary _entries = new(StringComparer.Ordinal);
+
+ ///
+ /// Gets the filter list registered for , creating an empty one if the method
+ /// has not been registered yet.
+ ///
+ /// The request parameter type Core dispatches with.
+ /// The result type Core dispatches with.
+ /// The JSON-RPC method name, for example .
+ /// The mutable filter list for .
+ ///
+ /// was already registered with a different parameter or result type.
+ ///
+ public IList>> GetOrAdd(string method)
+ where TResult : Result
+ {
+ Throw.IfNullOrWhiteSpace(method);
+
+ if (_entries.TryGetValue(method, out var entry))
+ {
+ return Cast(entry, method);
+ }
+
+ var filters = new List>>();
+ _entries[method] = new Entry(typeof(TParams), typeof(TResult), filters, () => filters.Count);
+ return filters;
+ }
+
+ ///
+ /// Replaces the filter list registered for .
+ ///
+ /// The request parameter type Core dispatches with.
+ /// The result type Core dispatches with.
+ /// The JSON-RPC method name.
+ /// The filter list to store.
+ public void Set(
+ string method,
+ IList>> filters)
+ where TResult : Result
+ {
+ Throw.IfNullOrWhiteSpace(method);
+ Throw.IfNull(filters);
+
+ _entries[method] = new Entry(typeof(TParams), typeof(TResult), filters, () => filters.Count);
+ }
+
+ ///
+ /// Gets the method names that currently have at least one registered filter.
+ ///
+ internal IEnumerable NonEmptyMethods
+ {
+ get
+ {
+ foreach (var entry in _entries)
+ {
+ if (entry.Value.Count > 0)
+ {
+ yield return entry.Key;
+ }
+ }
+ }
+ }
+
+ internal bool TryGetNonEmpty(
+ string method,
+ [NotNullWhen(true)] out IList>>? filters)
+ where TResult : Result
+ {
+ if (_entries.TryGetValue(method, out var entry) && entry.Count > 0)
+ {
+ filters = Cast(entry, method);
+ return true;
+ }
+
+ filters = null;
+ return false;
+ }
+
+ private static IList>> Cast(
+ Entry entry,
+ string method)
+ where TResult : Result
+ {
+ if (entry.Filters is IList>> typed)
+ {
+ return typed;
+ }
+
+ throw new InvalidOperationException(
+ $"Alternate-result filters for method '{method}' were registered with " +
+ $"({entry.ParamsType}, {entry.ResultType}), but ({typeof(TParams)}, {typeof(TResult)}) was requested. " +
+ $"A method can only carry alternate-result filters for the types the server dispatches it with.");
+ }
+
+ private sealed class Entry(Type paramsType, Type resultType, object filters, Func count)
+ {
+ public Type ParamsType { get; } = paramsType;
+
+ public Type ResultType { get; } = resultType;
+
+ public object Filters { get; } = filters;
+
+ public int Count => count();
+ }
+}
diff --git a/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs
index a1aad7112..67f5a34ea 100644
--- a/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs
+++ b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs
@@ -66,6 +66,24 @@ public IList> CallToolFi
}
}
+ ///
+ /// Gets the alternate-result filters registered for arbitrary JSON-RPC methods.
+ ///
+ ///
+ ///
+ /// Alternate-result filters let an extension augment a Core-dispatched method so it can return an alternate
+ /// subtype (for example a task-creation result) in place of the method's normal result.
+ /// The collection is keyed by method name, so enabling a new method is an extension-only change rather than a
+ /// new <Method>WithAlternateHandler/<Method>WithAlternateFilters pair on Core.
+ ///
+ ///
+ /// is a typed view over the
+ /// entry of this collection, so the two stay in sync.
+ ///
+ ///
+ [Experimental(Experimentals.Extensibility_DiagnosticId, UrlFormat = Experimentals.Extensibility_Url)]
+ public AlternateResultFilterCollection AlternateResultFilters => field ??= new();
+
///
/// Gets or sets the filters for the call-tool handler pipeline with alternate result support.
///
@@ -83,15 +101,19 @@ public IList> CallToolFi
/// Alternate-result filters run in registration order. If one filter dispatches the remainder of the pipeline
/// asynchronously, filters registered after it execute as part of that asynchronous operation.
///
+ ///
+ /// This property is a view over the entry of
+ /// . Filters added through either API share one ordered list.
+ ///
///
[Experimental(Experimentals.Extensibility_DiagnosticId, UrlFormat = Experimentals.Extensibility_Url)]
public IList>> CallToolWithAlternateFilters
{
- get => field ??= [];
+ get => AlternateResultFilters.GetOrAdd(RequestMethods.ToolsCall);
set
{
Throw.IfNull(value);
- field = value;
+ AlternateResultFilters.Set(RequestMethods.ToolsCall, value);
}
}
#pragma warning restore MCPEXP002
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index f68aacdff..610db20f3 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -49,6 +49,9 @@ internal sealed partial class McpServerImpl : McpServer
private ClientCapabilities? _clientCapabilities;
private Implementation? _clientInfo;
+ /// Methods whose alternate-result filters were actually wired into a dispatched handler.
+ private readonly HashSet _alternateResultMethodsApplied = new(StringComparer.Ordinal);
+
private readonly string _serverOnlyEndpointName;
private string? _negotiatedProtocolVersion;
private string _endpointName;
@@ -109,6 +112,7 @@ public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFact
ConfigureExperimentalAndExtensions(options);
ConfigureMrtr();
ConfigureCustomRequestHandlers(options);
+ ValidateAlternateResultRegistrations(options);
// Register any notification handlers that were provided.
if (options.Handlers.NotificationHandlers is { } notificationHandlers)
@@ -973,7 +977,7 @@ private void ConfigureCompletion(McpServerOptions options)
ServerCapabilities.Completions = new();
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.CompletionComplete,
completeHandler,
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
@@ -1240,31 +1244,31 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure
ServerCapabilities.Resources.ListChanged = listChanged;
ServerCapabilities.Resources.Subscribe = subscribe;
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.ResourcesList,
listResourcesHandler,
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.ResourcesTemplatesList,
listResourceTemplatesHandler,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.ResourcesRead,
readResourceHandler,
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.ResourcesSubscribe,
subscribeHandler,
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.ResourcesUnsubscribe,
unsubscribeHandler,
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
@@ -1351,13 +1355,13 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals
ServerCapabilities.Prompts.ListChanged = listChanged;
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.PromptsList,
listPromptsHandler,
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.PromptsGet,
getPromptHandler,
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
@@ -1487,12 +1491,13 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
}
ServerCapabilities.Tools.ListChanged = listChanged;
- SetHandler(
+ SetHandlerWithAlternateSupport(
RequestMethods.ToolsList,
listToolsHandler,
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
McpJsonUtilities.JsonContext.Default.ListToolsResult);
+ _alternateResultMethodsApplied.Add(RequestMethods.ToolsCall);
SetWithAlternateHandler(
RequestMethods.ToolsCall,
callToolWithAlternateHandler,
@@ -1761,30 +1766,7 @@ private void SetHandler(
JsonTypeInfo requestTypeInfo,
JsonTypeInfo responseTypeInfo)
{
- // SEP-2549: results that carry caching hints (tools/list, prompts/list, resources/list,
- // resources/templates/list, and resources/read) declare ttlMs and cacheScope as required fields.
- // When a handler leaves them unset, fill in conservative defaults (immediately stale and not
- // shareable) so the wire form always carries the fields while preserving today's "don't cache"
- // behavior. Any value supplied by the handler or a filter is left untouched.
- if (typeof(ICacheableResult).IsAssignableFrom(typeof(TResult)))
- {
- var innerHandler = handler;
- handler = async (request, cancellationToken) =>
- {
- var result = await innerHandler(request, cancellationToken).ConfigureAwait(false);
-
- // ttlMs and cacheScope are 2026-07-28 result fields; only stamp them when the request
- // was negotiated under that revision or later. Earlier revisions (e.g. 2025-11-25) reject
- // these as unrecognized keys (issue #1721).
- if (result is ICacheableResult cacheable && IsJuly2026OrLaterProtocolRequest(request.JsonRpcRequest))
- {
- cacheable.TimeToLive ??= TimeSpan.Zero;
- cacheable.CacheScope ??= CacheScope.Private;
- }
-
- return result;
- };
- }
+ handler = WrapWithCacheableDefaults(handler);
if (typeof(Result).IsAssignableFrom(typeof(TResult)))
{
@@ -1812,6 +1794,94 @@ private void SetHandler(
requestTypeInfo, responseTypeInfo);
}
+ ///
+ /// Registers a built-in typed handler, routing it through the method-keyed alternate-result pipeline
+ /// when registered filters for
+ /// .
+ ///
+ ///
+ /// Only adaptation and filter composition are generalized here. Per-method policy such as primitive
+ /// matching and lifecycle logging stays with the method that needs it; tools/call keeps its own
+ /// composition in .
+ ///
+#pragma warning disable MCPEXP002 // consumes the experimental alternate-result seam
+ private void SetHandlerWithAlternateSupport(
+ string method,
+ McpRequestHandler handler,
+ JsonTypeInfo requestTypeInfo,
+ JsonTypeInfo responseTypeInfo)
+ where TResult : Result
+ {
+ if (!ServerOptions.Filters.Request.AlternateResultFilters.TryGetNonEmpty(method, out var filters))
+ {
+ SetHandler(method, handler, requestTypeInfo, responseTypeInfo);
+ return;
+ }
+
+ _alternateResultMethodsApplied.Add(method);
+
+ var ordinaryHandler = WrapWithCacheableDefaults(handler);
+ McpRequestHandler> adapted = async (request, cancellationToken) =>
+ await ordinaryHandler(request, cancellationToken).ConfigureAwait(false);
+
+ SetWithAlternateHandler(
+ method,
+ BuildInvocationFilterPipeline(adapted, filters),
+ requestTypeInfo,
+ responseTypeInfo);
+ }
+
+ ///
+ /// Throws when alternate-result filters were registered for a method the server never dispatches, so a
+ /// typo or a missing capability surfaces at construction instead of silently dropping the filters.
+ ///
+ private void ValidateAlternateResultRegistrations(McpServerOptions options)
+ {
+ foreach (var method in options.Filters.Request.AlternateResultFilters.NonEmptyMethods)
+ {
+ if (!_alternateResultMethodsApplied.Contains(method))
+ {
+ throw new InvalidOperationException(
+ $"Alternate-result filters were registered for method '{method}', but this server does not " +
+ $"dispatch that method. Register them for a method the server handles, or configure the " +
+ $"capability that provides it.");
+ }
+ }
+ }
+#pragma warning restore MCPEXP002
+
+ ///
+ /// SEP-2549: results that carry caching hints (tools/list, prompts/list, resources/list,
+ /// resources/templates/list, and resources/read) declare ttlMs and cacheScope as required fields.
+ /// When a handler leaves them unset, fill in conservative defaults (immediately stale and not
+ /// shareable) so the wire form always carries the fields while preserving today's "don't cache"
+ /// behavior. Any value supplied by the handler or a filter is left untouched.
+ ///
+ private McpRequestHandler WrapWithCacheableDefaults(
+ McpRequestHandler handler)
+ {
+ if (!typeof(ICacheableResult).IsAssignableFrom(typeof(TResult)))
+ {
+ return handler;
+ }
+
+ return async (request, cancellationToken) =>
+ {
+ var result = await handler(request, cancellationToken).ConfigureAwait(false);
+
+ // ttlMs and cacheScope are 2026-07-28 result fields; only stamp them when the request
+ // was negotiated under that revision or later. Earlier revisions (e.g. 2025-11-25) reject
+ // these as unrecognized keys (issue #1721).
+ if (result is ICacheableResult cacheable && IsJuly2026OrLaterProtocolRequest(request.JsonRpcRequest))
+ {
+ cacheable.TimeToLive ??= TimeSpan.Zero;
+ cacheable.CacheScope ??= CacheScope.Private;
+ }
+
+ return result;
+ };
+ }
+
#pragma warning disable MCPEXP002 // SetWithAlternateHandler wraps the experimental ResultOrAlternate seam
private void SetWithAlternateHandler(
string method,
diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs
index 2a26868a1..de5f2b7d1 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs
@@ -220,4 +220,40 @@ public McpServerFilters Filters
///
[Experimental(Experimentals.Extensibility_DiagnosticId, UrlFormat = Experimentals.Extensibility_Url)]
public IList? RequestHandlers { get; set; }
+
+ ///
+ /// Registers a filter that can replace the result of a Core-dispatched method with an alternate
+ /// subtype.
+ ///
+ /// The request parameter type the server dispatches with.
+ /// The result type the server dispatches with.
+ /// The JSON-RPC method to augment, for example .
+ /// The filter to append to the method's alternate-result pipeline.
+ ///
+ ///
+ /// Unlike , this seam is method-agnostic:
+ /// enabling alternate results for another method is a call to this method rather than a new pair of
+ /// Core APIs. Filters run outside the method's ordinary filter pipeline, in registration order, and
+ /// receive a handler that produces the method's normal result.
+ ///
+ ///
+ /// Registering a filter for a method the server does not dispatch (for example because the corresponding
+ /// capability is not configured) throws when the server is constructed, so a typo or a missing capability
+ /// fails loudly instead of silently dropping the filter.
+ ///
+ ///
+ ///
+ /// already carries alternate-result filters registered with different types.
+ ///
+ [Experimental(Experimentals.Extensibility_DiagnosticId, UrlFormat = Experimentals.Extensibility_Url)]
+ public void AddAlternateResultFilter(
+ string method,
+ McpRequestInvocationFilter> filter)
+ where TResult : Result
+ {
+ Throw.IfNullOrWhiteSpace(method);
+ Throw.IfNull(filter);
+
+ Filters.Request.AlternateResultFilters.GetOrAdd(method).Add(filter);
+ }
}
diff --git a/tests/ModelContextProtocol.Tests/Server/AlternateResultFilterTests.cs b/tests/ModelContextProtocol.Tests/Server/AlternateResultFilterTests.cs
new file mode 100644
index 000000000..0a9ae72f6
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Server/AlternateResultFilterTests.cs
@@ -0,0 +1,206 @@
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using ModelContextProtocol.Tests.Utils;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace ModelContextProtocol.Tests.Server;
+
+#pragma warning disable MCPEXP002 // exercises the experimental alternate-result seam
+
+///
+/// An alternate result a filter can return in place of a prompt result, standing in for the kind of
+/// result a future task-enabled method would produce.
+///
+internal sealed class AlternatePromptResult : Result
+{
+ [JsonPropertyName("note")]
+ public string? Note { get; set; }
+}
+
+[JsonSerializable(typeof(AlternatePromptResult))]
+internal sealed partial class AlternateResultJsonContext : JsonSerializerContext;
+
+///
+/// Verifies that the alternate-result seam is method-keyed rather than tools/call-shaped, by
+/// driving it through and .
+///
+public class AlternateResultFilterPromptTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper)
+{
+ private static readonly JsonSerializerOptions s_serializerOptions = CreateSerializerOptions();
+
+ private readonly List _promptsListFilterOrder = [];
+
+ protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
+ {
+ mcpServerBuilder.Services.Configure(options =>
+ {
+ options.Capabilities = new() { Prompts = new() };
+
+ options.Handlers.ListPromptsHandler = static (_, _) => new(new ListPromptsResult
+ {
+ Prompts = [new Prompt { Name = "ordinary-prompt" }],
+ });
+
+ options.Handlers.GetPromptHandler = static (request, _) => new(new GetPromptResult
+ {
+ Description = $"ordinary:{request.Params?.Name}",
+ });
+
+ options.AddAlternateResultFilter(
+ RequestMethods.PromptsGet,
+ async (request, next, cancellationToken) =>
+ {
+ if (request.Params?.Name == "alternate-prompt")
+ {
+ return ResultOrAlternate.FromAlternate(
+ new AlternatePromptResult { Note = "replaced by filter" },
+ AlternateResultJsonContext.Default.AlternatePromptResult);
+ }
+
+ return await next(request, cancellationToken);
+ });
+
+ options.AddAlternateResultFilter(
+ RequestMethods.PromptsList,
+ async (request, next, cancellationToken) =>
+ {
+ _promptsListFilterOrder.Add("outer");
+ return await next(request, cancellationToken);
+ });
+
+ options.AddAlternateResultFilter(
+ RequestMethods.PromptsList,
+ async (request, next, cancellationToken) =>
+ {
+ _promptsListFilterOrder.Add("inner");
+ return await next(request, cancellationToken);
+ });
+ });
+ }
+
+ [Fact]
+ public async Task PromptsGet_AlternateFilter_ReturnsAlternateResultShape()
+ {
+ await using var client = await CreateMcpClientForServer();
+
+ var result = await client.SendRequestAsync(
+ RequestMethods.PromptsGet,
+ new GetPromptRequestParams { Name = "alternate-prompt" },
+ serializerOptions: s_serializerOptions,
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal("replaced by filter", result.Note);
+ }
+
+ [Fact]
+ public async Task PromptsGet_AlternateFilterDelegatingToNext_ReturnsOrdinaryResult()
+ {
+ await using var client = await CreateMcpClientForServer();
+
+ var result = await client.GetPromptAsync(
+ "ordinary-prompt",
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal("ordinary:ordinary-prompt", result.Description);
+ }
+
+ [Fact]
+ public async Task PromptsList_AlternateFilters_RunInRegistrationOrder()
+ {
+ await using var client = await CreateMcpClientForServer();
+
+ var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal("ordinary-prompt", Assert.Single(prompts).Name);
+ Assert.Equal(["outer", "inner"], _promptsListFilterOrder);
+ }
+
+ private static JsonSerializerOptions CreateSerializerOptions()
+ {
+ JsonSerializerOptions options = new(McpJsonUtilities.DefaultOptions)
+ {
+ TypeInfoResolver = JsonTypeInfoResolver.Combine(
+ AlternateResultJsonContext.Default,
+ McpJsonUtilities.DefaultOptions.TypeInfoResolver),
+ };
+
+ options.MakeReadOnly();
+ return options;
+ }
+}
+
+///
+/// Covers registration semantics of the method-keyed alternate-result store, including the
+/// tools/call view and the errors raised for unusable registrations.
+///
+public class AlternateResultFilterRegistrationTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper)
+{
+ private static McpRequestInvocationFilter> PassThroughToolFilter =>
+ static (context, next, cancellationToken) => next(context, cancellationToken);
+
+ [Fact]
+ public void CallToolWithAlternateFilters_IsViewOverToolsCallEntry()
+ {
+ var options = new McpServerOptions();
+
+ options.AddAlternateResultFilter(RequestMethods.ToolsCall, PassThroughToolFilter);
+
+ Assert.Same(
+ options.Filters.Request.AlternateResultFilters.GetOrAdd(RequestMethods.ToolsCall),
+ options.Filters.Request.CallToolWithAlternateFilters);
+ Assert.Single(options.Filters.Request.CallToolWithAlternateFilters);
+ }
+
+ [Fact]
+ public void GetOrAdd_WithMismatchedResultType_Throws()
+ {
+ var options = new McpServerOptions();
+ options.AddAlternateResultFilter(
+ RequestMethods.PromptsGet,
+ static (context, next, cancellationToken) => next(context, cancellationToken));
+
+ var ex = Assert.Throws(
+ () => options.AddAlternateResultFilter(
+ RequestMethods.PromptsGet,
+ static (context, next, cancellationToken) => next(context, cancellationToken)));
+
+ Assert.Contains(RequestMethods.PromptsGet, ex.Message);
+ Assert.Contains(nameof(GetPromptResult), ex.Message);
+ }
+
+ [Fact]
+ public async Task Filters_ForMethodTheServerDoesNotDispatch_ThrowActionableError()
+ {
+ await using var transport = new StreamServerTransport(Stream.Null, Stream.Null);
+ var options = new McpServerOptions { Capabilities = new() { Tools = new() } };
+
+ // No prompts capability is configured, so prompts/get is never registered.
+ options.AddAlternateResultFilter(
+ RequestMethods.PromptsGet,
+ static (context, next, cancellationToken) => next(context, cancellationToken));
+
+ var ex = Assert.Throws(
+ () => McpServer.Create(transport, options, LoggerFactory));
+
+ Assert.Contains(RequestMethods.PromptsGet, ex.Message);
+ Assert.Contains("does not", ex.Message);
+ }
+
+ [Fact]
+ public async Task EmptyRegistration_ForUndispatchedMethod_DoesNotThrow()
+ {
+ await using var transport = new StreamServerTransport(Stream.Null, Stream.Null);
+ var options = new McpServerOptions { Capabilities = new() { Tools = new() } };
+
+ // Touching the tools/call view without adding filters must not trip the validation.
+ _ = options.Filters.Request.CallToolWithAlternateFilters;
+ _ = options.Filters.Request.AlternateResultFilters.GetOrAdd(RequestMethods.PromptsGet);
+
+ await using var server = McpServer.Create(transport, options, LoggerFactory);
+
+ Assert.NotNull(server);
+ }
+}