Skip to content

Commit e02b109

Browse files
authored
Merge branch 'main' into copilot/update-client-oauth-provider
2 parents d293768 + 54caccc commit e02b109

18 files changed

Lines changed: 279 additions & 64 deletions

.github/workflows/ci-build-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ jobs:
4343
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
4444

4545
- name: 🔧 Set up .NET
46-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
46+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
4747
with:
4848
dotnet-version: |
4949
10.0.x
5050
9.0.x
5151
5252
- name: 🔧 Set up Node.js
53-
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
53+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
5454
with:
5555
node-version: '20'
5656

.github/workflows/ci-code-coverage.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1414
- name: Setup .NET
15-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
15+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
1616
with:
1717
dotnet-version: |
1818
10.0.x
1919
9.0.x
2020
2121
- name: Download test results
22-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
22+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
2323
with:
2424
pattern: testresults-*
2525

.github/workflows/copilot-setup-steps.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1515

1616
- name: Install .NET SDK
17-
uses: actions/setup-dotnet@v5.1.0
17+
uses: actions/setup-dotnet@v5.2.0
1818
with:
1919
global-json-file: global.json
2020

.github/workflows/docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3131

3232
- name: .NET Setup
33-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
33+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
3434
with:
3535
dotnet-version: |
3636
10.0.x
@@ -46,4 +46,4 @@ jobs:
4646

4747
- name: Deploy to GitHub Pages
4848
id: deployment
49-
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
49+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0

.github/workflows/release.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
5151

5252
- name: Set up .NET
53-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
53+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
5454
with:
5555
dotnet-version: |
5656
10.0.x
@@ -76,7 +76,7 @@ jobs:
7676
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
7777

7878
- name: Setup .NET
79-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
79+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
8080
with:
8181
dotnet-version: |
8282
10.0.x
@@ -106,12 +106,12 @@ jobs:
106106
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
107107

108108
- name: Setup .NET
109-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
109+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
110110
with:
111111
dotnet-version: 10.0.x
112112

113113
- name: Download build artifacts
114-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
114+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
115115

116116
- name: Authenticate to GitHub registry
117117
run: dotnet nuget add source
@@ -141,7 +141,7 @@ jobs:
141141
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
142142

143143
- name: Download build artifacts
144-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
144+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
145145

146146
- name: Upload release asset
147147
run: gh release upload ${{ github.event.release.tag_name }}
@@ -161,12 +161,12 @@ jobs:
161161
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
162162

163163
- name: Setup .NET
164-
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
164+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
165165
with:
166166
dotnet-version: 10.0.x
167167

168168
- name: Download build artifacts
169-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
169+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
170170

171171
- name: Publish to NuGet.org (Releases only)
172172
run: dotnet nuget push

Directory.Packages.props

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
<!-- Testing dependencies -->
6464
<ItemGroup>
6565
<PackageVersion Include="Anthropic" Version="12.9.0" />
66-
<PackageVersion Include="coverlet.collector" Version="8.0.0">
66+
<PackageVersion Include="coverlet.collector" Version="8.0.1">
6767
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
6868
<PrivateAssets>all</PrivateAssets>
6969
</PackageVersion>
@@ -76,12 +76,12 @@
7676
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.1.0" />
7777
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
7878
<PackageVersion Include="Moq" Version="4.20.72" />
79-
<PackageVersion Include="OpenTelemetry" Version="1.15.0" />
80-
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.0" />
81-
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
79+
<PackageVersion Include="OpenTelemetry" Version="1.15.1" />
80+
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.1" />
81+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.1" />
8282
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
83-
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
84-
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
83+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.1" />
84+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.1" />
8585
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
8686
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0" />
8787
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using ModelContextProtocol.Protocol;
2+
using System.Threading.Channels;
3+
4+
namespace ModelContextProtocol.Client;
5+
6+
/// <summary>
7+
/// An <see cref="IOException"/> that indicates the client transport was closed, carrying
8+
/// structured <see cref="ClientCompletionDetails"/> about why the closure occurred.
9+
/// </summary>
10+
/// <remarks>
11+
/// <para>
12+
/// This exception is thrown when an MCP transport closes, either during initialization
13+
/// (e.g., from <see cref="McpClient.CreateAsync"/>) or during an active session.
14+
/// Callers can catch this exception to access the <see cref="Details"/> property
15+
/// for structured information about the closure.
16+
/// </para>
17+
/// <para>
18+
/// For stdio-based transports, the <see cref="Details"/> will be a
19+
/// <see cref="StdioClientCompletionDetails"/> instance providing access to the
20+
/// server process exit code, process ID, and standard error output.
21+
/// </para>
22+
/// <para>
23+
/// Custom <see cref="ITransport"/> implementations can provide their own
24+
/// <see cref="ClientCompletionDetails"/>-derived types by completing their
25+
/// <see cref="ChannelWriter{T}"/> with this exception.
26+
/// </para>
27+
/// </remarks>
28+
public sealed class ClientTransportClosedException(ClientCompletionDetails details) :
29+
IOException(details.Exception?.Message ?? "The transport was closed.", details.Exception)
30+
{
31+
/// <summary>
32+
/// Gets the structured details about why the transport was closed.
33+
/// </summary>
34+
/// <remarks>
35+
/// The concrete type of the returned <see cref="ClientCompletionDetails"/> depends on
36+
/// the transport that was used. For example, <see cref="StdioClientCompletionDetails"/>
37+
/// for stdio-based transports and <see cref="HttpClientCompletionDetails"/> for HTTP-based transports.
38+
/// </remarks>
39+
public ClientCompletionDetails Details { get; } = details;
40+
}

src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.Logging;
22
using ModelContextProtocol.Protocol;
33
using ModelContextProtocol.Server;
4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Text.Json;
67
using System.Text.Json.Nodes;
@@ -52,20 +53,49 @@ public static async Task<McpClient> CreateAsync(
5253
{
5354
await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false);
5455
}
55-
catch
56+
catch (Exception ex) when (ex is not OperationCanceledException and not ClientTransportClosedException)
5657
{
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))
5872
{
59-
await clientSession.DisposeAsync().ConfigureAwait(false);
73+
throw new ClientTransportClosedException(completionDetails);
6074
}
61-
catch { } // allow the original exception to propagate
6275

6376
throw;
6477
}
6578

6679
return clientSession;
6780
}
6881

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+
6999
/// <summary>
70100
/// Recreates an <see cref="McpClient"/> using an existing transport session without sending a new initialize request.
71101
/// </summary>

src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,15 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
5858

5959
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false);
6060
}
61+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
62+
{
63+
throw;
64+
}
6165
catch (Exception ex)
6266
{
6367
LogTransportConnectFailed(Name, ex);
6468
await CloseAsync().ConfigureAwait(false);
65-
throw new InvalidOperationException("Failed to connect transport", ex);
69+
throw new IOException("Failed to connect transport.", ex);
6670
}
6771
}
6872

@@ -125,7 +129,7 @@ private async Task CloseAsync()
125129
}
126130
finally
127131
{
128-
SetDisconnected(new TransportClosedException(new HttpClientCompletionDetails()));
132+
SetDisconnected(new ClientTransportClosedException(new HttpClientCompletionDetails()));
129133
}
130134
}
131135

@@ -186,7 +190,7 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
186190
}
187191
else
188192
{
189-
SetDisconnected(new TransportClosedException(new HttpClientCompletionDetails
193+
SetDisconnected(new ClientTransportClosedException(new HttpClientCompletionDetails
190194
{
191195
HttpStatusCode = failureStatusCode,
192196
Exception = ex,
@@ -199,7 +203,7 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
199203
}
200204
finally
201205
{
202-
SetDisconnected(new TransportClosedException(new HttpClientCompletionDetails()));
206+
SetDisconnected(new ClientTransportClosedException(new HttpClientCompletionDetails()));
203207
}
204208
}
205209

0 commit comments

Comments
 (0)