Skip to content

Commit 5e08f3c

Browse files
aaronburtleCopilotAniruddh25
authored
Cherry pick MCP stdio: negotiate initialize protocol version, default to 2025-11-25, return description in serverInfo, and add initialize wire-shape tests #3622 (#3627)
## Why make this change? Closes #3626 ## What is this change? Cherry picks this PR: #3622 ## How was this tested? Built locally and passes normal test suite. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
1 parent ffd47a8 commit 5e08f3c

4 files changed

Lines changed: 353 additions & 14 deletions

File tree

src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using Azure.DataApiBuilder.Product;
23
using Microsoft.Extensions.Configuration;
34

@@ -19,13 +20,18 @@ public static class McpProtocolDefaults
1920
/// <summary>
2021
/// Default MCP protocol version advertised when no configuration override is provided.
2122
/// </summary>
22-
public const string DEFAULT_PROTOCOL_VERSION = "2025-06-18";
23+
public const string DEFAULT_PROTOCOL_VERSION = "2025-11-25";
2324

2425
/// <summary>
2526
/// Configuration key used to override the MCP protocol version.
2627
/// </summary>
2728
public const string PROTOCOL_VERSION_CONFIG_KEY = "MCP:ProtocolVersion";
2829

30+
/// <summary>
31+
/// Protocol version where MCP initialize server description is expected under serverInfo.description.
32+
/// </summary>
33+
public const string SERVER_INFO_DESCRIPTION_PROTOCOL_VERSION = "2025-11-25";
34+
2935
/// <summary>
3036
/// Helper to resolve the effective protocol version from configuration.
3137
/// Falls back to <see cref="DEFAULT_PROTOCOL_VERSION"/> when the key is not set.
@@ -34,6 +40,44 @@ public static string ResolveProtocolVersion(IConfiguration? configuration)
3440
{
3541
return configuration?.GetValue<string>(PROTOCOL_VERSION_CONFIG_KEY) ?? DEFAULT_PROTOCOL_VERSION;
3642
}
43+
44+
/// <summary>
45+
/// Resolves the protocol version to send in initialize response as the
46+
/// greatest version that does not exceed the client requested version.
47+
/// </summary>
48+
/// <param name="supportedProtocolVersion">The server's effective supported protocol version.</param>
49+
/// <param name="clientRequestedProtocolVersion">The protocol version requested by the client.</param>
50+
/// <returns>The protocol version to return to the client.</returns>
51+
public static string ResolveInitializeResponseProtocolVersion(string supportedProtocolVersion, string? clientRequestedProtocolVersion)
52+
{
53+
if (string.IsNullOrWhiteSpace(clientRequestedProtocolVersion))
54+
{
55+
return supportedProtocolVersion;
56+
}
57+
58+
return CompareProtocolVersions(supportedProtocolVersion, clientRequestedProtocolVersion) <= 0
59+
? supportedProtocolVersion
60+
: clientRequestedProtocolVersion;
61+
}
62+
63+
/// <summary>
64+
/// Indicates whether initialize response metadata should use serverInfo.description instead of top-level instructions.
65+
/// </summary>
66+
public static bool ShouldUseServerInfoDescription(string protocolVersion)
67+
{
68+
return CompareProtocolVersions(protocolVersion, SERVER_INFO_DESCRIPTION_PROTOCOL_VERSION) >= 0;
69+
}
70+
71+
private static int CompareProtocolVersions(string leftVersion, string rightVersion)
72+
{
73+
const string PROTOCOL_VERSION_DATE_FORMAT = "yyyy-MM-dd";
74+
if (DateOnly.TryParseExact(leftVersion, PROTOCOL_VERSION_DATE_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly leftDate) &&
75+
DateOnly.TryParseExact(rightVersion, PROTOCOL_VERSION_DATE_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly rightDate))
76+
{
77+
return leftDate.CompareTo(rightDate);
78+
}
79+
80+
return string.Compare(leftVersion, rightVersion, StringComparison.Ordinal);
81+
}
3782
}
3883
}
39-

src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public async Task RunAsync(CancellationToken cancellationToken)
122122
switch (method)
123123
{
124124
case "initialize":
125-
HandleInitialize(id);
125+
HandleInitialize(id, root);
126126
break;
127127

128128
case "notifications/initialized":
@@ -167,22 +167,27 @@ public async Task RunAsync(CancellationToken cancellationToken)
167167
/// <param name="id">
168168
/// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request.
169169
/// </param>
170+
/// <param name="root">The incoming initialize request payload.</param>
170171
/// <remarks>
171-
/// This method constructs and writes the MCP "initialize" response to STDOUT. It uses the protocol version defined by <c>PROTOCOL_VERSION</c>
172-
/// and includes supported capabilities and server information. No notifications are sent here; the server waits for the client to send
173-
/// "notifications/initialized" before sending any notifications.
172+
/// This method constructs and writes the MCP "initialize" response to STDOUT. It negotiates the response protocol version from the
173+
/// server-supported version and client-requested version, and includes supported capabilities and server information. No notifications
174+
/// are sent here; the server waits for the client to send "notifications/initialized" before sending any notifications.
174175
/// </remarks>
175-
private void HandleInitialize(JsonElement? id)
176+
private void HandleInitialize(JsonElement? id, JsonElement root)
176177
{
178+
string? clientRequestedProtocolVersion = GetClientProtocolVersion(root);
179+
string negotiatedProtocolVersion =
180+
McpProtocolDefaults.ResolveInitializeResponseProtocolVersion(_protocolVersion, clientRequestedProtocolVersion);
181+
177182
// Get the description from runtime config if available
178-
string? instructions = null;
183+
string? description = null;
179184
RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService<RuntimeConfigProvider>();
180185
if (runtimeConfigProvider != null)
181186
{
182187
try
183188
{
184189
RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig();
185-
instructions = runtimeConfig.Runtime?.Mcp?.Description;
190+
description = runtimeConfig.Runtime?.Mcp?.Description;
186191
}
187192
catch (Exception)
188193
{
@@ -191,13 +196,33 @@ private void HandleInitialize(JsonElement? id)
191196
}
192197
}
193198

194-
// Create the initialize response - only include instructions if non-empty
199+
bool shouldUseServerInfoDescription = McpProtocolDefaults.ShouldUseServerInfoDescription(negotiatedProtocolVersion);
200+
201+
// Create the initialize response - only include description/instructions if non-empty
195202
object result;
196-
if (!string.IsNullOrWhiteSpace(instructions))
203+
if (!string.IsNullOrWhiteSpace(description) && shouldUseServerInfoDescription)
197204
{
198205
result = new
199206
{
200-
protocolVersion = _protocolVersion,
207+
protocolVersion = negotiatedProtocolVersion,
208+
capabilities = new
209+
{
210+
tools = new { listChanged = true },
211+
logging = new { }
212+
},
213+
serverInfo = new
214+
{
215+
name = McpProtocolDefaults.MCP_SERVER_NAME,
216+
version = McpProtocolDefaults.MCP_SERVER_VERSION,
217+
description = description
218+
}
219+
};
220+
}
221+
else if (!string.IsNullOrWhiteSpace(description))
222+
{
223+
result = new
224+
{
225+
protocolVersion = negotiatedProtocolVersion,
201226
capabilities = new
202227
{
203228
tools = new { listChanged = true },
@@ -208,14 +233,14 @@ private void HandleInitialize(JsonElement? id)
208233
name = McpProtocolDefaults.MCP_SERVER_NAME,
209234
version = McpProtocolDefaults.MCP_SERVER_VERSION
210235
},
211-
instructions = instructions
236+
instructions = description
212237
};
213238
}
214239
else
215240
{
216241
result = new
217242
{
218-
protocolVersion = _protocolVersion,
243+
protocolVersion = negotiatedProtocolVersion,
219244
capabilities = new
220245
{
221246
tools = new { listChanged = true },
@@ -232,6 +257,22 @@ private void HandleInitialize(JsonElement? id)
232257
WriteResult(id, result);
233258
}
234259

260+
private static string? GetClientProtocolVersion(JsonElement root)
261+
{
262+
if (!root.TryGetProperty("params", out JsonElement paramsElement) || paramsElement.ValueKind != JsonValueKind.Object)
263+
{
264+
return null;
265+
}
266+
267+
if (!paramsElement.TryGetProperty("protocolVersion", out JsonElement protocolVersionElement) ||
268+
protocolVersionElement.ValueKind != JsonValueKind.String)
269+
{
270+
return null;
271+
}
272+
273+
return protocolVersionElement.GetString();
274+
}
275+
235276
/// <summary>
236277
/// Handles the "tools/list" JSON-RPC method by sending the list of available tools to the client.
237278
/// </summary>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.DataApiBuilder.Mcp.Core;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Azure.DataApiBuilder.Service.Tests.UnitTests
9+
{
10+
[TestClass]
11+
public class McpProtocolDefaultsTests
12+
{
13+
[TestMethod]
14+
public void ResolveProtocolVersion_WithoutOverride_UsesLatestDefault()
15+
{
16+
IConfiguration config = new ConfigurationBuilder().Build();
17+
18+
string resolved = McpProtocolDefaults.ResolveProtocolVersion(config);
19+
20+
Assert.AreEqual("2025-11-25", resolved);
21+
}
22+
23+
[DataTestMethod]
24+
[DataRow("2025-11-25", "2026-01-01", "2025-11-25")]
25+
[DataRow("2025-11-25", "2025-06-18", "2025-06-18")]
26+
[DataRow("2025-11-25", "2025-11-25", "2025-11-25")]
27+
[DataRow("2025-11-25", null, "2025-11-25")]
28+
[DataRow("a-version", "z-version", "a-version")]
29+
public void ResolveInitializeResponseProtocolVersion_ReturnsExpectedNegotiatedVersion(
30+
string supportedProtocolVersion,
31+
string clientRequestedProtocolVersion,
32+
string expectedVersion)
33+
{
34+
string resolved = McpProtocolDefaults.ResolveInitializeResponseProtocolVersion(
35+
supportedProtocolVersion,
36+
clientRequestedProtocolVersion);
37+
38+
Assert.AreEqual(expectedVersion, resolved);
39+
}
40+
41+
[TestMethod]
42+
public void ShouldUseServerInfoDescription_AtOrAboveThreshold_ReturnsTrue()
43+
{
44+
Assert.IsTrue(McpProtocolDefaults.ShouldUseServerInfoDescription("2025-11-25"));
45+
Assert.IsTrue(McpProtocolDefaults.ShouldUseServerInfoDescription("2025-12-01"));
46+
}
47+
48+
[TestMethod]
49+
public void ShouldUseServerInfoDescription_BelowThreshold_ReturnsFalse()
50+
{
51+
Assert.IsFalse(McpProtocolDefaults.ShouldUseServerInfoDescription("2025-06-18"));
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)