Skip to content

Let the typed elicitation overload reach the interceptor - #1853

Open
ConnorMoss02 wants to merge 1 commit into
modelcontextprotocol:mainfrom
ConnorMoss02:fix/typed-elicit-interceptor
Open

Let the typed elicitation overload reach the interceptor#1853
ConnorMoss02 wants to merge 1 commit into
modelcontextprotocol:mainfrom
ConnorMoss02:fix/typed-elicit-interceptor

Conversation

@ConnorMoss02

@ConnorMoss02 ConnorMoss02 commented Sep 4, 2026

Copy link
Copy Markdown

Closes #1854

Problem

ElicitAsync<T> runs the capability check itself before delegating:

ThrowIfElicitationUnsupported(request);

var raw = await ElicitAsync(request, cancellationToken).ConfigureAwait(false);

The untyped overload it delegates to skips that check when an outgoing-request interceptor is installed, and says so (McpServer.Methods.cs:348-351):

Capability checks (ThrowIfElicitationUnsupported) are intentionally skipped because the interceptor's alternate channel is responsible for delivering the request to the client.

So with an interceptor installed and a client that does not advertise elicitation, the untyped overload succeeds and the typed one throws Elicitation is not supported in stateless mode. The interceptor is how the Tasks package delivers outgoing requests, so a server eliciting from a background task can use one API and not the other.

SampleAsync (:96-107) and RequestRootsAsync (:302-312) both check the interceptor first. This was the only overload that did not.

Change

Remove the early check and let the untyped overload run it, with a comment recording why it belongs there. One line of behavior.

Test

ElicitAsync_Generic_Should_Use_Interceptor_When_Client_Does_Not_Support_Elicitation initializes with empty ClientCapabilities, installs an interceptor that returns an accepted ElicitResult, and asserts the typed overload reaches the interceptor and deserializes the payload.

This is the first test in the repo to exercise WithOutgoingRequestInterceptor. grep finds no other reference under tests/.

Verified red then green: the test fails on unmodified main and passes with the change.

Validation

dotnet test tests/ModelContextProtocol.Tests/ --framework net10.0

2360 passed, 2 failed, 3 skipped. The two failures are DockerEverythingServerTests.Sampling_Sse_EverythingServer and ConnectAndReceiveMessage_EverythingServerWithSse, which fail the same way on an unmodified clean checkout without a Docker daemon. Baseline before this change was 2359 passed / 2 failed, so this adds one passing test and changes nothing else.

Note on scope

WithOutgoingRequestInterceptor is MCPEXP002, so this is an inconsistency between two overloads of an experimental seam rather than a break in stable API. The test suppresses the diagnostic with a reason comment, matching the existing pattern in McpServerTaskTests and TaskCallToolFilterCompositionTests.

Note

This pull request was prepared with AI assistance.

ElicitAsync<T> called ThrowIfElicitationUnsupported before delegating to
the untyped overload, which skips that check when an outgoing-request
interceptor is installed because the interceptor delivers the request
instead. The typed overload therefore threw where the untyped one
succeeded, so a server eliciting from a background task could use one API
and not the other.

Leave the check to the untyped overload, which already runs it on the
path that needs it. SampleAsync and RequestRootsAsync already order it
this way; this was the only outlier.

Add the first test to exercise WithOutgoingRequestInterceptor at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ElicitAsync<T> throws where the untyped ElicitAsync succeeds under an outgoing-request interceptor

1 participant