Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/Service.Tests/UnitTests/McpStdioHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,9 +28,12 @@ public void RunMcpStdioHost_DoesNotStartWebHost()
TestApplicationLifetime lifetime = new();
TestMcpStdioServer stdioServer = new();

TestMetadataProviderFactory metadataProviderFactory = new();

services.AddSingleton<McpToolRegistry>();
services.AddSingleton<IHostApplicationLifetime>(lifetime);
services.AddSingleton<IMcpStdioServer>(stdioServer);
services.AddSingleton<IMetadataProviderFactory>(metadataProviderFactory);

using ServiceProvider serviceProvider = services.BuildServiceProvider();
TestHost host = new(serviceProvider);
Expand All @@ -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 '<name>' 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<string, Dictionary<string, DatabaseObject>> entityToDatabaseObjectMap,
Dictionary<string, Dictionary<string, string>> graphQLStoredProcedureExposedNameToEntityNameMap)
=> InitializeAsyncCallCount++;

public ISqlMetadataProvider GetMetadataProvider(string dataSourceName)
=> throw new NotImplementedException();

public IEnumerable<ISqlMetadataProvider> ListMetadataProviders()
=> Array.Empty<ISqlMetadataProvider>();

public List<Exception> GetAllMetadataExceptions()
=> new();
}

private sealed class TestHost : IHost
Expand Down
10 changes: 10 additions & 0 deletions src/Service/Utilities/McpStdioHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<name>' has not been inferred."
Core.Services.MetadataProviders.IMetadataProviderFactory metadataProviderFactory =
host.Services.GetRequiredService<Core.Services.MetadataProviders.IMetadataProviderFactory>();
metadataProviderFactory.InitializeAsync().GetAwaiter().GetResult();

Mcp.Core.McpToolRegistry registry =
host.Services.GetRequiredService<Mcp.Core.McpToolRegistry>();
IEnumerable<Mcp.Model.IMcpTool> tools =
Expand Down
Loading