From 6176d27cecce5df1ddc7ad0e720e25643f2c6ac8 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Feb 2026 19:14:12 +0530 Subject: [PATCH 1/6] Migrate MCP layer to Microsoft.ModelContextProtocol.HttpServer (0.1.0-preview.25) --- Nuget.config | 1 + .../Azure.DataApiBuilder.Mcp.csproj | 3 +- .../Core/McpEndpointRouteBuilderExtensions.cs | 17 +- .../Core/McpServerConfiguration.cs | 149 +++++++++++------- .../Core/McpServiceCollectionExtensions.cs | 15 +- .../Core/McpToolRegistry.cs | 8 + .../Utils/McpResponseBuilder.cs | 4 +- src/Core/Telemetry/TelemetryTracesHelper.cs | 7 +- src/Directory.Packages.props | 11 +- .../UnitTests/McpTelemetryTests.cs | 2 +- src/Service/Startup.cs | 10 +- 11 files changed, 148 insertions(+), 79 deletions(-) diff --git a/Nuget.config b/Nuget.config index 704c9d13ba..a0a0e4c91c 100644 --- a/Nuget.config +++ b/Nuget.config @@ -3,6 +3,7 @@ + diff --git a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj index c6a8b7bf21..3dfa099b44 100644 --- a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj +++ b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj @@ -11,8 +11,7 @@ - - + diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs index 6401e17e22..3a2d3ab4f3 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs @@ -6,6 +6,9 @@ using Azure.DataApiBuilder.Core.Configurations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ModelContextProtocol.HttpServer; namespace Azure.DataApiBuilder.Mcp.Core { @@ -16,6 +19,8 @@ public static class McpEndpointRouteBuilderExtensions { /// /// Maps the MCP endpoint to the specified if MCP is enabled in the runtime configuration. + /// Uses Microsoft MCP endpoint mapping (with auth/rate-limiting) when Entra ID is configured, + /// otherwise falls back to base MCP endpoint mapping. /// public static IEndpointRouteBuilder MapDabMcp( this IEndpointRouteBuilder endpoints, @@ -29,8 +34,16 @@ public static IEndpointRouteBuilder MapDabMcp( string mcpPath = mcpOptions.Path ?? McpRuntimeOptions.DEFAULT_PATH; - // Map the MCP endpoint - endpoints.MapMcp(mcpPath); + // Use Microsoft MCP endpoint mapping when Entra ID is configured, otherwise use base MCP + IConfiguration configuration = endpoints.ServiceProvider.GetRequiredService(); + if (McpServerConfiguration.IsEntraIdConfigured(configuration)) + { + endpoints.MapMicrosoftMcpServer(mcpPath); + } + else + { + endpoints.MapMcp(mcpPath); + } return endpoints; } diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs index bcba1a50e4..9c370baddc 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs @@ -4,9 +4,12 @@ using System.Text.Json; using Azure.DataApiBuilder.Mcp.Model; using Azure.DataApiBuilder.Mcp.Utils; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.ModelContextProtocol.HttpServer; using ModelContextProtocol; using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; namespace Azure.DataApiBuilder.Mcp.Core { @@ -16,80 +19,108 @@ namespace Azure.DataApiBuilder.Mcp.Core internal static class McpServerConfiguration { /// - /// Configures the MCP server with tool capabilities + /// Determines whether Entra ID (AzureAd) is configured for Microsoft MCP authentication. /// - internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services) + internal static bool IsEntraIdConfigured(IConfiguration configuration) { - services.AddMcpServer(options => + string? clientId = configuration["AzureAd:ClientId"]; + return !string.IsNullOrEmpty(clientId); + } + + /// + /// Configures the MCP server with tool capabilities. + /// Uses Microsoft MCP server (with MISE/Entra ID auth) when AzureAd is configured, + /// otherwise falls back to base MCP server without enterprise auth. + /// + internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services, IConfiguration configuration) + { + IMcpServerBuilder builder; + + if (IsEntraIdConfigured(configuration)) { - options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; - options.Capabilities = new() + // Use Microsoft MCP server with MISE/Entra ID authentication + builder = services.AddMicrosoftMcpServer(configuration, options => { - Tools = new() - { - ListToolsHandler = (request, ct) => - { - McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); - if (toolRegistry == null) - { - throw new InvalidOperationException("Tool registry is not available."); - } - - List tools = toolRegistry.GetAllTools().ToList(); + options.ResourceHost = "https://localhost"; + }); + } + else + { + // Fall back to base MCP server without enterprise auth + builder = services.AddMcpServer(); + } - return ValueTask.FromResult(new ListToolsResult - { - Tools = tools - }); - }, - CallToolHandler = async (request, ct) => - { - McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); - if (toolRegistry == null) - { - throw new InvalidOperationException("Tool registry is not available."); - } + builder + .WithListToolsHandler((RequestContext request, CancellationToken ct) => + { + McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); + if (toolRegistry == null) + { + throw new InvalidOperationException("Tool registry is not available."); + } - string? toolName = request.Params?.Name; - if (string.IsNullOrEmpty(toolName)) - { - throw new McpException("Tool name is required."); - } + List tools = toolRegistry.GetAllTools().ToList(); - if (!toolRegistry.TryGetTool(toolName, out IMcpTool? tool)) - { - throw new McpException($"Unknown tool: '{toolName}'"); - } + return ValueTask.FromResult(new ListToolsResult + { + Tools = tools + }); + }) + .WithCallToolHandler(async (RequestContext request, CancellationToken ct) => + { + McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); + if (toolRegistry == null) + { + throw new InvalidOperationException("Tool registry is not available."); + } - JsonDocument? arguments = null; - try - { - if (request.Params?.Arguments != null) - { - // Convert IReadOnlyDictionary to JsonDocument - Dictionary jsonObject = new(); - foreach (KeyValuePair kvp in request.Params.Arguments) - { - jsonObject[kvp.Key] = kvp.Value; - } + string? toolName = request.Params?.Name; + if (string.IsNullOrEmpty(toolName)) + { + throw new McpException("Tool name is required."); + } - string json = JsonSerializer.Serialize(jsonObject); - arguments = JsonDocument.Parse(json); - } + if (!toolRegistry.TryGetTool(toolName, out IMcpTool? tool)) + { + throw new McpException($"Unknown tool: '{toolName}'"); + } - return await McpTelemetryHelper.ExecuteWithTelemetryAsync( - tool!, toolName, arguments, request.Services!, ct); - } - finally - { - arguments?.Dispose(); - } + JsonDocument? arguments = null; + try + { + if (request.Params?.Arguments != null) + { + // Convert IReadOnlyDictionary to JsonDocument + Dictionary jsonObject = new(); + foreach (KeyValuePair kvp in request.Params.Arguments) + { + jsonObject[kvp.Key] = kvp.Value; } + + string json = JsonSerializer.Serialize(jsonObject); + arguments = JsonDocument.Parse(json); } - }; + + return await McpTelemetryHelper.ExecuteWithTelemetryAsync( + tool!, toolName, arguments, request.Services!, ct); + } + finally + { + arguments?.Dispose(); + } }) .WithHttpTransport(); + // Configure underlying MCP server options + services.Configure(options => + { + options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; + options.Capabilities = new() + { + Tools = new() + }; + }); + return services; } } diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs index bc87602da9..1b92e13ae1 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Mcp.Model; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Azure.DataApiBuilder.Mcp.Core @@ -14,10 +15,20 @@ namespace Azure.DataApiBuilder.Mcp.Core /// public static class McpServiceCollectionExtensions { + /// + /// Determines whether Entra ID (AzureAd) is configured for Microsoft MCP authentication. + /// When configured, the Microsoft MCP server with MISE auth is used. + /// When not configured, the base MCP server without enterprise auth is used. + /// + public static bool IsEntraIdConfigured(IConfiguration configuration) + { + return McpServerConfiguration.IsEntraIdConfigured(configuration); + } + /// /// Adds MCP server and related services to the service collection /// - public static IServiceCollection AddDabMcpServer(this IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider) + public static IServiceCollection AddDabMcpServer(this IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider, IConfiguration configuration) { if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) { @@ -42,7 +53,7 @@ public static IServiceCollection AddDabMcpServer(this IServiceCollection service RegisterCustomTools(services, runtimeConfig); // Configure MCP server - services.ConfigureMcpServer(); + services.ConfigureMcpServer(configuration); return services; } diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs index 0ba182b6db..626ddc9125 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs @@ -37,6 +37,14 @@ public void RegisterTool(IMcpTool tool) // Check for duplicate tool names (case-insensitive) if (_tools.TryGetValue(toolName, out IMcpTool? existingTool)) { + // If the same tool instance is already registered, skip silently. + // This can happen when both McpToolRegistryInitializer (hosted service) + // and McpStdioHelper register tools during stdio mode startup. + if (ReferenceEquals(existingTool, tool)) + { + return; + } + string existingToolType = existingTool.ToolType == ToolType.BuiltIn ? "built-in" : "custom"; string newToolType = tool.ToolType == ToolType.BuiltIn ? "built-in" : "custom"; diff --git a/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs b/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs index 49cacef2c3..401f270f42 100644 --- a/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs +++ b/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs @@ -34,7 +34,7 @@ public static CallToolResult BuildSuccessResult( { Content = new List { - new TextContentBlock { Type = "text", Text = output } + new TextContentBlock { Text = output } } }; } @@ -67,7 +67,7 @@ public static CallToolResult BuildErrorResult( { Content = new List { - new TextContentBlock { Type = "text", Text = output } + new TextContentBlock { Text = output } }, IsError = true }; diff --git a/src/Core/Telemetry/TelemetryTracesHelper.cs b/src/Core/Telemetry/TelemetryTracesHelper.cs index a6b0ef2b0d..152a2e6e68 100644 --- a/src/Core/Telemetry/TelemetryTracesHelper.cs +++ b/src/Core/Telemetry/TelemetryTracesHelper.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Net; using Azure.DataApiBuilder.Config.ObjectModel; -using OpenTelemetry.Trace; using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; namespace Azure.DataApiBuilder.Core.Telemetry @@ -104,8 +103,8 @@ public static void TrackMainControllerActivityFinishedWithException( { if (activity.IsAllDataRequested) { - activity.SetStatus(Status.Error.WithDescription(ex.Message)); - activity.RecordException(ex); + activity.SetStatus(ActivityStatusCode.Error, ex.Message); + activity.AddException(ex); activity.SetTag("error.type", ex.GetType().Name); activity.SetTag("error.message", ex.Message); activity.SetTag("status.code", statusCode); @@ -174,7 +173,7 @@ public static void TrackMcpToolExecutionFinishedWithException( if (activity.IsAllDataRequested) { activity.SetStatus(ActivityStatusCode.Error, ex.Message); - activity.RecordException(ex); + activity.AddException(ex); activity.SetTag("error.type", ex.GetType().Name); activity.SetTag("error.message", ex.Message); diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 60b48241fc..6f5b331bbb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -38,16 +38,15 @@ - - + - - - + + + - + diff --git a/src/Service.Tests/UnitTests/McpTelemetryTests.cs b/src/Service.Tests/UnitTests/McpTelemetryTests.cs index 18c043d4dd..9a8130f012 100644 --- a/src/Service.Tests/UnitTests/McpTelemetryTests.cs +++ b/src/Service.Tests/UnitTests/McpTelemetryTests.cs @@ -104,7 +104,7 @@ private static CallToolResult CreateToolResult(string text = "result", bool isEr { return new CallToolResult { - Content = new List { new TextContentBlock { Type = "text", Text = text } }, + Content = new List { new TextContentBlock { Text = text } }, IsError = isError }; } diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 05f920a2c0..106a6a3f05 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -56,6 +56,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Client; +using Microsoft.ModelContextProtocol.HttpServer; using NodaTime; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; @@ -499,7 +500,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); - services.AddDabMcpServer(configProvider); + services.AddDabMcpServer(configProvider, Configuration); services.AddSingleton(); @@ -812,6 +813,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC // without proper authorization headers. app.UseClientRoleHeaderAuthorizationMiddleware(); + // Only use Microsoft MCP middleware (MISE/Entra ID auth, rate limiting) when AzureAd is configured. + // When AzureAd is not configured, base MCP endpoints are used without enterprise auth. + if (McpServiceCollectionExtensions.IsEntraIdConfigured(Configuration)) + { + app.UseMicrosoftMcpServer(); + } + IRequestExecutorManager requestExecutorManager = app.ApplicationServices.GetRequiredService(); _hotReloadEventHandler.Subscribe( "GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED", From 295cda16d509a5aa623031e0f4792f8e48b696e0 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Feb 2026 22:31:47 +0530 Subject: [PATCH 2/6] Added new nuget package connection for Microsoft MCP --- .pipelines/cosmos-pipelines.yml | 2 ++ .pipelines/dwsql-pipelines.yml | 4 ++++ .pipelines/mssql-pipelines.yml | 4 ++++ .pipelines/mysql-pipelines.yml | 2 ++ .pipelines/pg-pipelines.yml | 2 ++ .pipelines/templates/build-pipelines.yml | 2 ++ .pipelines/templates/static-tools.yml | 2 ++ 7 files changed, 18 insertions(+) diff --git a/.pipelines/cosmos-pipelines.yml b/.pipelines/cosmos-pipelines.yml index 61d066aac7..72f792b4d8 100644 --- a/.pipelines/cosmos-pipelines.yml +++ b/.pipelines/cosmos-pipelines.yml @@ -49,6 +49,8 @@ steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from diff --git a/.pipelines/dwsql-pipelines.yml b/.pipelines/dwsql-pipelines.yml index ab79529030..fe03b5e549 100644 --- a/.pipelines/dwsql-pipelines.yml +++ b/.pipelines/dwsql-pipelines.yml @@ -42,6 +42,8 @@ jobs: steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from @@ -170,6 +172,8 @@ jobs: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from diff --git a/.pipelines/mssql-pipelines.yml b/.pipelines/mssql-pipelines.yml index f5e330b073..753f896f6b 100644 --- a/.pipelines/mssql-pipelines.yml +++ b/.pipelines/mssql-pipelines.yml @@ -43,6 +43,8 @@ jobs: steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from @@ -174,6 +176,8 @@ jobs: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from diff --git a/.pipelines/mysql-pipelines.yml b/.pipelines/mysql-pipelines.yml index 35d1fb1ac4..155da99417 100644 --- a/.pipelines/mysql-pipelines.yml +++ b/.pipelines/mysql-pipelines.yml @@ -41,6 +41,8 @@ jobs: steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from diff --git a/.pipelines/pg-pipelines.yml b/.pipelines/pg-pipelines.yml index c98430a23f..3268f4435b 100644 --- a/.pipelines/pg-pipelines.yml +++ b/.pipelines/pg-pipelines.yml @@ -36,6 +36,8 @@ jobs: steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # The .NET CLI commands in proceeding tasks use the .NET SDK version specified ("selected") here. # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from diff --git a/.pipelines/templates/build-pipelines.yml b/.pipelines/templates/build-pipelines.yml index 5f166084a3..9f0dd06846 100644 --- a/.pipelines/templates/build-pipelines.yml +++ b/.pipelines/templates/build-pipelines.yml @@ -9,6 +9,8 @@ steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' # If this is a release, do not append the build number at the end as it will # generate the prerelease nuget version. diff --git a/.pipelines/templates/static-tools.yml b/.pipelines/templates/static-tools.yml index 14bc25764f..4d44fa08a2 100644 --- a/.pipelines/templates/static-tools.yml +++ b/.pipelines/templates/static-tools.yml @@ -16,6 +16,8 @@ jobs: steps: - task: NuGetAuthenticate@1 displayName: 'NuGet Authenticate' + inputs: + nuGetServiceConnections: 'EngThriveNugetFeedAccessForSqlDab' - checkout: self # self represents the repo where the initial Pipelines YAML file was found clean: true # if true, execute `execute git clean -ffdx && git reset --hard HEAD` before fetching From 22482345860065645047c6b420a6d0f794a10384 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Feb 2026 23:09:53 +0530 Subject: [PATCH 3/6] Added packagesourcemapping --- Nuget.config | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Nuget.config b/Nuget.config index a0a0e4c91c..f051c067d7 100644 --- a/Nuget.config +++ b/Nuget.config @@ -5,6 +5,20 @@ + + + + + + + + + + + + + + From a8de54a2efc3a77efd015cbf1158edfb6846e5cb Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Feb 2026 23:24:39 +0530 Subject: [PATCH 4/6] added additional package source mappings --- Nuget.config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Nuget.config b/Nuget.config index f051c067d7..1babec7c1f 100644 --- a/Nuget.config +++ b/Nuget.config @@ -17,6 +17,9 @@ + + + From cddaaa476d2a1eb6f9d8a4c732ca6b72c6b04832 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 27 Feb 2026 11:16:03 +0530 Subject: [PATCH 5/6] Use * instead of package names --- Nuget.config | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Nuget.config b/Nuget.config index 1babec7c1f..f3a731d175 100644 --- a/Nuget.config +++ b/Nuget.config @@ -13,13 +13,7 @@ - - - - - - - + From 6e37fd03090b14d33c7255acb853019a01b615b1 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 27 Feb 2026 15:55:06 +0530 Subject: [PATCH 6/6] Refactoring and self review fixes --- Nuget.config | 9 +++- .../Azure.DataApiBuilder.Mcp.csproj | 2 + .../Core/McpEndpointRouteBuilderExtensions.cs | 17 +----- .../Core/McpServerConfiguration.cs | 54 ++++++------------- .../Core/McpServiceCollectionExtensions.cs | 15 +----- src/Directory.Packages.props | 4 +- src/Service.Tests/Mcp/McpToolRegistryTests.cs | 44 +++++++++++++++ src/Service/Startup.cs | 10 +--- 8 files changed, 76 insertions(+), 79 deletions(-) diff --git a/Nuget.config b/Nuget.config index f3a731d175..9d04e37970 100644 --- a/Nuget.config +++ b/Nuget.config @@ -9,11 +9,18 @@ + - + + + + + + + diff --git a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj index 3dfa099b44..78dcdef8ed 100644 --- a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj +++ b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj @@ -4,6 +4,8 @@ net8.0 enable enable + + $(NoWarn);NU1603 diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs index 3a2d3ab4f3..6401e17e22 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs @@ -6,9 +6,6 @@ using Azure.DataApiBuilder.Core.Configurations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.ModelContextProtocol.HttpServer; namespace Azure.DataApiBuilder.Mcp.Core { @@ -19,8 +16,6 @@ public static class McpEndpointRouteBuilderExtensions { /// /// Maps the MCP endpoint to the specified if MCP is enabled in the runtime configuration. - /// Uses Microsoft MCP endpoint mapping (with auth/rate-limiting) when Entra ID is configured, - /// otherwise falls back to base MCP endpoint mapping. /// public static IEndpointRouteBuilder MapDabMcp( this IEndpointRouteBuilder endpoints, @@ -34,16 +29,8 @@ public static IEndpointRouteBuilder MapDabMcp( string mcpPath = mcpOptions.Path ?? McpRuntimeOptions.DEFAULT_PATH; - // Use Microsoft MCP endpoint mapping when Entra ID is configured, otherwise use base MCP - IConfiguration configuration = endpoints.ServiceProvider.GetRequiredService(); - if (McpServerConfiguration.IsEntraIdConfigured(configuration)) - { - endpoints.MapMicrosoftMcpServer(mcpPath); - } - else - { - endpoints.MapMcp(mcpPath); - } + // Map the MCP endpoint + endpoints.MapMcp(mcpPath); return endpoints; } diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs index 9c370baddc..5a3b2df506 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs @@ -4,9 +4,7 @@ using System.Text.Json; using Azure.DataApiBuilder.Mcp.Model; using Azure.DataApiBuilder.Mcp.Utils; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.ModelContextProtocol.HttpServer; using ModelContextProtocol; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; @@ -18,39 +16,12 @@ namespace Azure.DataApiBuilder.Mcp.Core /// internal static class McpServerConfiguration { - /// - /// Determines whether Entra ID (AzureAd) is configured for Microsoft MCP authentication. - /// - internal static bool IsEntraIdConfigured(IConfiguration configuration) - { - string? clientId = configuration["AzureAd:ClientId"]; - return !string.IsNullOrEmpty(clientId); - } - /// /// Configures the MCP server with tool capabilities. - /// Uses Microsoft MCP server (with MISE/Entra ID auth) when AzureAd is configured, - /// otherwise falls back to base MCP server without enterprise auth. /// - internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services, IConfiguration configuration) + internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services) { - IMcpServerBuilder builder; - - if (IsEntraIdConfigured(configuration)) - { - // Use Microsoft MCP server with MISE/Entra ID authentication - builder = services.AddMicrosoftMcpServer(configuration, options => - { - options.ResourceHost = "https://localhost"; - }); - } - else - { - // Fall back to base MCP server without enterprise auth - builder = services.AddMcpServer(); - } - - builder + services.AddMcpServer() .WithListToolsHandler((RequestContext request, CancellationToken ct) => { McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); @@ -85,6 +56,11 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se throw new McpException($"Unknown tool: '{toolName}'"); } + if (tool is null || request.Services is null) + { + throw new InvalidOperationException("Tool or service provider unexpectedly null."); + } + JsonDocument? arguments = null; try { @@ -102,7 +78,7 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se } return await McpTelemetryHelper.ExecuteWithTelemetryAsync( - tool!, toolName, arguments, request.Services!, ct); + tool, toolName, arguments, request.Services, ct); } finally { @@ -111,14 +87,14 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se }) .WithHttpTransport(); - // Configure underlying MCP server options - services.Configure(options => + // Configure underlying MCP server options defensively to avoid overwriting any defaults + services.PostConfigure(options => { - options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; - options.Capabilities = new() - { - Tools = new() - }; + options.ServerInfo ??= new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; + options.ServerInfo.Name = McpProtocolDefaults.MCP_SERVER_NAME; + options.ServerInfo.Version = McpProtocolDefaults.MCP_SERVER_VERSION; + options.Capabilities ??= new(); + options.Capabilities.Tools ??= new(); }); return services; diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs index 1b92e13ae1..bc87602da9 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs @@ -5,7 +5,6 @@ using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Mcp.Model; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Azure.DataApiBuilder.Mcp.Core @@ -15,20 +14,10 @@ namespace Azure.DataApiBuilder.Mcp.Core /// public static class McpServiceCollectionExtensions { - /// - /// Determines whether Entra ID (AzureAd) is configured for Microsoft MCP authentication. - /// When configured, the Microsoft MCP server with MISE auth is used. - /// When not configured, the base MCP server without enterprise auth is used. - /// - public static bool IsEntraIdConfigured(IConfiguration configuration) - { - return McpServerConfiguration.IsEntraIdConfigured(configuration); - } - /// /// Adds MCP server and related services to the service collection /// - public static IServiceCollection AddDabMcpServer(this IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider, IConfiguration configuration) + public static IServiceCollection AddDabMcpServer(this IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider) { if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) { @@ -53,7 +42,7 @@ public static IServiceCollection AddDabMcpServer(this IServiceCollection service RegisterCustomTools(services, runtimeConfig); // Configure MCP server - services.ConfigureMcpServer(configuration); + services.ConfigureMcpServer(); return services; } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6f5b331bbb..441d8ae3b0 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -39,11 +39,11 @@ - + - + diff --git a/src/Service.Tests/Mcp/McpToolRegistryTests.cs b/src/Service.Tests/Mcp/McpToolRegistryTests.cs index c8fa6a9768..7bbd91341c 100644 --- a/src/Service.Tests/Mcp/McpToolRegistryTests.cs +++ b/src/Service.Tests/Mcp/McpToolRegistryTests.cs @@ -141,6 +141,50 @@ public void RegisterTool_WithDifferentCasing_ThrowsException() Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, exception.SubStatusCode); } + /// + /// Test that registering the same tool instance twice is silently ignored (idempotent). + /// This supports stdio mode where both McpToolRegistryInitializer and McpStdioHelper may register the same tools. + /// + [TestMethod] + public void RegisterTool_SameInstanceTwice_IsIdempotent() + { + // Arrange + McpToolRegistry registry = new(); + IMcpTool tool = new MockMcpTool("my_tool", ToolType.BuiltIn); + + // Act - Register the same instance twice + registry.RegisterTool(tool); + registry.RegisterTool(tool); + + // Assert - Tool should be registered only once + IEnumerable allTools = registry.GetAllTools(); + Assert.AreEqual(1, allTools.Count()); + } + + /// + /// Test that registering a different instance with the same name throws an exception, + /// even though a same-instance re-registration would be allowed. + /// + [TestMethod] + public void RegisterTool_DifferentInstanceSameName_ThrowsException() + { + // Arrange + McpToolRegistry registry = new(); + IMcpTool tool1 = new MockMcpTool("my_tool", ToolType.BuiltIn); + IMcpTool tool2 = new MockMcpTool("my_tool", ToolType.BuiltIn); + + // Act - Register first instance + registry.RegisterTool(tool1); + + // Assert - Different instance with same name should throw + DataApiBuilderException exception = Assert.ThrowsException( + () => registry.RegisterTool(tool2) + ); + + Assert.IsTrue(exception.Message.Contains("Duplicate MCP tool name 'my_tool' detected")); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, exception.SubStatusCode); + } + /// /// Test that GetAllTools returns all registered tools. /// diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 106a6a3f05..05f920a2c0 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -56,7 +56,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Client; -using Microsoft.ModelContextProtocol.HttpServer; using NodaTime; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; @@ -500,7 +499,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); - services.AddDabMcpServer(configProvider, Configuration); + services.AddDabMcpServer(configProvider); services.AddSingleton(); @@ -813,13 +812,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC // without proper authorization headers. app.UseClientRoleHeaderAuthorizationMiddleware(); - // Only use Microsoft MCP middleware (MISE/Entra ID auth, rate limiting) when AzureAd is configured. - // When AzureAd is not configured, base MCP endpoints are used without enterprise auth. - if (McpServiceCollectionExtensions.IsEntraIdConfigured(Configuration)) - { - app.UseMicrosoftMcpServer(); - } - IRequestExecutorManager requestExecutorManager = app.ApplicationServices.GetRequiredService(); _hotReloadEventHandler.Subscribe( "GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED",