From b783c73094b8e05573b5e5cf001495c366909866 Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Mon, 29 Dec 2025 17:20:04 -0800 Subject: [PATCH 1/4] Addressed comments --- .../Core/JsonRpcErrorCodes.cs | 36 +++++++++++++ .../Core/McpStdioServer.cs | 50 ++++++++----------- src/Service/Program.cs | 3 ++ src/Service/Utilities/McpStdioHelper.cs | 20 ++++---- 4 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs diff --git a/src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs b/src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs new file mode 100644 index 0000000000..ce68ab6dcd --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs @@ -0,0 +1,36 @@ +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// JSON-RPC 2.0 standard error codes used by the MCP stdio server. + /// These values come from the JSON-RPC 2.0 specification and are shared + /// so they are not hard-coded throughout the codebase. + /// + internal static class JsonRpcErrorCodes + { + /// + /// Invalid JSON was received by the server. + /// An error occurred on the server while parsing the JSON text. + /// + public const int PARSEERROR = -32700; + + /// + /// The JSON sent is not a valid Request object. + /// + public const int INVALIDREQUEST = -32600; + + /// + /// The method does not exist / is not available. + /// + public const int METHODNOTFOUND = -32601; + + /// + /// Invalid method parameter(s). + /// + public const int INVALIDPARAMS = -32602; + + /// + /// Internal JSON-RPC error. + /// + public const int INTERNALERROR = -32603; + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs index 79ccf39356..46550d8ac5 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -65,7 +65,7 @@ public async Task RunAsync(CancellationToken cancellationToken) if (line.Length > MAX_LINE_LENGTH) { - WriteError(id: null, code: -32600, message: "Request too large"); + WriteError(id: null, code: JsonRpcErrorCodes.INVALIDREQUEST, message: "Request too large"); continue; } @@ -77,13 +77,13 @@ public async Task RunAsync(CancellationToken cancellationToken) catch (JsonException jsonEx) { Console.Error.WriteLine($"[MCP DEBUG] JSON parse error: {jsonEx.Message}"); - WriteError(id: null, code: -32700, message: "Parse error"); + WriteError(id: null, code: JsonRpcErrorCodes.PARSEERROR, message: "Parse error"); continue; } catch (Exception ex) { Console.Error.WriteLine($"[MCP DEBUG] Unexpected error parsing request: {ex.Message}"); - WriteError(id: null, code: -32603, message: "Internal error"); + WriteError(id: null, code: JsonRpcErrorCodes.INTERNALERROR, message: "Internal error"); continue; } @@ -99,7 +99,7 @@ public async Task RunAsync(CancellationToken cancellationToken) if (!root.TryGetProperty("method", out JsonElement methodEl)) { - WriteError(id, -32600, "Invalid Request"); + WriteError(id, JsonRpcErrorCodes.INVALIDREQUEST, "Invalid Request"); continue; } @@ -133,13 +133,13 @@ public async Task RunAsync(CancellationToken cancellationToken) return; default: - WriteError(id, -32601, $"Method not found: {method}"); + WriteError(id, JsonRpcErrorCodes.METHODNOTFOUND, $"Method not found: {method}"); break; } } catch (Exception) { - WriteError(id, -32603, "Internal error"); + WriteError(id, JsonRpcErrorCodes.INTERNALERROR, "Internal error"); } } } @@ -158,32 +158,22 @@ public async Task RunAsync(CancellationToken cancellationToken) /// private void HandleInitialize(JsonElement? id) { - // Extract the actual id value from the request - object? requestId = id.HasValue ? GetIdValue(id.Value) : null; - - // Create the initialize response - var response = new + var result = new { - jsonrpc = "2.0", - id = requestId, - result = new + protocolVersion = _protocolVersion, + capabilities = new { - protocolVersion = _protocolVersion, - capabilities = new - { - tools = new { listChanged = true }, - logging = new { } - }, - serverInfo = new - { - name = "Data API Builder", - version = "1.0.0" - } + tools = new { listChanged = true }, + logging = new { } + }, + serverInfo = new + { + name = "SQL MCP Server", + version = "1.0.0" } }; - string json = JsonSerializer.Serialize(response); - Console.Out.WriteLine(json); + WriteResult(id, result); } /// @@ -225,7 +215,7 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel { if (!root.TryGetProperty("params", out JsonElement @params) || @params.ValueKind != JsonValueKind.Object) { - WriteError(id, -32602, "Missing params"); + WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, "Missing params"); return; } @@ -247,14 +237,14 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel if (string.IsNullOrWhiteSpace(toolName)) { Console.Error.WriteLine("[MCP DEBUG] callTool → missing tool name."); - WriteError(id, -32602, "Missing tool name"); + WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, "Missing tool name"); return; } if (!_toolRegistry.TryGetTool(toolName!, out IMcpTool? tool) || tool is null) { Console.Error.WriteLine($"[MCP DEBUG] callTool → tool not found: {toolName}"); - WriteError(id, -32602, $"Tool not found: {toolName}"); + WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, $"Tool not found: {toolName}"); return; } diff --git a/src/Service/Program.cs b/src/Service/Program.cs index e0a74bd9d1..e23fb98cd9 100644 --- a/src/Service/Program.cs +++ b/src/Service/Program.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using System; using System.CommandLine; using System.CommandLine.Parsing; diff --git a/src/Service/Utilities/McpStdioHelper.cs b/src/Service/Utilities/McpStdioHelper.cs index 9e337d0809..b8a9c04e98 100644 --- a/src/Service/Utilities/McpStdioHelper.cs +++ b/src/Service/Utilities/McpStdioHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -15,9 +16,9 @@ internal static class McpStdioHelper /// Determines if MCP stdio mode should be run based on command line arguments. /// /// The command line arguments. - /// The role for MCP stdio mode, if specified. - /// - public static bool ShouldRunMcpStdio(string[] args, out string? mcpRole) + /// The role for MCP stdio mode. When this method returns true, the role is guaranteed to be non-null. + /// True when MCP stdio mode should be enabled; otherwise false. + public static bool ShouldRunMcpStdio(string[] args, [NotNullWhen(true)] out string? mcpRole) { mcpRole = null; @@ -43,6 +44,11 @@ public static bool ShouldRunMcpStdio(string[] args, out string? mcpRole) } } + // Ensure that when MCP stdio is enabled, mcpRole is always non-null. + // This matches the NotNullWhen(true) contract and avoids nullable warnings + // for callers while still allowing an implicit default when no role is provided. + mcpRole ??= "anonymous"; + return true; } @@ -76,17 +82,13 @@ public static bool RunMcpStdioHost(IHost host) foreach (Mcp.Model.IMcpTool tool in tools) { - _ = tool.GetToolMetadata(); registry.RegisterTool(tool); } - IServiceScopeFactory scopeFactory = - host.Services.GetRequiredService(); - using IServiceScope scope = scopeFactory.CreateScope(); IHostApplicationLifetime lifetime = - scope.ServiceProvider.GetRequiredService(); + host.Services.GetRequiredService(); Mcp.Core.IMcpStdioServer stdio = - scope.ServiceProvider.GetRequiredService(); + host.Services.GetRequiredService(); stdio.RunAsync(lifetime.ApplicationStopping).GetAwaiter().GetResult(); host.StopAsync().GetAwaiter().GetResult(); From 6979a8dbd4e4fb8e16a94abdef6ada23f0d54bca Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Tue, 30 Dec 2025 14:14:03 -0800 Subject: [PATCH 2/4] Added copyright header for McpStdioHelper --- src/Service/Utilities/McpStdioHelper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Service/Utilities/McpStdioHelper.cs b/src/Service/Utilities/McpStdioHelper.cs index b8a9c04e98..36f2b72807 100644 --- a/src/Service/Utilities/McpStdioHelper.cs +++ b/src/Service/Utilities/McpStdioHelper.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; From a20bb09c65fba891d21ad67f707f677222d46e68 Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Thu, 15 Jan 2026 18:44:39 -0800 Subject: [PATCH 3/4] Addressed comments --- .../Core/McpProtocolDefaults.cs | 9 ++++++++ .../Core/McpServerConfiguration.cs | 2 +- .../Core/McpStdioServer.cs | 22 +++++++++---------- .../McpStdioJsonRpcErrorCodes.cs} | 14 ++++++------ src/Service/Utilities/McpStdioHelper.cs | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) rename src/Azure.DataApiBuilder.Mcp/{Core/JsonRpcErrorCodes.cs => Model/McpStdioJsonRpcErrorCodes.cs} (70%) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs index 8e307a7c0e..48b235f480 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs @@ -1,3 +1,4 @@ +using Azure.DataApiBuilder.Product; using Microsoft.Extensions.Configuration; namespace Azure.DataApiBuilder.Mcp.Core @@ -7,6 +8,14 @@ namespace Azure.DataApiBuilder.Mcp.Core /// public static class McpProtocolDefaults { + /// + /// Default MCP server name advertised during initialization. + /// + public const string MCP_SERVER_NAME = "SQL MCP Server"; + /// + /// Default MCP server version advertised during initialization. + /// + public static readonly string MCP_SERVER_VERSION = ProductInfo.GetProductVersion(); /// /// Default MCP protocol version advertised when no configuration override is provided. /// diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs index 86cccd2aaf..d76af816bd 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs @@ -21,7 +21,7 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se { services.AddMcpServer(options => { - options.ServerInfo = new() { Name = "Data API builder MCP Server", Version = "1.0.0" }; + options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; options.Capabilities = new() { Tools = new() diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs index 46550d8ac5..6584e819af 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -65,7 +65,7 @@ public async Task RunAsync(CancellationToken cancellationToken) if (line.Length > MAX_LINE_LENGTH) { - WriteError(id: null, code: JsonRpcErrorCodes.INVALIDREQUEST, message: "Request too large"); + WriteError(id: null, code: McpStdioJsonRpcErrorCodes.INVALID_REQUEST, message: "Request too large"); continue; } @@ -77,13 +77,13 @@ public async Task RunAsync(CancellationToken cancellationToken) catch (JsonException jsonEx) { Console.Error.WriteLine($"[MCP DEBUG] JSON parse error: {jsonEx.Message}"); - WriteError(id: null, code: JsonRpcErrorCodes.PARSEERROR, message: "Parse error"); + WriteError(id: null, code: McpStdioJsonRpcErrorCodes.PARSE_ERROR, message: "Parse error"); continue; } catch (Exception ex) { Console.Error.WriteLine($"[MCP DEBUG] Unexpected error parsing request: {ex.Message}"); - WriteError(id: null, code: JsonRpcErrorCodes.INTERNALERROR, message: "Internal error"); + WriteError(id: null, code: McpStdioJsonRpcErrorCodes.INTERNAL_ERROR, message: "Internal error"); continue; } @@ -99,7 +99,7 @@ public async Task RunAsync(CancellationToken cancellationToken) if (!root.TryGetProperty("method", out JsonElement methodEl)) { - WriteError(id, JsonRpcErrorCodes.INVALIDREQUEST, "Invalid Request"); + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_REQUEST, "Invalid Request"); continue; } @@ -133,13 +133,13 @@ public async Task RunAsync(CancellationToken cancellationToken) return; default: - WriteError(id, JsonRpcErrorCodes.METHODNOTFOUND, $"Method not found: {method}"); + WriteError(id, McpStdioJsonRpcErrorCodes.METHOD_NOT_FOUND, $"Method not found: {method}"); break; } } catch (Exception) { - WriteError(id, JsonRpcErrorCodes.INTERNALERROR, "Internal error"); + WriteError(id, McpStdioJsonRpcErrorCodes.INTERNAL_ERROR, "Internal error"); } } } @@ -168,8 +168,8 @@ private void HandleInitialize(JsonElement? id) }, serverInfo = new { - name = "SQL MCP Server", - version = "1.0.0" + name = McpProtocolDefaults.MCP_SERVER_NAME, + version = McpProtocolDefaults.MCP_SERVER_VERSION } }; @@ -215,7 +215,7 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel { if (!root.TryGetProperty("params", out JsonElement @params) || @params.ValueKind != JsonValueKind.Object) { - WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, "Missing params"); + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, "Missing params"); return; } @@ -237,14 +237,14 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel if (string.IsNullOrWhiteSpace(toolName)) { Console.Error.WriteLine("[MCP DEBUG] callTool → missing tool name."); - WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, "Missing tool name"); + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, "Missing tool name"); return; } if (!_toolRegistry.TryGetTool(toolName!, out IMcpTool? tool) || tool is null) { Console.Error.WriteLine($"[MCP DEBUG] callTool → tool not found: {toolName}"); - WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, $"Tool not found: {toolName}"); + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, $"Tool not found: {toolName}"); return; } diff --git a/src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs b/src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs similarity index 70% rename from src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs rename to src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs index ce68ab6dcd..3bac194068 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs +++ b/src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs @@ -1,36 +1,36 @@ -namespace Azure.DataApiBuilder.Mcp.Core +namespace Azure.DataApiBuilder.Mcp.Model { /// /// JSON-RPC 2.0 standard error codes used by the MCP stdio server. /// These values come from the JSON-RPC 2.0 specification and are shared /// so they are not hard-coded throughout the codebase. /// - internal static class JsonRpcErrorCodes + internal static class McpStdioJsonRpcErrorCodes { /// /// Invalid JSON was received by the server. /// An error occurred on the server while parsing the JSON text. /// - public const int PARSEERROR = -32700; + public const int PARSE_ERROR = -32700; /// /// The JSON sent is not a valid Request object. /// - public const int INVALIDREQUEST = -32600; + public const int INVALID_REQUEST = -32600; /// /// The method does not exist / is not available. /// - public const int METHODNOTFOUND = -32601; + public const int METHOD_NOT_FOUND = -32601; /// /// Invalid method parameter(s). /// - public const int INVALIDPARAMS = -32602; + public const int INVALID_PARAMS = -32602; /// /// Internal JSON-RPC error. /// - public const int INTERNALERROR = -32603; + public const int INTERNAL_ERROR = -32603; } } diff --git a/src/Service/Utilities/McpStdioHelper.cs b/src/Service/Utilities/McpStdioHelper.cs index 36f2b72807..043e9dd85d 100644 --- a/src/Service/Utilities/McpStdioHelper.cs +++ b/src/Service/Utilities/McpStdioHelper.cs @@ -19,7 +19,7 @@ internal static class McpStdioHelper /// Determines if MCP stdio mode should be run based on command line arguments. /// /// The command line arguments. - /// The role for MCP stdio mode. When this method returns true, the role is guaranteed to be non-null. + /// The role for MCP stdio mode. When this method returns true, the role defaults to anonymous. /// True when MCP stdio mode should be enabled; otherwise false. public static bool ShouldRunMcpStdio(string[] args, [NotNullWhen(true)] out string? mcpRole) { From ce2c0b6d80649e825f3d094b7c3bea12536c0516 Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Thu, 15 Jan 2026 21:38:47 -0800 Subject: [PATCH 4/4] Minor fix --- src/Service.Tests/SqlTests/SqlTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index 5e90e77b85..16e804f117 100644 --- a/src/Service.Tests/SqlTests/SqlTestBase.cs +++ b/src/Service.Tests/SqlTests/SqlTestBase.cs @@ -514,7 +514,7 @@ protected static async Task SetupAndRunRestApiTest( request.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, clientRoleHeader); // Detect runtime auth provider once per call - var configProvider = _application.Services.GetRequiredService(); + RuntimeConfigProvider configProvider = _application.Services.GetRequiredService(); string provider = configProvider.GetConfig().Runtime.Host.Authentication.Provider; if (string.Equals(provider, nameof(EasyAuthType.AppService), StringComparison.OrdinalIgnoreCase))