Skip to content

Commit 3df91d1

Browse files
authored
Merge branch 'main' into localden/experimental
2 parents 6ccc1d1 + c560f47 commit 3df91d1

File tree

17 files changed

+193
-47
lines changed

17 files changed

+193
-47
lines changed

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.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;

src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol;
66
/// Provides a base class for paginated requests.
77
/// </summary>
88
/// <remarks>
9-
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
9+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
1010
/// </remarks>
1111
public class PaginatedRequestParams : RequestParams
1212
{

src/ModelContextProtocol.Core/README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,3 @@ await server.RunAsync(stdioTransport, CancellationToken.None);
102102
```
103103

104104
For more advanced scenarios with dependency injection, hosting, and automatic tool discovery, see the `ModelContextProtocol` package.
105-
106-
## Acknowledgements
107-
108-
The MCP C# SDK builds upon the excellent work from the [mcpdotnet](https://github.com/ReallyLiri/mcpdotnet) project by [Liri](https://github.com/ReallyLiri). We extend our gratitude for providing a foundational implementation that inspired this SDK.
109-
110-
## License
111-
112-
This project is licensed under the MIT License. See the [LICENSE](../../LICENSE) file for details.

src/ModelContextProtocol.Core/Server/McpServer.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,20 @@ private void ConfigureInitialize(McpServerOptions options)
161161
UpdateEndpointNameWithClientInfo();
162162
GetSessionOrThrow().EndpointName = EndpointName;
163163

164+
// Negotiate a protocol version. If the server options provide one, use that.
165+
// Otherwise, try to use whatever the client requested as long as it's supported.
166+
// If it's not supported, fall back to the latest supported version.
167+
string? protocolVersion = options.ProtocolVersion;
168+
if (protocolVersion is null)
169+
{
170+
protocolVersion = request?.ProtocolVersion is string clientProtocolVersion && McpSession.SupportedProtocolVersions.Contains(clientProtocolVersion) ?
171+
clientProtocolVersion :
172+
McpSession.LatestProtocolVersion;
173+
}
174+
164175
return new InitializeResult
165176
{
166-
ProtocolVersion = options.ProtocolVersion,
177+
ProtocolVersion = protocolVersion,
167178
Instructions = options.ServerInstructions,
168179
ServerInfo = options.ServerInfo ?? DefaultImplementation,
169180
Capabilities = ServerCapabilities ?? new(),

src/ModelContextProtocol.Core/Server/McpServerOptions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ public class McpServerOptions
3232
/// <remarks>
3333
/// The protocol version defines which features and message formats this server supports.
3434
/// This uses a date-based versioning scheme in the format "YYYY-MM-DD".
35+
/// If <see langword="null"/>, the server will advertize to the client the version requested
36+
/// by the client if that version is known to be supported, and otherwise will advertize the latest
37+
/// version supported by the server.
3538
/// </remarks>
36-
public string ProtocolVersion { get; set; } = "2024-11-05";
39+
public string? ProtocolVersion { get; set; }
3740

3841
/// <summary>
3942
/// Gets or sets a timeout used for the client-server initialization handshake sequence.

src/ModelContextProtocol.Core/Server/StreamableHttpPostTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async ValueTask DisposeAsync()
6060
{
6161
yield return message;
6262

63-
if (message.Data is JsonRpcMessageWithId response && response.Id == _pendingRequest)
63+
if (message.Data is JsonRpcResponse or JsonRpcError && ((JsonRpcMessageWithId)message.Data).Id == _pendingRequest)
6464
{
6565
// Complete the SSE response stream now that all pending requests have been processed.
6666
break;

0 commit comments

Comments
 (0)