Skip to content

Commit 8d31b21

Browse files
authored
Merge branch 'main' into copilot/fix-health-check-stored-procedures
2 parents f71da2c + d98a4ea commit 8d31b21

10 files changed

Lines changed: 718 additions & 17 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.Extensions.Configuration;
2+
3+
namespace Azure.DataApiBuilder.Mcp.Core
4+
{
5+
/// <summary>
6+
/// Centralized defaults and configuration keys for MCP protocol settings.
7+
/// </summary>
8+
public static class McpProtocolDefaults
9+
{
10+
/// <summary>
11+
/// Default MCP protocol version advertised when no configuration override is provided.
12+
/// </summary>
13+
public const string DEFAULT_PROTOCOL_VERSION = "2025-06-18";
14+
15+
/// <summary>
16+
/// Configuration key used to override the MCP protocol version.
17+
/// </summary>
18+
public const string PROTOCOL_VERSION_CONFIG_KEY = "MCP:ProtocolVersion";
19+
20+
/// <summary>
21+
/// Helper to resolve the effective protocol version from configuration.
22+
/// Falls back to <see cref="DEFAULT_PROTOCOL_VERSION"/> when the key is not set.
23+
/// </summary>
24+
public static string ResolveProtocolVersion(IConfiguration? configuration)
25+
{
26+
return configuration?.GetValue<string>(PROTOCOL_VERSION_CONFIG_KEY) ?? DEFAULT_PROTOCOL_VERSION;
27+
}
28+
}
29+
}
30+

src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Azure.DataApiBuilder.Mcp.Core
2+
{
3+
public interface IMcpStdioServer
4+
{
5+
Task RunAsync(CancellationToken cancellationToken);
6+
}
7+
}

src/Cli/Commands/StartOptions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ public class StartOptions : Options
1919
{
2020
private const string LOGLEVEL_HELPTEXT = "Specifies logging level as provided value. For possible values, see: https://go.microsoft.com/fwlink/?linkid=2263106";
2121

22-
public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, string config)
22+
public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config)
2323
: base(config)
2424
{
2525
// When verbose is true we set LogLevel to information.
2626
LogLevel = verbose is true ? Microsoft.Extensions.Logging.LogLevel.Information : logLevel;
2727
IsHttpsRedirectionDisabled = isHttpsRedirectionDisabled;
28+
McpStdio = mcpStdio;
29+
McpRole = mcpRole;
2830
}
2931

3032
// SetName defines mutually exclusive sets, ie: can not have
@@ -38,14 +40,21 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis
3840
[Option("no-https-redirect", Required = false, HelpText = "Disables automatic https redirects.")]
3941
public bool IsHttpsRedirectionDisabled { get; }
4042

43+
[Option("mcp-stdio", Required = false, HelpText = "Run Data API Builder in MCP stdio mode while starting the engine.")]
44+
public bool McpStdio { get; }
45+
46+
[Value(0, MetaName = "role", Required = false, HelpText = "Optional MCP permissions role, e.g. role:anonymous. If omitted, defaults to anonymous.")]
47+
public string? McpRole { get; }
48+
4149
public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
4250
{
4351
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
4452
bool isSuccess = ConfigGenerator.TryStartEngineWithOptions(this, loader, fileSystem);
4553

4654
if (!isSuccess)
4755
{
48-
logger.LogError("Failed to start the engine.");
56+
logger.LogError("Failed to start the engine{mode}.",
57+
McpStdio ? " in MCP stdio mode" : string.Empty);
4958
}
5059

5160
return isSuccess ? CliReturnCode.SUCCESS : CliReturnCode.GENERAL_ERROR;

src/Cli/ConfigGenerator.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,6 +2359,17 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
23592359
args.Add(Startup.NO_HTTPS_REDIRECT_FLAG);
23602360
}
23612361

2362+
// If MCP stdio was requested, append the stdio-specific switches.
2363+
if (options.McpStdio)
2364+
{
2365+
string effectiveRole = string.IsNullOrWhiteSpace(options.McpRole)
2366+
? "anonymous"
2367+
: options.McpRole;
2368+
2369+
args.Add("--mcp-stdio");
2370+
args.Add(effectiveRole);
2371+
}
2372+
23622373
return Azure.DataApiBuilder.Service.Program.StartEngine(args.ToArray());
23632374
}
23642375

src/Cli/Exporter.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ private static async Task ExportGraphQL(
110110
}
111111
else
112112
{
113-
StartOptions startOptions = new(false, LogLevel.None, false, options.Config!);
113+
StartOptions startOptions = new(
114+
verbose: false,
115+
logLevel: LogLevel.None,
116+
isHttpsRedirectionDisabled: false,
117+
config: options.Config!,
118+
mcpStdio: false,
119+
mcpRole: null);
114120

115121
Task dabService = Task.Run(() =>
116122
{

src/Config/DeserializationVariableReplacementSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options)
259259
clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? AKVRetryPolicyOptions.DEFAULT_NETWORK_TIMEOUT_SECONDS);
260260
}
261261

262-
return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions);
262+
return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); // CodeQL [SM05137] DefaultAzureCredential will use Managed Identity if available or fallback to default.
263263
}
264264

265265
private string? GetAkvVariable(string name)

src/Service/Program.cs

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// Licensed under the MIT License.
3-
41
using System;
52
using System.CommandLine;
63
using System.CommandLine.Parsing;
74
using System.Runtime.InteropServices;
5+
using System.Text;
86
using System.Text.RegularExpressions;
97
using System.Threading.Tasks;
108
using Azure.DataApiBuilder.Config;
119
using Azure.DataApiBuilder.Service.Exceptions;
1210
using Azure.DataApiBuilder.Service.Telemetry;
11+
using Azure.DataApiBuilder.Service.Utilities;
1312
using Microsoft.ApplicationInsights;
1413
using Microsoft.AspNetCore;
1514
using Microsoft.AspNetCore.Builder;
1615
using Microsoft.AspNetCore.Hosting;
1716
using Microsoft.Extensions.Configuration;
17+
using Microsoft.Extensions.DependencyInjection;
1818
using Microsoft.Extensions.Hosting;
1919
using Microsoft.Extensions.Logging;
2020
using Microsoft.Extensions.Logging.ApplicationInsights;
@@ -33,27 +33,41 @@ public class Program
3333

3434
public static void Main(string[] args)
3535
{
36+
bool runMcpStdio = McpStdioHelper.ShouldRunMcpStdio(args, out string? mcpRole);
37+
38+
if (runMcpStdio)
39+
{
40+
Console.OutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
41+
Console.InputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
42+
}
43+
3644
if (!ValidateAspNetCoreUrls())
3745
{
3846
Console.Error.WriteLine("Invalid ASPNETCORE_URLS format. e.g.: ASPNETCORE_URLS=\"http://localhost:5000;https://localhost:5001\"");
3947
Environment.ExitCode = -1;
4048
return;
4149
}
4250

43-
if (!StartEngine(args))
51+
if (!StartEngine(args, runMcpStdio, mcpRole))
4452
{
4553
Environment.ExitCode = -1;
4654
}
4755
}
4856

49-
public static bool StartEngine(string[] args)
57+
public static bool StartEngine(string[] args, bool runMcpStdio, string? mcpRole)
5058
{
51-
// Unable to use ILogger because this code is invoked before LoggerFactory
52-
// is instantiated.
5359
Console.WriteLine("Starting the runtime engine...");
5460
try
5561
{
56-
CreateHostBuilder(args).Build().Run();
62+
IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build();
63+
64+
if (runMcpStdio)
65+
{
66+
return McpStdioHelper.RunMcpStdioHost(host);
67+
}
68+
69+
// Normal web mode
70+
host.Run();
5771
return true;
5872
}
5973
// Catch exception raised by explicit call to IHostApplicationLifetime.StopApplication()
@@ -72,17 +86,28 @@ public static bool StartEngine(string[] args)
7286
}
7387
}
7488

75-
public static IHostBuilder CreateHostBuilder(string[] args)
89+
// Compatibility overload used by external callers that do not pass the runMcpStdio flag.
90+
public static bool StartEngine(string[] args)
91+
{
92+
bool runMcpStdio = McpStdioHelper.ShouldRunMcpStdio(args, out string? mcpRole);
93+
return StartEngine(args, runMcpStdio, mcpRole: mcpRole);
94+
}
95+
96+
public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, string? mcpRole)
7697
{
7798
return Host.CreateDefaultBuilder(args)
7899
.ConfigureAppConfiguration(builder =>
79100
{
80101
AddConfigurationProviders(builder, args);
102+
if (runMcpStdio)
103+
{
104+
McpStdioHelper.ConfigureMcpStdio(builder, mcpRole);
105+
}
81106
})
82107
.ConfigureWebHostDefaults(webBuilder =>
83108
{
84109
Startup.MinimumLogLevel = GetLogLevelFromCommandLineArgs(args, out Startup.IsLogLevelOverriddenByCli);
85-
ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel);
110+
ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel, stdio: runMcpStdio);
86111
ILogger<Startup> startupLogger = loggerFactory.CreateLogger<Startup>();
87112
DisableHttpsRedirectionIfNeeded(args);
88113
webBuilder.UseStartup(builder => new Startup(builder.Configuration, startupLogger));
@@ -140,7 +165,14 @@ private static ParseResult GetParseResult(Command cmd, string[] args)
140165
/// <param name="appTelemetryClient">Telemetry client</param>
141166
/// <param name="logLevelInitializer">Hot-reloadable log level</param>
142167
/// <param name="serilogLogger">Core Serilog logging pipeline</param>
143-
public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, TelemetryClient? appTelemetryClient = null, LogLevelInitializer? logLevelInitializer = null, Logger? serilogLogger = null)
168+
/// <param name="stdio">Whether the logger is for stdio mode</param>
169+
/// <returns>ILoggerFactory</returns>
170+
public static ILoggerFactory GetLoggerFactoryForLogLevel(
171+
LogLevel logLevel,
172+
TelemetryClient? appTelemetryClient = null,
173+
LogLevelInitializer? logLevelInitializer = null,
174+
Logger? serilogLogger = null,
175+
bool stdio = false)
144176
{
145177
return LoggerFactory
146178
.Create(builder =>
@@ -229,7 +261,19 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, Tele
229261
}
230262
}
231263

232-
builder.AddConsole();
264+
// In stdio mode, route console logs to STDERR to keep STDOUT clean for MCP JSON
265+
if (stdio)
266+
{
267+
builder.ClearProviders();
268+
builder.AddConsole(options =>
269+
{
270+
options.LogToStandardErrorThreshold = LogLevel.Trace;
271+
});
272+
}
273+
else
274+
{
275+
builder.AddConsole();
276+
}
233277
});
234278
}
235279

src/Service/Startup.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,16 @@ public void ConfigureServices(IServiceCollection services)
348348
return handler;
349349
});
350350

351-
if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development)
351+
bool isMcpStdio = Configuration.GetValue<bool>("MCP:StdioMode");
352+
353+
if (isMcpStdio)
354+
{
355+
// Explicitly force Simulator when running in MCP stdio mode.
356+
services.AddAuthentication(
357+
defaultScheme: SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME)
358+
.AddSimulatorAuthentication();
359+
}
360+
else if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development)
352361
{
353362
// Development mode implies support for "Hot Reload". The V2 authentication function
354363
// wires up all DAB supported authentication providers (schemes) so that at request time,
@@ -456,6 +465,8 @@ public void ConfigureServices(IServiceCollection services)
456465

457466
services.AddDabMcpServer(configProvider);
458467

468+
services.AddSingleton<IMcpStdioServer, McpStdioServer>();
469+
459470
services.AddControllers();
460471
}
461472

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Hosting;
6+
7+
namespace Azure.DataApiBuilder.Service.Utilities
8+
{
9+
/// <summary>
10+
/// Helper methods for configuring and running MCP in stdio mode.
11+
/// </summary>
12+
internal static class McpStdioHelper
13+
{
14+
/// <summary>
15+
/// Determines if MCP stdio mode should be run based on command line arguments.
16+
/// </summary>
17+
/// <param name="args"> The command line arguments.</param>
18+
/// <param name="mcpRole"> The role for MCP stdio mode, if specified.</param>
19+
/// <returns></returns>
20+
public static bool ShouldRunMcpStdio(string[] args, out string? mcpRole)
21+
{
22+
mcpRole = null;
23+
24+
bool runMcpStdio = Array.Exists(
25+
args,
26+
a => string.Equals(a, "--mcp-stdio", StringComparison.OrdinalIgnoreCase));
27+
28+
if (!runMcpStdio)
29+
{
30+
return false;
31+
}
32+
33+
string? roleArg = Array.Find(
34+
args,
35+
a => a != null && a.StartsWith("role:", StringComparison.OrdinalIgnoreCase));
36+
37+
if (!string.IsNullOrEmpty(roleArg))
38+
{
39+
string roleValue = roleArg[(roleArg.IndexOf(':') + 1)..];
40+
if (!string.IsNullOrWhiteSpace(roleValue))
41+
{
42+
mcpRole = roleValue;
43+
}
44+
}
45+
46+
return true;
47+
}
48+
49+
/// <summary>
50+
/// Configures the IConfigurationBuilder for MCP stdio mode.
51+
/// </summary>
52+
/// <param name="builder"></param>
53+
/// <param name="mcpRole"></param>
54+
public static void ConfigureMcpStdio(IConfigurationBuilder builder, string? mcpRole)
55+
{
56+
builder.AddInMemoryCollection(new Dictionary<string, string?>
57+
{
58+
["MCP:StdioMode"] = "true",
59+
["MCP:Role"] = mcpRole ?? "anonymous",
60+
["Runtime:Host:Authentication:Provider"] = "Simulator"
61+
});
62+
}
63+
64+
/// <summary>
65+
/// Runs the MCP stdio host.
66+
/// </summary>
67+
/// <param name="host"> The host to run.</param>
68+
public static bool RunMcpStdioHost(IHost host)
69+
{
70+
host.Start();
71+
72+
Mcp.Core.McpToolRegistry registry =
73+
host.Services.GetRequiredService<Mcp.Core.McpToolRegistry>();
74+
IEnumerable<Mcp.Model.IMcpTool> tools =
75+
host.Services.GetServices<Mcp.Model.IMcpTool>();
76+
77+
foreach (Mcp.Model.IMcpTool tool in tools)
78+
{
79+
_ = tool.GetToolMetadata();
80+
registry.RegisterTool(tool);
81+
}
82+
83+
IServiceScopeFactory scopeFactory =
84+
host.Services.GetRequiredService<IServiceScopeFactory>();
85+
using IServiceScope scope = scopeFactory.CreateScope();
86+
IHostApplicationLifetime lifetime =
87+
scope.ServiceProvider.GetRequiredService<IHostApplicationLifetime>();
88+
Mcp.Core.IMcpStdioServer stdio =
89+
scope.ServiceProvider.GetRequiredService<Mcp.Core.IMcpStdioServer>();
90+
91+
stdio.RunAsync(lifetime.ApplicationStopping).GetAwaiter().GetResult();
92+
host.StopAsync().GetAwaiter().GetResult();
93+
94+
return true;
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)