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.")]
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 Value attribute with index 0 makes "role" a positional argument, but the help text suggests it should be prefixed with "role:" (e.g., "role:anonymous"). This conflicts with how positional arguments work in command-line parsing. Either change this to an Option attribute if it should be "--role" or "role:value" format, or update the help text to clarify the expected format for positional arguments.

Suggested change
[Value(0, MetaName = "role", Required = false, HelpText = "Optional MCP permissions role, e.g. role:anonymous. If omitted, defaults to anonymous.")]
[Value(0, MetaName = "role", Required = false, HelpText = "Optional MCP permissions role name, e.g. anonymous. If omitted, defaults to anonymous.")]

Copilot uses AI. Check for mistakes.
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);
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 argument being added should be prefixed with "role:" to match the parsing logic in McpStdioHelper.ShouldRunMcpStdio which expects the role argument to start with "role:". Currently, only the effectiveRole value is being added, but the parsing code on line 35 of McpStdioHelper.cs looks for arguments starting with "role:".

Suggested change
args.Add(effectiveRole);
args.Add($"role:{effectiveRole}");

Copilot uses AI. Check for mistakes.
}

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);
Comment on lines +117 to +119
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 StartOptions constructor parameter ordering is inconsistent with how the parameters are being passed. The constructor signature expects parameters in the order (verbose, logLevel, isHttpsRedirectionDisabled, mcpStdio, mcpRole, config), but the call is using named parameters which makes the positional parameter 'config' at the end confusing. Consider making all parameters named for clarity, or reorder the constructor parameters to have 'config' before the new mcpStdio/mcpRole parameters to maintain backward compatibility patterns.

Suggested change
config: options.Config!,
mcpStdio: false,
mcpRole: null);
mcpStdio: false,
mcpRole: null,
config: options.Config!);

Copilot uses AI. Check for mistakes.

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;
});
Comment on lines +267 to +271
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.

Calling ClearProviders() on line 267 will remove all logging providers including any that were configured earlier in the method (such as Application Insights, Serilog, etc.). Then immediately adding only the Console provider means that in stdio mode, all other logging configuration will be lost. Consider preserving other providers or selectively removing only the console provider and re-adding it with stderr configuration, rather than clearing all providers.

Copilot uses AI. Check for mistakes.
}
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)
{
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 method call to tool.GetToolMetadata() is made but its return value is discarded using the discard operator. If this is intentionally calling the method for side effects only (such as ensuring lazy initialization), this should be documented with a comment explaining why. Otherwise, if the metadata is not needed, consider whether this call is necessary.

Suggested change
{
{
// Ensure the tool's metadata is initialized/validated before registration.
// The return value is intentionally discarded; only side effects are required here.

Copilot uses AI. Check for mistakes.
_ = 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;
}
}
}