Skip to content

Commit 7f4a34d

Browse files
CopilotAniruddh25aaronburtle
authored
MCP stdio: negotiate initialize protocol version, default to 2025-11-25, return description in serverInfo, and add initialize wire-shape tests (#3622)
## Why make this change? DAB MCP stdio always returned `2025-06-18` in `initialize`, even when clients requested newer protocol versions, causing modern MCP clients (e.g., Claude Code 2.x) to disconnect. This update aligns initialize negotiation behavior with MCP version selection expectations, updates the default supported version to `2025-11-25`, aligns initialize metadata shape for newer protocol versions, and adds focused wiring tests to lock down the initialize response wire format end-to-end. ## What is this change? - **Protocol default** - Bumped MCP default protocol from `2025-06-18` to `2025-11-25` in `McpProtocolDefaults`. - **Initialize version negotiation** - `initialize` now reads `params.protocolVersion` from the client request. - Server responds with the greatest supported version `<=` client requested (never higher than the client). - **Initialize metadata field shape by negotiated protocol** - For negotiated protocol versions `>= 2025-11-25`, runtime MCP description is returned as `serverInfo.description`. - For older negotiated protocol versions, description remains top-level `instructions` for backward compatibility. - **Negotiation utility + parsing behavior** - Added centralized negotiation helper in `McpProtocolDefaults`. - Compares date-formatted versions (`yyyy-MM-dd`) with ordinal string fallback for non-date values. - Added protocol-threshold helper for choosing `serverInfo.description` vs `instructions`. - **Focused test coverage** - Added helper unit tests for: - new default protocol value, - client newer than server (server version returned), - client older than server (client version returned), - missing client version, - non-date fallback comparison path, - protocol threshold behavior for `serverInfo.description`. - Added focused initialize wiring tests for `HandleInitialize` that validate: - JSON-RPC envelope shape (`jsonrpc`, `id`), - negotiated `result.protocolVersion`, - capabilities shape, - field placement across protocol/description scenarios (`serverInfo.description` vs `instructions` vs neither). ```csharp string negotiatedProtocolVersion = McpProtocolDefaults.ResolveInitializeResponseProtocolVersion( _protocolVersion, clientRequestedProtocolVersion); ``` ## How was this tested? - [ ] Integration Tests - [x] Unit Tests ## Sample Request(s) - Example CLI usage: - `dab start --mcp-stdio` - Example initialize request: ```json {"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"claude-code","version":"2.1.98"}},"id":1} ``` - Example initialize response behavior for negotiated `2025-11-25`: ```json {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{"listChanged":true},"logging":{}},"serverInfo":{"name":"SQL MCP Server","version":"<dab-version>","description":"<mcp-runtime-description>"}}} ``` - Example initialize response behavior for negotiated older versions: ```json {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true},"logging":{}},"serverInfo":{"name":"SQL MCP Server","version":"<dab-version>"},"instructions":"<mcp-runtime-description>"}} ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> Co-authored-by: Aniruddh Munde <anmunde@microsoft.com> Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
1 parent e386dad commit 7f4a34d

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)