Skip to content
Merged
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
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
"Apim",
"appconfig",
"appservice",
"ASPNETCORE",
"australiacentral",
"australiaeast",
"australiasoutheast",
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
<PackageVersion Include="Azure.ResourceManager.ResourceGraph" Version="1.1.0-beta.4" />
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="13.0.2" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.72.1" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.3" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,15 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
});

var mcpServerBuilder = services.AddMcpServer();
mcpServerBuilder.WithStdioServerTransport();

if (serviceStartOptions.EnableInsecureTransports)
{
mcpServerBuilder.WithHttpTransport();
}
else
{
mcpServerBuilder.WithStdioServerTransport();
}

return services;
}
Expand Down
130 changes: 130 additions & 0 deletions core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Core.Areas.Server.Options;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Helpers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.AspNetCore;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
Expand All @@ -24,6 +29,7 @@ public sealed class ServiceStartCommand : BaseCommand
private readonly Option<string[]?> _namespaceOption = ServiceOptionDefinitions.Namespace;
private readonly Option<string?> _modeOption = ServiceOptionDefinitions.Mode;
private readonly Option<bool?> _readOnlyOption = ServiceOptionDefinitions.ReadOnly;
private readonly Option<bool> _enableInsecureTransportsOption = ServiceOptionDefinitions.EnableInsecureTransports;

/// <summary>
/// Gets the name of the command.
Expand Down Expand Up @@ -58,6 +64,7 @@ protected override void RegisterOptions(Command command)
command.AddOption(_namespaceOption);
command.AddOption(_modeOption);
command.AddOption(_readOnlyOption);
command.AddOption(_enableInsecureTransportsOption);
}

/// <summary>
Expand Down Expand Up @@ -85,12 +92,24 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
throw new ArgumentException($"Invalid mode '{mode}'. Valid modes are: {ModeTypes.SingleToolProxy}, {ModeTypes.NamespaceProxy}, {ModeTypes.All}.");
}

var enableInsecureTransports = parseResult.GetValueForOption(_enableInsecureTransportsOption);

if (enableInsecureTransports)
{
var includeProdCreds = EnvironmentHelpers.GetEnvironmentVariableAsBool("AZURE_MCP_INCLUDE_PRODUCTION_CREDENTIALS");
if (!includeProdCreds)
{
throw new InvalidOperationException("Using --enable-insecure-transport requires the host to have either Managed Identity or Workload Identity enabled. Please refer to the troubleshooting guidelines here at https://aka.ms/azmcp/troubleshooting.");
}
}

var serverOptions = new ServiceStartOptions
{
Transport = parseResult.GetValueForOption(_transportOption) ?? TransportTypes.StdIo,
Namespace = namespaces,
Mode = mode,
ReadOnly = readOnly,
EnableInsecureTransports = enableInsecureTransports,
};

using var host = CreateHost(serverOptions);
Expand Down Expand Up @@ -118,6 +137,23 @@ private static bool IsValidMode(string? mode)
/// <param name="serverOptions">The server configuration options.</param>
/// <returns>An IHost instance configured for the MCP server.</returns>
private IHost CreateHost(ServiceStartOptions serverOptions)
{
if (serverOptions.EnableInsecureTransports)
{
return CreateHttpHost(serverOptions);
}
else
{
return CreateStdioHost(serverOptions);
}
}

/// <summary>
/// Creates a host for STDIO transport.
/// </summary>
/// <param name="serverOptions">The server configuration options.</param>
/// <returns>An IHost instance configured for STDIO transport.</returns>
private IHost CreateStdioHost(ServiceStartOptions serverOptions)
{
return Host.CreateDefaultBuilder()
.ConfigureLogging(logging =>
Expand All @@ -134,6 +170,55 @@ private IHost CreateHost(ServiceStartOptions serverOptions)
.Build();
}

/// <summary>
/// Creates a host for HTTP transport.
/// </summary>
/// <param name="serverOptions">The server configuration options.</param>
/// <returns>An IHost instance configured for HTTP transport.</returns>
private IHost CreateHttpHost(ServiceStartOptions serverOptions)
{
return Host.CreateDefaultBuilder()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.ConfigureOpenTelemetryLogger();
logging.AddEventSourceLogger();
logging.AddConsole();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureServices(services =>
{
services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

ConfigureServices(services);
ConfigureMcpServer(services, serverOptions);
});

webBuilder.Configure(app =>
{
app.UseCors("AllowAll");
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapMcp();
});
});

var url = GetSafeAspNetCoreUrl();
webBuilder.UseUrls(url);
})
.Build();
}

/// <summary>
/// Configures the MCP server services.
/// </summary>
Expand All @@ -144,6 +229,51 @@ private static void ConfigureMcpServer(IServiceCollection services, ServiceStart
services.AddAzureMcpServer(options);
}

/// <summary>
/// Gets a safe ASP.NET Core URL with security validation.
/// </summary>
/// <returns>A validated URL string for ASP.NET Core binding.</returns>
private static string GetSafeAspNetCoreUrl()
{
string url = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://127.0.0.1:5001";

if (url.Contains(';'))
{
throw new InvalidOperationException("Multiple endpoints in ASPNETCORE_URLS are not supported. Provide a single URL.");
}

if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
throw new InvalidOperationException($"Invalid URL: '{url}'");
}

var scheme = uri.Scheme.ToLowerInvariant();
if (scheme is not ("http" or "https"))
{
throw new InvalidOperationException($"Unsupported scheme '{scheme}' in URL '{url}'.");
}

// loopback IP: 127.0.0.0/8 range, IPv6 (::1) and localhost when resolved to loopback addresses.
bool isLoopback = uri.IsLoopback;

// All interfaces, allowed only with ALLOW_INSECURE_EXTERNAL_BINDING opt-in.
bool isWildcard = uri.Host is "*" or "+" or "0.0.0.0" or "::" || (IPAddress.TryParse(uri.Host, out var anyIp) && (anyIp.Equals(IPAddress.Any) || anyIp.Equals(IPAddress.IPv6Any)));

if (!isLoopback && !isWildcard)
{
throw new InvalidOperationException($"Explicit external binding is not supported for '{url}'.");
}

if (isWildcard && !EnvironmentHelpers.GetEnvironmentVariableAsBool("ALLOW_INSECURE_EXTERNAL_BINDING"))
{
throw new InvalidOperationException(
$"External binding blocked for '{url}'. " +
$"Set ALLOW_INSECURE_EXTERNAL_BINDING=true if you intentionally want to bind beyond loopback.");
}

return url;
}

/// <summary>
/// Hosted service for running the MCP server using standard input/output.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static class ServiceOptionDefinitions
public const string NamespaceName = "namespace";
public const string ModeName = "mode";
public const string ReadOnlyName = "read-only";
public const string EnableInsecureTransportsName = "enable-insecure-transports";

public static readonly Option<string> Transport = new(
$"--{TransportName}",
Expand Down Expand Up @@ -44,4 +45,13 @@ public static class ServiceOptionDefinitions
$"--{ReadOnlyName}",
() => null,
"Whether the MCP server should be read-only. If true, no write operations will be allowed.");

public static readonly Option<bool> EnableInsecureTransports = new(
$"--{EnableInsecureTransportsName}",
() => false,
"Enable insecure transport")
{
IsRequired = false,
IsHidden = true
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ public class ServiceStartOptions
/// </summary>
[JsonPropertyName("readOnly")]
public bool? ReadOnly { get; set; } = null;

/// <summary>
/// Gets or sets whether insecure transport mechanisms are enabled.
/// </summary>
[JsonPropertyName("enableInsecureTransports")]
public bool EnableInsecureTransports { get; set; } = false;
}
1 change: 1 addition & 0 deletions core/Azure.Mcp.Core/src/Azure.Mcp.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Azure.ResourceManager.ResourceGraph" />
<PackageReference Include="Microsoft.Extensions.Azure" />
<PackageReference Include="ModelContextProtocol.AspNetCore" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>
Loading