diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index a9a5dddfb..c857ec118 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -411,8 +411,10 @@ public async ValueTask> ElicitAsync( Meta = options?.GetMetaForRequest(), }; - ThrowIfElicitationUnsupported(request); - + // The untyped overload runs the capability check itself, and skips it when an + // outgoing-request interceptor is installed because that channel delivers the + // request instead. Checking here would make this overload throw where the + // untyped one succeeds. var raw = await ElicitAsync(request, cancellationToken).ConfigureAwait(false); if (!raw.IsAccepted || raw.Content is null) diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index e54f40dcb..0370a7b81 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -258,6 +258,52 @@ public async Task ElicitAsync_Should_SendRequest() await runTask; } + private sealed class ElicitationTarget + { + public string Answer { get; set; } = string.Empty; + } + + [Fact] + public async Task ElicitAsync_Generic_Should_Use_Interceptor_When_Client_Does_Not_Support_Elicitation() + { + // An installed interceptor delivers the request over its own channel, so the + // untyped overload skips the capability check on purpose. The generic overload + // has to reach that branch rather than throwing ahead of it. + await using var transport = new TestServerTransport(); + await using var server = McpServer.Create(transport, _options, LoggerFactory); + var runTask = server.RunAsync(TestContext.Current.CancellationToken); + await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); + + var intercepted = false; +#pragma warning disable MCPEXP002 // exercises the experimental outgoing-request interception seam + var interceptingServer = server.WithOutgoingRequestInterceptor((method, _, _) => + { + intercepted = true; + Assert.Equal(RequestMethods.ElicitationCreate, method); + return new ValueTask(JsonSerializer.SerializeToNode( + new ElicitResult + { + Action = "accept", + Content = new Dictionary + { + ["answer"] = JsonSerializer.SerializeToElement("yes"), + }, + }, + McpJsonUtilities.DefaultOptions)); + }); +#pragma warning restore MCPEXP002 + + var result = await interceptingServer.ElicitAsync( + "Proceed?", cancellationToken: TestContext.Current.CancellationToken); + + Assert.True(intercepted); + Assert.True(result.IsAccepted); + Assert.Equal("yes", result.Content?.Answer); + + await transport.DisposeAsync(); + await runTask; + } + [Fact] public async Task Can_Handle_Ping_Requests() {