Fix race condition in StdioClientTransport on .NET Framework#996
Merged
stephentoub merged 3 commits intomainfrom Nov 24, 2025
Merged
Fix race condition in StdioClientTransport on .NET Framework#996stephentoub merged 3 commits intomainfrom
stephentoub merged 3 commits intomainfrom
Conversation
Add thread synchronization around Console.InputEncoding modification to prevent race conditions when multiple transports are created concurrently on .NET Framework. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Diagnose and fix flakiness in ConnectAndPing_Stdio test
Fix race condition in StdioClientTransport on .NET Framework
Nov 20, 2025
stephentoub
reviewed
Nov 20, 2025
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
stephentoub
approved these changes
Nov 20, 2025
ericstj
reviewed
Nov 20, 2025
eiriktsarpalis
approved these changes
Nov 24, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix flaky ConnectAndPing_Stdio test on .NET Framework 4.7.2
This PR fixes a race condition in
StdioClientTransportthat causes test flakiness on .NET Framework 4.7.2.Problem
The
ConnectAndPing_Stdio(clientId: "everything")test was failing intermittently on .NET Framework 4.7.2 with a 60-second timeout during client initialization:The test passed consistently on .NET 8.0, 9.0, and 10.0, but failed only on .NET Framework 4.7.2.
Root Cause
On .NET Framework,
StdioClientTransport.ConnectAsynctemporarily modifies the globalConsole.InputEncodingto ensure UTF-8 encoding for stdin. This is necessary because .NET Framework'sProcessStartInfodoesn't have aStandardInputEncodingproperty.When multiple tests run in parallel (which they do by default in xUnit), they create a race condition:
At T8, Test B restores
Console.InputEncodingto UTF-8 instead of the original Default encoding. This leaves the console in an incorrect state. Subsequent process starts may get the wrong encoding, causing the spawned npx process to receive corrupted input, leading to communication failures and timeout errors.Solution
Added a static lock object (
s_consoleEncodingLock) that serializes access to theConsole.InputEncodingmodification on .NET Framework. This ensures atomic execution of:The lock is only compiled for .NET Framework builds (using
#if !NET) since modern .NET versions haveProcessStartInfo.StandardInputEncodingand don't need this workaround.Changes
s_consoleEncodingLockstatic field (lines 28-32) - only on .NET Frameworklockstatement (lines 170-182) - only on .NET Framework#elseblock (lines 168-169)Testing
Impact
This is a minimal, surgical fix that:
Original prompt
A test failed in CI. Please diagnose the flakiness and submit a PR to fix it.
failed ModelContextProtocol.Tests.ClientIntegrationTests.ConnectAndPing_Stdio(clientId: "everything") (1m 00s 092ms)
System.TimeoutException : Initialization timed out
---- System.OperationCanceledException : The operation was canceled.
from D:\a\csharp-sdk\csharp-sdk\artifacts\bin\ModelContextProtocol.Tests\Debug\net472\ModelContextProtocol.Tests.exe (net48|x64)
Xunit.MicrosoftTestingPlatform.XunitException: System.TimeoutException : Initialization timed out
---- System.OperationCanceledException : The operation was canceled.
at ModelContextProtocol.Client.McpClientImpl.d__27.MoveNext() in //src/ModelContextProtocol.Core/Client/McpClientImpl.cs:197
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at ModelContextProtocol.Client.McpClientImpl.d__27.MoveNext() in //src/ModelContextProtocol.Core/Client/McpClientImpl.cs:204
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
at ModelContextProtocol.Client.McpClient.d__6.MoveNext() in //src/ModelContextProtocol.Core/Client/McpClient.Methods.cs:42
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at ModelContextProtocol.Client.McpClient.d__6.MoveNext() in //src/ModelContextProtocol.Core/Client/McpClient.Methods.cs:47
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter
1.GetResult() at ModelContextProtocol.Tests.ClientIntegrationTests.<ConnectAndPing_Stdio>d__6.MoveNext() in /_/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs:36 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) ----- Inner Stack Trace ----- at System.Threading.CancellationToken.ThrowOperationCanceledException() at System.Threading.CancellationToken.ThrowIfCancellationRequested() at System.Threading.Tasks.TaskExtensions.<WaitAsync>d__3.MoveNext() in /_/src/Common/Polyfills/System/Threading/Tasks/TaskExtensions.cs:46 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult() at System.Threading.Tasks.TaskExtensions.<WaitAsync>d__21.MoveNext() in //src/Common/Polyfills/System/Threading/Tasks/TaskExtensions.cs:20--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable
1.ConfiguredTaskAwaiter.GetResult() at ModelContextProtocol.McpSessionHandler.<SendRequestAsync>d__33.MoveNext() in /_/src/ModelContextProtocol.Core/McpSessionHandler.cs:449 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.ConfiguredTaskAwaitable1.ConfiguredTaskAwaiter.GetResult()at ModelContextProtocol.McpSession.d__9`2.MoveNext() in //src/ModelContextProtocol.Core/McpSession.Methods.cs:71
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.E...
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.