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
30 changes: 30 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.Extensions.Configuration;

namespace Azure.DataApiBuilder.Mcp.Core
{
/// <summary>
/// Centralized defaults and configuration keys for MCP protocol settings.
/// </summary>
public static class McpProtocolDefaults
{
/// <summary>
/// Default MCP protocol version advertised when no configuration override is provided.
/// </summary>
public const string DEFAULT_PROTOCOL_VERSION = "2025-06-18";

/// <summary>
/// Configuration key used to override the MCP protocol version.
/// </summary>
public const string PROTOCOL_VERSION_CONFIG_KEY = "MCP:ProtocolVersion";

/// <summary>
/// Helper to resolve the effective protocol version from configuration.
/// Falls back to <see cref="DEFAULT_PROTOCOL_VERSION"/> when the key is not set.
/// </summary>
public static string ResolveProtocolVersion(IConfiguration? configuration)
{
return configuration?.GetValue<string>(PROTOCOL_VERSION_CONFIG_KEY) ?? DEFAULT_PROTOCOL_VERSION;
}
}
}

486 changes: 486 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/IMcpStdioServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Azure.DataApiBuilder.Mcp.Core
{
public interface IMcpStdioServer
{
Task RunAsync(CancellationToken cancellationToken);
}
}
Comment on lines +1 to +7
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface is missing necessary using directives. The Task type is used in the method signature but the System.Threading.Tasks namespace is not imported. Additionally, CancellationToken requires System.Threading namespace.

Copilot uses AI. Check for mistakes.
13 changes: 11 additions & 2 deletions src/Cli/Commands/StartOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ public class StartOptions : Options
{
private const string LOGLEVEL_HELPTEXT = "Specifies logging level as provided value. For possible values, see: https://go.microsoft.com/fwlink/?linkid=2263106";

public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, string config)
public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config)
: base(config)
{
// When verbose is true we set LogLevel to information.
LogLevel = verbose is true ? Microsoft.Extensions.Logging.LogLevel.Information : logLevel;
IsHttpsRedirectionDisabled = isHttpsRedirectionDisabled;
McpStdio = mcpStdio;
McpRole = mcpRole;
}

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

[Option("mcp-stdio", Required = false, HelpText = "Run Data API Builder in MCP stdio mode while starting the engine.")]
public bool McpStdio { get; }

[Value(0, MetaName = "role", Required = false, HelpText = "Optional MCP permissions role, e.g. role:anonymous. If omitted, defaults to anonymous.")]
public string? McpRole { get; }

public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
bool isSuccess = ConfigGenerator.TryStartEngineWithOptions(this, loader, fileSystem);

if (!isSuccess)
{
logger.LogError("Failed to start the engine.");
logger.LogError("Failed to start the engine{mode}.",
McpStdio ? " in MCP stdio mode" : string.Empty);
}

return isSuccess ? CliReturnCode.SUCCESS : CliReturnCode.GENERAL_ERROR;
Expand Down
11 changes: 11 additions & 0 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2359,6 +2359,17 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
args.Add(Startup.NO_HTTPS_REDIRECT_FLAG);
}

// If MCP stdio was requested, append the stdio-specific switches.
if (options.McpStdio)
{
string effectiveRole = string.IsNullOrWhiteSpace(options.McpRole)
? "anonymous"
: options.McpRole;

args.Add("--mcp-stdio");
args.Add(effectiveRole);
}

return Azure.DataApiBuilder.Service.Program.StartEngine(args.ToArray());
}

Expand Down
8 changes: 7 additions & 1 deletion src/Cli/Exporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ private static async Task ExportGraphQL(
}
else
{
StartOptions startOptions = new(false, LogLevel.None, false, options.Config!);
StartOptions startOptions = new(
verbose: false,
logLevel: LogLevel.None,
isHttpsRedirectionDisabled: false,
config: options.Config!,
mcpStdio: false,
mcpRole: null);

Task dabService = Task.Run(() =>
{
Expand Down
68 changes: 56 additions & 12 deletions src/Service/Program.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyright header comment was removed from this file. All source files in the repository should maintain consistent copyright headers for legal and attribution purposes.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this

using System.CommandLine;
using System.CommandLine.Parsing;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.Telemetry;
using Azure.DataApiBuilder.Service.Utilities;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
Expand All @@ -33,27 +33,41 @@ public class Program

public static void Main(string[] args)
{
bool runMcpStdio = McpStdioHelper.ShouldRunMcpStdio(args, out string? mcpRole);

if (runMcpStdio)
{
Console.OutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
Console.InputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
}

if (!ValidateAspNetCoreUrls())
{
Console.Error.WriteLine("Invalid ASPNETCORE_URLS format. e.g.: ASPNETCORE_URLS=\"http://localhost:5000;https://localhost:5001\"");
Environment.ExitCode = -1;
return;
}

if (!StartEngine(args))
if (!StartEngine(args, runMcpStdio, mcpRole))
{
Environment.ExitCode = -1;
}
}

public static bool StartEngine(string[] args)
public static bool StartEngine(string[] args, bool runMcpStdio, string? mcpRole)
{
// Unable to use ILogger because this code is invoked before LoggerFactory
// is instantiated.
Console.WriteLine("Starting the runtime engine...");
try
{
CreateHostBuilder(args).Build().Run();
IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build();

if (runMcpStdio)
{
return McpStdioHelper.RunMcpStdioHost(host);
}

// Normal web mode
host.Run();
return true;
}
// Catch exception raised by explicit call to IHostApplicationLifetime.StopApplication()
Expand All @@ -72,17 +86,28 @@ public static bool StartEngine(string[] args)
}
}

public static IHostBuilder CreateHostBuilder(string[] args)
// Compatibility overload used by external callers that do not pass the runMcpStdio flag.
public static bool StartEngine(string[] args)
{
bool runMcpStdio = McpStdioHelper.ShouldRunMcpStdio(args, out string? mcpRole);
return StartEngine(args, runMcpStdio, mcpRole: mcpRole);
}

public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, string? mcpRole)
{
return Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder =>
{
AddConfigurationProviders(builder, args);
if (runMcpStdio)
{
McpStdioHelper.ConfigureMcpStdio(builder, mcpRole);
}
})
.ConfigureWebHostDefaults(webBuilder =>
{
Startup.MinimumLogLevel = GetLogLevelFromCommandLineArgs(args, out Startup.IsLogLevelOverriddenByCli);
ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel);
ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel, stdio: runMcpStdio);
ILogger<Startup> startupLogger = loggerFactory.CreateLogger<Startup>();
DisableHttpsRedirectionIfNeeded(args);
webBuilder.UseStartup(builder => new Startup(builder.Configuration, startupLogger));
Expand Down Expand Up @@ -140,7 +165,14 @@ private static ParseResult GetParseResult(Command cmd, string[] args)
/// <param name="appTelemetryClient">Telemetry client</param>
/// <param name="logLevelInitializer">Hot-reloadable log level</param>
/// <param name="serilogLogger">Core Serilog logging pipeline</param>
public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, TelemetryClient? appTelemetryClient = null, LogLevelInitializer? logLevelInitializer = null, Logger? serilogLogger = null)
/// <param name="stdio">Whether the logger is for stdio mode</param>
/// <returns>ILoggerFactory</returns>
public static ILoggerFactory GetLoggerFactoryForLogLevel(
LogLevel logLevel,
TelemetryClient? appTelemetryClient = null,
LogLevelInitializer? logLevelInitializer = null,
Logger? serilogLogger = null,
bool stdio = false)
{
return LoggerFactory
.Create(builder =>
Expand Down Expand Up @@ -229,7 +261,19 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, Tele
}
}

builder.AddConsole();
// In stdio mode, route console logs to STDERR to keep STDOUT clean for MCP JSON
if (stdio)
{
builder.ClearProviders();
builder.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
}
else
{
builder.AddConsole();
}
});
}

Expand Down
13 changes: 12 additions & 1 deletion src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,16 @@ public void ConfigureServices(IServiceCollection services)
return handler;
});

if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development)
bool isMcpStdio = Configuration.GetValue<bool>("MCP:StdioMode");

if (isMcpStdio)
{
// Explicitly force Simulator when running in MCP stdio mode.
services.AddAuthentication(
defaultScheme: SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME)
.AddSimulatorAuthentication();
}
else if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development)
{
// Development mode implies support for "Hot Reload". The V2 authentication function
// wires up all DAB supported authentication providers (schemes) so that at request time,
Expand Down Expand Up @@ -456,6 +465,8 @@ public void ConfigureServices(IServiceCollection services)

services.AddDabMcpServer(configProvider);

services.AddSingleton<IMcpStdioServer, McpStdioServer>();

services.AddControllers();
}

Expand Down
97 changes: 97 additions & 0 deletions src/Service/Utilities/McpStdioHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Azure.DataApiBuilder.Service.Utilities
{
/// <summary>
/// Helper methods for configuring and running MCP in stdio mode.
/// </summary>
internal static class McpStdioHelper
{
/// <summary>
/// Determines if MCP stdio mode should be run based on command line arguments.
/// </summary>
/// <param name="args"> The command line arguments.</param>
/// <param name="mcpRole"> The role for MCP stdio mode, if specified.</param>
/// <returns></returns>
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The missing return value documentation should be added. The XML documentation comment has the <returns> tag present but no description text following it.

Suggested change
/// <returns></returns>
/// <returns><see langword="true"/> if MCP stdio mode should be run; otherwise, <see langword="false"/>.</returns>

Copilot uses AI. Check for mistakes.
public static bool ShouldRunMcpStdio(string[] args, out string? mcpRole)
{
mcpRole = null;

bool runMcpStdio = Array.Exists(
args,
a => string.Equals(a, "--mcp-stdio", StringComparison.OrdinalIgnoreCase));

if (!runMcpStdio)
{
return false;
}

string? roleArg = Array.Find(
args,
a => a != null && a.StartsWith("role:", StringComparison.OrdinalIgnoreCase));

if (!string.IsNullOrEmpty(roleArg))
{
string roleValue = roleArg[(roleArg.IndexOf(':') + 1)..];
if (!string.IsNullOrWhiteSpace(roleValue))
{
mcpRole = roleValue;
}
}

return true;
}

/// <summary>
/// Configures the IConfigurationBuilder for MCP stdio mode.
/// </summary>
/// <param name="builder"></param>
/// <param name="mcpRole"></param>
public static void ConfigureMcpStdio(IConfigurationBuilder builder, string? mcpRole)
{
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
["MCP:StdioMode"] = "true",
["MCP:Role"] = mcpRole ?? "anonymous",
["Runtime:Host:Authentication:Provider"] = "Simulator"
});
}

/// <summary>
/// Runs the MCP stdio host.
/// </summary>
/// <param name="host"> The host to run.</param>
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name in the XML documentation does not match the actual parameter name. The documentation says "builder" but the parameter is named "host".

Copilot uses AI. Check for mistakes.
public static bool RunMcpStdioHost(IHost host)
{
host.Start();

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

foreach (Mcp.Model.IMcpTool tool in tools)
{
_ = tool.GetToolMetadata();
registry.RegisterTool(tool);
}

IServiceScopeFactory scopeFactory =
host.Services.GetRequiredService<IServiceScopeFactory>();
using IServiceScope scope = scopeFactory.CreateScope();
IHostApplicationLifetime lifetime =
scope.ServiceProvider.GetRequiredService<IHostApplicationLifetime>();
Mcp.Core.IMcpStdioServer stdio =
scope.ServiceProvider.GetRequiredService<Mcp.Core.IMcpStdioServer>();

stdio.RunAsync(lifetime.ApplicationStopping).GetAwaiter().GetResult();
host.StopAsync().GetAwaiter().GetResult();

return true;
}
}
}