Skip to content

Commit e1e81e5

Browse files
halter73Copilot
andcommitted
Route MRTR backcompat resolver requests through destination-bound transport to avoid GET-stream race
Server-side InputRequiredException backcompat resolver was calling this.ElicitAsync / SampleAsync / RequestRootsAsync, which routes outgoing requests through the session-level _transport. StreamableHttpServerTransport.SendMessageAsync silently drops messages when no GET request has arrived yet, so under CI load the McpClient's async GET startup could race with the in-flight tools/call, causing the resolver to wait on a TCS forever. Route the outgoing requests through CreateDestinationBoundServer(request) instead, matching the pattern used by tool-initiated server.SampleAsync etc. Outgoing JSON-RPC then flows back through the original POST's response stream (always open during the tool call) instead of the standalone GET. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c2467d3 commit e1e81e5

1 file changed

Lines changed: 23 additions & 13 deletions

File tree

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,13 @@ internal bool IsStatefulSession() =>
12081208
}
12091209

12101210
// Resolve each input request by sending the corresponding JSON-RPC call to the client.
1211-
var inputResponses = await ResolveInputRequestsAsync(inputRequests, cancellationToken).ConfigureAwait(false);
1211+
// Route the outgoing requests via the same DestinationBoundMcpServer used for normal tool
1212+
// handlers, so they go through the POST's response stream (RelatedTransport) rather than
1213+
// the session-level transport. Without this, the messages can race with the client's GET
1214+
// stream startup and be silently dropped by StreamableHttpServerTransport.SendMessageAsync
1215+
// when no GET request has arrived yet.
1216+
var destinationServer = CreateDestinationBoundServer(request);
1217+
var inputResponses = await ResolveInputRequestsAsync(destinationServer, inputRequests, cancellationToken).ConfigureAwait(false);
12121218

12131219
// Reconstruct request params with inputResponses and requestState for the retry.
12141220
var paramsObj = request.Params?.DeepClone() as JsonObject ?? new JsonObject();
@@ -1233,12 +1239,15 @@ internal bool IsStatefulSession() =>
12331239

12341240
/// <summary>
12351241
/// Resolves a batch of MRTR input requests concurrently by dispatching each as a standard
1236-
/// JSON-RPC request to the client. On the first failure all remaining handlers are cancelled
1237-
/// so user-facing flows (sampling/elicitation prompts) don't keep running once the caller has
1238-
/// given up, and exceptions from late-completing tasks are observed before the original
1239-
/// exception is rethrown.
1242+
/// JSON-RPC request to the client. The requests are routed via <paramref name="destinationServer"/>
1243+
/// so they go out through the POST's response stream (matching the behavior of tool-initiated
1244+
/// server-to-client requests like <c>server.SampleAsync</c>) and avoid racing with the client's
1245+
/// GET stream startup. On the first failure all remaining handlers are cancelled so user-facing
1246+
/// flows (sampling/elicitation prompts) don't keep running once the caller has given up, and
1247+
/// exceptions from late-completing tasks are observed before the original exception is rethrown.
12401248
/// </summary>
1241-
private async Task<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
1249+
private static async Task<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
1250+
McpServer destinationServer,
12421251
IDictionary<string, InputRequest> inputRequests,
12431252
CancellationToken cancellationToken)
12441253
{
@@ -1248,7 +1257,7 @@ private async Task<IDictionary<string, InputResponse>> ResolveInputRequestsAsync
12481257
int i = 0;
12491258
foreach (var kvp in inputRequests)
12501259
{
1251-
keyed[i++] = (kvp.Key, ResolveInputRequestAsync(kvp.Value, linkedCts.Token));
1260+
keyed[i++] = (kvp.Key, ResolveInputRequestAsync(destinationServer, kvp.Value, linkedCts.Token));
12521261
}
12531262

12541263
try
@@ -1279,28 +1288,29 @@ private async Task<IDictionary<string, InputResponse>> ResolveInputRequestsAsync
12791288

12801289
/// <summary>
12811290
/// Resolves a single MRTR <see cref="InputRequest"/> by dispatching it as a standard JSON-RPC
1282-
/// request to the client. This is the server-side mirror of the client's input resolution logic,
1283-
/// used for backward compatibility when the client doesn't support MRTR.
1291+
/// request to the client via <paramref name="destinationServer"/>. This is the server-side mirror
1292+
/// of the client's input resolution logic, used for backward compatibility when the client doesn't
1293+
/// support MRTR.
12841294
/// </summary>
1285-
private async Task<InputResponse> ResolveInputRequestAsync(InputRequest inputRequest, CancellationToken cancellationToken)
1295+
private static async Task<InputResponse> ResolveInputRequestAsync(McpServer destinationServer, InputRequest inputRequest, CancellationToken cancellationToken)
12861296
{
12871297
switch (inputRequest.Method)
12881298
{
12891299
case RequestMethods.ElicitationCreate:
12901300
var elicitParams = inputRequest.ElicitationParams
12911301
?? throw new McpException("Failed to deserialize elicitation parameters from MRTR input request.");
1292-
var elicitResult = await ElicitAsync(elicitParams, cancellationToken).ConfigureAwait(false);
1302+
var elicitResult = await destinationServer.ElicitAsync(elicitParams, cancellationToken).ConfigureAwait(false);
12931303
return InputResponse.FromElicitResult(elicitResult);
12941304

12951305
case RequestMethods.SamplingCreateMessage:
12961306
var samplingParams = inputRequest.SamplingParams
12971307
?? throw new McpException("Failed to deserialize sampling parameters from MRTR input request.");
1298-
var samplingResult = await SampleAsync(samplingParams, cancellationToken).ConfigureAwait(false);
1308+
var samplingResult = await destinationServer.SampleAsync(samplingParams, cancellationToken).ConfigureAwait(false);
12991309
return InputResponse.FromSamplingResult(samplingResult);
13001310

13011311
case RequestMethods.RootsList:
13021312
var rootsParams = inputRequest.RootsParams ?? new ListRootsRequestParams();
1303-
var rootsResult = await RequestRootsAsync(rootsParams, cancellationToken).ConfigureAwait(false);
1313+
var rootsResult = await destinationServer.RequestRootsAsync(rootsParams, cancellationToken).ConfigureAwait(false);
13041314
return InputResponse.FromRootsResult(rootsResult);
13051315

13061316
default:

0 commit comments

Comments
 (0)