|
1 | 1 | using Microsoft.Extensions.Logging; |
2 | 2 | using ModelContextProtocol.Protocol; |
3 | 3 | using ModelContextProtocol.Server; |
| 4 | +using System.Diagnostics; |
4 | 5 | using System.Diagnostics.CodeAnalysis; |
5 | 6 | using System.Text.Json; |
6 | 7 | using System.Text.Json.Nodes; |
@@ -52,20 +53,49 @@ public static async Task<McpClient> CreateAsync( |
52 | 53 | { |
53 | 54 | await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false); |
54 | 55 | } |
55 | | - catch |
| 56 | + catch (Exception ex) when (ex is not OperationCanceledException and not ClientTransportClosedException) |
56 | 57 | { |
57 | | - try |
| 58 | + // ConnectAsync already disposed the session (which includes awaiting Completion). |
| 59 | + // Check if the transport provided structured completion details indicating |
| 60 | + // why the transport closed that aren't already in the original exception chain. |
| 61 | + Debug.Assert(clientSession.Completion.IsCompleted, "Completion should already be finished after ConnectAsync's DisposeAsync."); |
| 62 | + var completionDetails = await clientSession.Completion.ConfigureAwait(false); |
| 63 | + |
| 64 | + // If the transport closed with a non-graceful error (e.g., server process exited) |
| 65 | + // and the completion details carry an exception that's NOT already in the original |
| 66 | + // exception chain, throw a ClientTransportClosedException with the structured details so |
| 67 | + // callers can programmatically inspect the closure reason (exit code, stderr, etc.). |
| 68 | + // When the same exception is already in the chain (e.g., HttpRequestException from |
| 69 | + // an HTTP transport), the original exception is more appropriate to re-throw. |
| 70 | + if (completionDetails.Exception is { } detailsException && |
| 71 | + !ExceptionChainContains(ex, detailsException)) |
58 | 72 | { |
59 | | - await clientSession.DisposeAsync().ConfigureAwait(false); |
| 73 | + throw new ClientTransportClosedException(completionDetails); |
60 | 74 | } |
61 | | - catch { } // allow the original exception to propagate |
62 | 75 |
|
63 | 76 | throw; |
64 | 77 | } |
65 | 78 |
|
66 | 79 | return clientSession; |
67 | 80 | } |
68 | 81 |
|
| 82 | + /// <summary> |
| 83 | + /// Returns <see langword="true"/> if <paramref name="target"/> is the same object as |
| 84 | + /// <paramref name="exception"/> or any exception in its <see cref="Exception.InnerException"/> chain. |
| 85 | + /// </summary> |
| 86 | + private static bool ExceptionChainContains(Exception exception, Exception target) |
| 87 | + { |
| 88 | + for (Exception? current = exception; current is not null; current = current.InnerException) |
| 89 | + { |
| 90 | + if (ReferenceEquals(current, target)) |
| 91 | + { |
| 92 | + return true; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + return false; |
| 97 | + } |
| 98 | + |
69 | 99 | /// <summary> |
70 | 100 | /// Recreates an <see cref="McpClient"/> using an existing transport session without sending a new initialize request. |
71 | 101 | /// </summary> |
|
0 commit comments