diff --git a/src/Service.Tests/UnitTests/McpStdioHelperTests.cs b/src/Service.Tests/UnitTests/McpStdioHelperTests.cs index daf0c9e3b1..5de547ab71 100644 --- a/src/Service.Tests/UnitTests/McpStdioHelperTests.cs +++ b/src/Service.Tests/UnitTests/McpStdioHelperTests.cs @@ -3,8 +3,13 @@ #nullable enable +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Mcp.Core; using Azure.DataApiBuilder.Service.Utilities; using Microsoft.Extensions.DependencyInjection; @@ -23,9 +28,12 @@ public void RunMcpStdioHost_DoesNotStartWebHost() TestApplicationLifetime lifetime = new(); TestMcpStdioServer stdioServer = new(); + TestMetadataProviderFactory metadataProviderFactory = new(); + services.AddSingleton(); services.AddSingleton(lifetime); services.AddSingleton(stdioServer); + services.AddSingleton(metadataProviderFactory); using ServiceProvider serviceProvider = services.BuildServiceProvider(); TestHost host = new(serviceProvider); @@ -43,6 +51,36 @@ public void RunMcpStdioHost_DoesNotStartWebHost() "The stdio loop should keep using the host lifetime cancellation token."); Assert.AreEqual(1, host.DisposeCallCount, "MCP stdio mode should dispose the host after the stdio loop exits."); + Assert.AreEqual(1, metadataProviderFactory.InitializeAsyncCallCount, + "MCP stdio mode must initialize the metadata providers itself: it never calls " + + "host.Run(), so Startup.Configure -- the only caller of PerformOnConfigChangeAsync " + + "-- never runs, and without this every tool call fails with " + + "\"Database object for entity '' has not been inferred.\""); + } + + private sealed class TestMetadataProviderFactory : IMetadataProviderFactory + { + public int InitializeAsyncCallCount { get; private set; } + + public Task InitializeAsync() + { + InitializeAsyncCallCount++; + return Task.CompletedTask; + } + + public void InitializeAsync( + Dictionary> entityToDatabaseObjectMap, + Dictionary> graphQLStoredProcedureExposedNameToEntityNameMap) + => InitializeAsyncCallCount++; + + public ISqlMetadataProvider GetMetadataProvider(string dataSourceName) + => throw new NotImplementedException(); + + public IEnumerable ListMetadataProviders() + => Array.Empty(); + + public List GetAllMetadataExceptions() + => new(); } private sealed class TestHost : IHost diff --git a/src/Service/Utilities/McpStdioHelper.cs b/src/Service/Utilities/McpStdioHelper.cs index 4ee403b98e..7af1ff7862 100644 --- a/src/Service/Utilities/McpStdioHelper.cs +++ b/src/Service/Utilities/McpStdioHelper.cs @@ -78,6 +78,16 @@ public static bool RunMcpStdioHost(IHost host) { try { + // Stdio mode never calls host.Run(), so Startup.Configure -- and with it + // PerformOnConfigChangeAsync, the only caller of IMetadataProviderFactory + // .InitializeAsync() -- never executes. Without this, entities are known to + // the tool registry (their names come from config) while no entity ever gets + // a database object, and every tool call fails with + // "Database object for entity '' has not been inferred." + Core.Services.MetadataProviders.IMetadataProviderFactory metadataProviderFactory = + host.Services.GetRequiredService(); + metadataProviderFactory.InitializeAsync().GetAwaiter().GetResult(); + Mcp.Core.McpToolRegistry registry = host.Services.GetRequiredService(); IEnumerable tools =