Skip to content

Make TransportClosedException public and unify transport exception handling#1467

Open
Copilot wants to merge 8 commits intomainfrom
copilot/fix-client-completion-details-access
Open

Make TransportClosedException public and unify transport exception handling#1467
Copilot wants to merge 8 commits intomainfrom
copilot/fix-client-completion-details-access

Conversation

Copy link
Contributor

Copilot AI commented Mar 25, 2026

When a stdio server process exits during McpClient.CreateAsync (before completing the initialization handshake), callers cannot access StdioClientCompletionDetails (ExitCode, ProcessId, StandardErrorTail). The McpClient is created and disposed internally by CreateAsync, so Completion is never reachable. The only workaround is parsing IOException messages or reflecting on internal types.

Changes

  • Make TransportClosedException public — was already internal with a comment saying "could be made public in the future." It extends IOException, so existing catch blocks are unaffected. Carries a Details property of type ClientCompletionDetails.

  • Propagate channel completion exception to pending requests (McpSessionHandler.ProcessMessagesCoreAsync) — instead of always failing pending requests with a generic IOException("The server shut down unexpectedly."), propagate the TransportClosedException from the transport's channel completion when available.

  • Throw TransportClosedException from CreateAsync when completion details add value — after initialization failure, check the session's completion details. Only throw TransportClosedException when the details carry an exception not already present in the original exception chain (reference equality walk). This gives stdio callers structured exit info while preserving existing HTTP exception behavior (e.g., HttpRequestException with StatusCode).

  • Propagate caller-triggered OperationCanceledException in transports — when a caller's CancellationToken triggers an OperationCanceledException during a transport send or connect, re-throw it instead of wrapping it in IOException. Applied to StreamClientSessionTransport.SendMessageAsync, StreamServerTransport.SendMessageAsync, and SseClientSessionTransport.ConnectAsync. Also changed SseClientSessionTransport.ConnectAsync from throwing InvalidOperationException to IOException for consistency (connection failure is I/O, not invalid state).

Usage

try
{
    await using var client = await McpClient.CreateAsync(stdioTransport);
}
catch (TransportClosedException ex) when (ex.Details is StdioClientCompletionDetails stdio)
{
    // Structured access — no string parsing needed
    Console.WriteLine($"Exit code: {stdio.ExitCode}");   // e.g., 42 = Feature Disabled
    Console.WriteLine($"PID: {stdio.ProcessId}");
    Console.WriteLine($"Stderr: {string.Join("\n", stdio.StandardErrorTail ?? [])}");
}

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 25, 2026 23:14
… through CreateAsync

- Make TransportClosedException public with proper XML documentation
- Update ProcessMessagesCoreAsync to propagate channel completion exception
  to pending requests instead of a generic IOException
- Update McpClient.CreateAsync to throw TransportClosedException with structured
  completion details when the transport closes during initialization, while
  preserving existing behavior for cases where the original exception already
  carries the relevant information
- Add unit tests for TransportClosedException construction and details access
- Add integration test verifying TransportClosedException with
  StdioClientCompletionDetails when CreateAsync fails

Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/dfc5fff0-2e51-4b55-a663-e9cd3477cad4

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix access to StdioClientCompletionDetails on McpClient.CreateAsync failure Make TransportClosedException public to expose ClientCompletionDetails when CreateAsync fails Mar 25, 2026
Copilot AI requested a review from stephentoub March 25, 2026 23:20
…eAsync

ConnectAsync already calls DisposeAsync (which awaits Completion) before
re-throwing, so the session is already disposed and Completion is resolved
by the time CreateAsync's catch block runs.

Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/dc4efc81-4b40-45c3-a743-59ade1c1c6df

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@stephentoub stephentoub marked this pull request as ready for review March 26, 2026 15:24
@stephentoub stephentoub requested a review from halter73 March 26, 2026 15:24
GetCompletionDetailsAsync already catches all exceptions internally and
always returns a ClientCompletionDetails, so Completion never faults.

Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/e1a0f673-063d-4707-916d-51824e00b557

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
ConnectAsync's DisposeAsync already awaits Completion, so by the time
CreateAsync's catch block runs, it's guaranteed to be completed. The
assert documents this invariant.

Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/69e63301-5b59-4d6e-bf84-d1e02e421c67

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@halter73
Copy link
Contributor

@copilot While not exactly the same thing, would it make sense to incorporate any of my fixes from https://github.com/modelcontextprotocol/csharp-sdk/tree/halter73/transport-exceptions into this PR? If it makes sense to just merge these changes into this PR, it'd definitely save me time, so I'd appreciate it 😆

@halter73
Copy link
Contributor

@copilot To be clear, I'm not talking about anything more than the last two commits in https://github.com/modelcontextprotocol/csharp-sdk/tree/halter73/transport-exceptions. Ignore all the previous MRTR stuff

Split the TransportClosedDuringInit test into two deterministic tests:
- ChannelClosed test: SendMessageAsync returns successfully, only the
  read channel completes with TransportClosedException. Verifies the
  structured completion details and error log.
- SendFails test: SendMessageAsync throws IOException without
  TransportClosedException in the channel. Verifies the original
  IOException propagates and error log is produced.

Make test class inherit from LoggedTest to verify initialization error
logs via MockLoggerProvider.

Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/97b170d4-86a2-4f08-998c-65b1c13dbfdb

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Cherry-pick from halter73/transport-exceptions (fd2d71e, 19a1f2c):

- StreamClientSessionTransport.SendMessageAsync: re-throw
  OperationCanceledException when the caller's token triggers it,
  instead of wrapping in IOException.
- StreamServerTransport.SendMessageAsync: same pattern.
- SseClientSessionTransport.ConnectAsync: add OperationCanceledException
  guard and change InvalidOperationException to IOException for
  consistency (connection failure is I/O, not invalid state).

Co-authored-by: Stephen Halter <halter73@gmail.com>

Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/f1ba1333-443c-4239-8205-c32db49e9f22

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot While not exactly the same thing, would it make sense to incorporate any of my fixes from https://github.com/modelcontextprotocol/csharp-sdk/tree/halter73/transport-exceptions...

Incorporated the last two commits from halter73/transport-exceptions (fd2d71e, 19a1f2c) in 2a5e1d7:

  • StreamClientSessionTransport.SendMessageAsync: re-throw OperationCanceledException when the caller's token triggers it
  • StreamServerTransport.SendMessageAsync: same pattern
  • SseClientSessionTransport.ConnectAsync: add OperationCanceledException guard + change InvalidOperationException to IOException

Copilot AI changed the title Make TransportClosedException public to expose ClientCompletionDetails when CreateAsync fails Make TransportClosedException public and unify transport exception handling Mar 26, 2026
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.

ClientCompletionDetails not accessible when McpClient.CreateAsync fails for stdio transport

3 participants