Skip to content

Commit ecf63e3

Browse files
committed
Merge branch 'localden/experimental' of https://github.com/modelcontextprotocol/csharp-sdk into localden/experimental
2 parents 2b4fb27 + 39f0588 commit ecf63e3

File tree

22 files changed

+200
-99
lines changed

22 files changed

+200
-99
lines changed

Directory.Packages.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
<!-- Product dependencies netstandard -->
1010
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
11-
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
1211
<PackageVersion Include="Microsoft.Bcl.Memory" Version="$(System9Version)" />
1312
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
1413
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl>
77
<RepositoryType>git</RepositoryType>
88
<VersionPrefix>0.2.0</VersionPrefix>
9-
<VersionSuffix>preview.3</VersionSuffix>
9+
<VersionSuffix>preview.4</VersionSuffix>
1010
<Authors>ModelContextProtocolOfficial</Authors>
1111
<Copyright>© Anthropic and Contributors.</Copyright>
1212
<PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags>

src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationExceptions.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,9 @@ public static AuthenticationBuilder AddMcp(
3939
string displayName,
4040
Action<McpAuthenticationOptions>? configureOptions = null)
4141
{
42-
if (configureOptions != null)
43-
{
44-
if (authenticationScheme == McpAuthenticationDefaults.AuthenticationScheme)
45-
{
46-
builder.Services.Configure(configureOptions);
47-
}
48-
else
49-
{
50-
builder.Services.Configure(authenticationScheme, configureOptions);
51-
}
52-
}
53-
5442
return builder.AddScheme<McpAuthenticationOptions, McpAuthenticationHandler>(
5543
authenticationScheme,
5644
displayName,
57-
options => { }); // No-op to avoid overriding
45+
configureOptions); // No-op to avoid overriding
5846
}
5947
}

src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ private string GetAbsoluteResourceMetadataUri()
7070

7171
if (resourceMetadataUri.IsAbsoluteUri)
7272
{
73-
return resourceMetadataUri.ToString();
73+
return currentPath;
7474
}
7575

7676
// For relative URIs, combine with the base URL

src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationOptions.cs

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -87,41 +87,6 @@ public Func<HttpContext, ProtectedResourceMetadata>? ProtectedResourceMetadataPr
8787
set => _resourceMetadataProvider = value;
8888
}
8989

90-
/// <summary>
91-
/// Sets a static resource metadata instance that will be returned for all requests.
92-
/// </summary>
93-
/// <param name="metadata">The static resource metadata to use.</param>
94-
/// <returns>The current options instance for method chaining.</returns>
95-
/// <remarks>
96-
/// This is a convenience method equivalent to setting the <see cref="ResourceMetadata"/> property.
97-
/// </remarks>
98-
/// <exception cref="ArgumentNullException">Thrown when metadata is null.</exception>
99-
/// <exception cref="ArgumentException">Thrown when the Resource property of the metadata is null.</exception>
100-
public McpAuthenticationOptions UseStaticResourceMetadata(ProtectedResourceMetadata metadata)
101-
{
102-
ArgumentNullException.ThrowIfNull(metadata);
103-
if (metadata.Resource == null)
104-
{
105-
throw new ArgumentException("The Resource property of the metadata cannot be null. A valid resource URI is required.", nameof(metadata));
106-
}
107-
108-
ResourceMetadata = metadata;
109-
return this;
110-
}
111-
112-
/// <summary>
113-
/// Sets a delegate to dynamically provide resource metadata for each request.
114-
/// </summary>
115-
/// <param name="provider">A delegate that returns resource metadata for a given HTTP context.</param>
116-
/// <returns>The current options instance for method chaining.</returns>
117-
/// <remarks>
118-
/// This is a convenience method equivalent to setting the <see cref="ProtectedResourceMetadataProvider"/> property.
119-
/// </remarks>
120-
public McpAuthenticationOptions UseDynamicResourceMetadata(Func<HttpContext, ProtectedResourceMetadata> provider)
121-
{
122-
ProtectedResourceMetadataProvider = provider ?? throw new ArgumentNullException(nameof(provider));
123-
return this;
124-
}
12590

12691
/// <summary>
12792
/// Gets the resource metadata for the current request.

src/ModelContextProtocol.Core/Authentication/GenericOAuthProvider.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public class GenericOAuthProvider : IMcpCredentialProvider
5050
private readonly List<string> _scopes;
5151
private readonly string _clientId;
5252
private readonly string _clientSecret;
53-
private readonly HttpClient _httpClient; private readonly AuthorizationHelpers _authorizationHelpers;
53+
private readonly HttpClient _httpClient;
54+
private readonly AuthorizationHelpers _authorizationHelpers;
5455
private readonly ILogger _logger;
5556
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
5657
private readonly AuthorizationUrlHandler _authorizationUrlHandler;
@@ -60,7 +61,9 @@ public class GenericOAuthProvider : IMcpCredentialProvider
6061

6162
private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true };
6263
private TokenContainer? _token;
63-
private AuthorizationServerMetadata? _authServerMetadata; /// <summary>
64+
private AuthorizationServerMetadata? _authServerMetadata;
65+
66+
/// <summary>
6467
/// Initializes a new instance of the <see cref="GenericOAuthProvider"/> class.
6568
/// </summary>
6669
/// <param name="serverUrl">The MCP server URL.</param>

src/ModelContextProtocol.Core/Client/McpClient.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,12 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
129129
try
130130
{
131131
// Send initialize request
132+
string requestProtocol = _options.ProtocolVersion ?? McpSession.LatestProtocolVersion;
132133
var initializeResponse = await this.SendRequestAsync(
133134
RequestMethods.Initialize,
134135
new InitializeRequestParams
135136
{
136-
ProtocolVersion = _options.ProtocolVersion,
137+
ProtocolVersion = requestProtocol,
137138
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
138139
ClientInfo = _options.ClientInfo ?? DefaultImplementation,
139140
},
@@ -154,10 +155,13 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
154155
_serverInstructions = initializeResponse.Instructions;
155156

156157
// Validate protocol version
157-
if (initializeResponse.ProtocolVersion != _options.ProtocolVersion)
158+
bool isResponseProtocolValid =
159+
_options.ProtocolVersion is { } optionsProtocol ? optionsProtocol == initializeResponse.ProtocolVersion :
160+
McpSession.SupportedProtocolVersions.Contains(initializeResponse.ProtocolVersion);
161+
if (!isResponseProtocolValid)
158162
{
159-
LogServerProtocolVersionMismatch(EndpointName, _options.ProtocolVersion, initializeResponse.ProtocolVersion);
160-
throw new McpException($"Server protocol version mismatch. Expected {_options.ProtocolVersion}, got {initializeResponse.ProtocolVersion}");
163+
LogServerProtocolVersionMismatch(EndpointName, requestProtocol, initializeResponse.ProtocolVersion);
164+
throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
161165
}
162166

163167
// Send initialized notification

src/ModelContextProtocol.Core/Client/McpClientOptions.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,18 @@ public class McpClientOptions
3434
/// Gets or sets the protocol version to request from the server, using a date-based versioning scheme.
3535
/// </summary>
3636
/// <remarks>
37+
/// <para>
3738
/// The protocol version is a key part of the initialization handshake. The client and server must
38-
/// agree on a compatible protocol version to communicate successfully. If the server doesn't support
39-
/// the requested version, it will respond with a version mismatch error.
39+
/// agree on a compatible protocol version to communicate successfully.
40+
/// </para>
41+
/// <para>
42+
/// If non-<see langword="null"/>, this version will be sent to the server, and the handshake
43+
/// will fail if the version in the server's response does not match this version.
44+
/// If <see langword="null"/>, the client will request the latest version supported by the server
45+
/// but will allow any supported version that the server advertizes in its response.
46+
/// </para>
4047
/// </remarks>
41-
public string ProtocolVersion { get; set; } = "2024-11-05";
48+
public string? ProtocolVersion { get; set; }
4249

4350
/// <summary>
4451
/// Gets or sets a timeout for the client-server initialization handshake sequence.

src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,30 +97,30 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
9797
}
9898

9999
var rpcRequest = message as JsonRpcRequest;
100-
JsonRpcMessage? rpcResponseCandidate = null;
100+
JsonRpcMessageWithId? rpcResponseOrError = null;
101101

102102
if (response.Content.Headers.ContentType?.MediaType == "application/json")
103103
{
104104
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
105-
rpcResponseCandidate = await ProcessMessageAsync(responseContent, cancellationToken).ConfigureAwait(false);
105+
rpcResponseOrError = await ProcessMessageAsync(responseContent, rpcRequest, cancellationToken).ConfigureAwait(false);
106106
}
107107
else if (response.Content.Headers.ContentType?.MediaType == "text/event-stream")
108108
{
109109
using var responseBodyStream = await response.Content.ReadAsStreamAsync(cancellationToken);
110-
rpcResponseCandidate = await ProcessSseResponseAsync(responseBodyStream, rpcRequest, cancellationToken).ConfigureAwait(false);
110+
rpcResponseOrError = await ProcessSseResponseAsync(responseBodyStream, rpcRequest, cancellationToken).ConfigureAwait(false);
111111
}
112112

113113
if (rpcRequest is null)
114114
{
115115
return response;
116116
}
117117

118-
if (rpcResponseCandidate is not JsonRpcMessageWithId messageWithId || messageWithId.Id != rpcRequest.Id)
118+
if (rpcResponseOrError is null)
119119
{
120120
throw new McpException($"Streamable HTTP POST response completed without a reply to request with ID: {rpcRequest.Id}");
121121
}
122122

123-
if (rpcRequest.Method == RequestMethods.Initialize && rpcResponseCandidate is JsonRpcResponse)
123+
if (rpcRequest.Method == RequestMethods.Initialize && rpcResponseOrError is JsonRpcResponse)
124124
{
125125
// We've successfully initialized! Copy session-id and start GET request if any.
126126
if (response.Headers.TryGetValues("mcp-session-id", out var sessionIdValues))
@@ -199,20 +199,20 @@ private async Task ReceiveUnsolicitedMessagesAsync()
199199
continue;
200200
}
201201

202-
var message = await ProcessMessageAsync(sseEvent.Data, cancellationToken).ConfigureAwait(false);
202+
var rpcResponseOrError = await ProcessMessageAsync(sseEvent.Data, relatedRpcRequest, cancellationToken).ConfigureAwait(false);
203203

204-
// The server SHOULD end the response here anyway, but we won't leave it to chance. This transport makes
204+
// The server SHOULD end the HTTP response body here anyway, but we won't leave it to chance. This transport makes
205205
// a GET request for any notifications that might need to be sent after the completion of each POST.
206-
if (message is JsonRpcMessageWithId messageWithId && relatedRpcRequest?.Id == messageWithId.Id)
206+
if (rpcResponseOrError is not null)
207207
{
208-
return messageWithId;
208+
return rpcResponseOrError;
209209
}
210210
}
211211

212212
return null;
213213
}
214214

215-
private async Task<JsonRpcMessage?> ProcessMessageAsync(string data, CancellationToken cancellationToken)
215+
private async Task<JsonRpcMessageWithId?> ProcessMessageAsync(string data, JsonRpcRequest? relatedRpcRequest, CancellationToken cancellationToken)
216216
{
217217
try
218218
{
@@ -224,7 +224,12 @@ private async Task ReceiveUnsolicitedMessagesAsync()
224224
}
225225

226226
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
227-
return message;
227+
if (message is JsonRpcResponse or JsonRpcError &&
228+
message is JsonRpcMessageWithId rpcResponseOrError &&
229+
rpcResponseOrError.Id == relatedRpcRequest?.Id)
230+
{
231+
return rpcResponseOrError;
232+
}
228233
}
229234
catch (JsonException ex)
230235
{

src/ModelContextProtocol.Core/McpSession.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ internal sealed partial class McpSession : IDisposable
2828
private static readonly Histogram<double> s_serverOperationDuration = Diagnostics.CreateDurationHistogram(
2929
"mcp.server.operation.duration", "Measures the duration of inbound message processing.", longBuckets: false);
3030

31+
/// <summary>The latest version of the protocol supported by this implementation.</summary>
32+
internal const string LatestProtocolVersion = "2025-03-26";
33+
34+
/// <summary>All protocol versions supported by this implementation.</summary>
35+
internal static readonly string[] SupportedProtocolVersions =
36+
[
37+
"2024-11-05",
38+
LatestProtocolVersion,
39+
];
40+
3141
private readonly bool _isServer;
3242
private readonly string _transportKind;
3343
private readonly ITransport _transport;

0 commit comments

Comments
 (0)