Skip to content

Commit 9f60b30

Browse files
authored
Added support for MCP custom handler preview on Flex (#11355)
* Added support for MCP custom handler preview: worker runtime environment is set to "custom" when enabled (Flex Consumption SKU only) * PR Feedback fixes. * Cleanup * styling * remove unused method. * Linebreak. * Moved the change to reset FWR one level up to StandByManager. Changed custom handler to native executable via AOT. Improved tests. * remove line break. * Fixed a test input (copy paste mistake)
1 parent 156b305 commit 9f60b30

File tree

9 files changed

+142
-3
lines changed

9 files changed

+142
-3
lines changed

WebJobs.Script.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebJobsStartupTests", "test
6565
EndProject
6666
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebJobs.Script.Tests.Shared", "test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.csproj", "{6C6ABC17-1262-4AD8-8CDA-AF6819EF60EE}"
6767
EndProject
68+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCustomHandler", "test\Resources\TestProjects\DotNetCustomHandler\DotNetCustomHandler.csproj", "{276857C1-88C8-994D-593E-AE6FA9B14556}"
69+
EndProject
6870
Global
6971
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7072
Debug|Any CPU = Debug|Any CPU
@@ -145,6 +147,10 @@ Global
145147
{6C6ABC17-1262-4AD8-8CDA-AF6819EF60EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
146148
{6C6ABC17-1262-4AD8-8CDA-AF6819EF60EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
147149
{6C6ABC17-1262-4AD8-8CDA-AF6819EF60EE}.Release|Any CPU.Build.0 = Release|Any CPU
150+
{276857C1-88C8-994D-593E-AE6FA9B14556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
151+
{276857C1-88C8-994D-593E-AE6FA9B14556}.Debug|Any CPU.Build.0 = Debug|Any CPU
152+
{276857C1-88C8-994D-593E-AE6FA9B14556}.Release|Any CPU.ActiveCfg = Release|Any CPU
153+
{276857C1-88C8-994D-593E-AE6FA9B14556}.Release|Any CPU.Build.0 = Release|Any CPU
148154
EndGlobalSection
149155
GlobalSection(SolutionProperties) = preSolution
150156
HideSolutionNode = FALSE
@@ -171,6 +177,7 @@ Global
171177
{98B2CC4A-33EB-4310-B08D-3DB58630E981} = {B2312621-6D0A-429F-9A45-06B45CE69691}
172178
{3746E49F-DD26-4CBD-B961-0ADF6EA84909} = {B2312621-6D0A-429F-9A45-06B45CE69691}
173179
{6C6ABC17-1262-4AD8-8CDA-AF6819EF60EE} = {AFB0F5F7-A612-4F4A-94DD-8B69CABF7970}
180+
{276857C1-88C8-994D-593E-AE6FA9B14556} = {B2312621-6D0A-429F-9A45-06B45CE69691}
174181
EndGlobalSection
175182
GlobalSection(ExtensibilityGlobals) = postSolution
176183
SolutionGuid = {85400884-5FFD-4C27-A571-58CB3C8CAAC5}

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
- Bug fix that fails in-flight invocations when a worker channel shuts down (#11159)
1919
- Adds WebHost and ScriptHost health checks. (#11341, #11183, #11178, #11173, #11161)
2020
- Update Node.js Worker Version to [3.12.0](https://github.com/Azure/azure-functions-nodejs-worker/releases/tag/v3.12.0)
21+
- Added support for MCP custom handler. (#11355)
2122
- Update Python Worker Version to [4.40.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.40.0)

src/WebJobs.Script.WebHost/Standby/StandbyManager.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
55
using System.IO;
66
using System.Reactive.Linq;
77
using System.Threading;
88
using System.Threading.Tasks;
9-
using Google.Protobuf.WellKnownTypes;
109
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.Azure.WebJobs.Script.Config;
1111
using Microsoft.Azure.WebJobs.Script.Description;
1212
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1313
using Microsoft.Azure.WebJobs.Script.WebHost.Properties;
@@ -92,6 +92,8 @@ public async Task SpecializeHostCoreAsync()
9292
// the PlaceholderSpecializationMiddleware is properly suppressed.
9393
await Task.Yield();
9494

95+
ApplyMcpCustomHandlerSettings();
96+
9597
_logger.LogInformation(Resources.HostSpecializationTrace);
9698

9799
// After specialization, we need to ensure that custom timezone
@@ -184,6 +186,16 @@ public async Task InitializeAsync()
184186
}
185187
}
186188

189+
private void ApplyMcpCustomHandlerSettings()
190+
{
191+
// For Flex Consumption SKU: if MCP custom handler preview is enabled, set worker runtime env to "custom".
192+
if (_environment.IsFlexConsumptionSku() && FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableMcpCustomHandlerPreview, _environment))
193+
{
194+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "custom");
195+
_logger.LogInformation("MCP custom handler preview is enabled. Setting {envVar} to 'custom'", EnvironmentSettingNames.FunctionWorkerRuntime);
196+
}
197+
}
198+
187199
private async Task CreateStandbyWarmupFunctions()
188200
{
189201
ScriptApplicationHostOptions options = _options.CurrentValue;

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ public static class ScriptConstants
139139
public const string FeatureFlagStrictHISModeWarn = "StrictHISModeWarn";
140140
public const string FeatureFlagEnableOrderedInvocationmessages = "EnableOrderedInvocationMessages";
141141
public const string FeatureFlagEnableResponseCompression = "EnableResponseCompression";
142+
public const string FeatureFlagEnableMcpCustomHandlerPreview = "EnableMcpCustomHandlerPreview";
142143
public const string FeatureFlagDisableOrderedInvocationMessages = "DisableOrderedInvocationMessages";
143144
public const string FeatureFlagEnableAzureMonitorTimeIsoFormat = "EnableAzureMonitorTimeIsoFormat";
144145
public const string FeatureFlagEnableTestDataSuppression = "EnableTestDataSuppression";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<PublishAot>true</PublishAot>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var app = WebApplication.CreateBuilder(args).Build();
2+
3+
var port = Environment.GetEnvironmentVariable("FUNCTIONS_CUSTOMHANDLER_PORT")
4+
?? throw new InvalidOperationException("FUNCTIONS_CUSTOMHANDLER_PORT environment variable is not set.");
5+
6+
app.Urls.Add($"http://localhost:{port}");
7+
8+
app.MapGet("/api/SimpleHttpTrigger", () => "Hello from .NET custom handler");
9+
10+
Console.WriteLine($".NET server listening on FUNCTIONS_CUSTOMHANDLER_PORT: {port}");
11+
await app.RunAsync();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"bindings": [
3+
{
4+
"type": "httpTrigger",
5+
"direction": "in",
6+
"authLevel": "anonymous",
7+
"name": "req",
8+
"methods": [
9+
"get",
10+
"post"
11+
]
12+
},
13+
{
14+
"type": "http",
15+
"direction": "out",
16+
"name": "res"
17+
}
18+
]
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version": "2.0",
3+
"extensionBundle": {
4+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
5+
"version": "[4.*, 5.0.0)"
6+
},
7+
"customHandler": {
8+
"description": {
9+
"defaultExecutablePath": "DotNetCustomHandler.exe"
10+
},
11+
"enableProxyingHttpRequest": true
12+
}
13+
}

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class SpecializationE2ETests
5454
private static readonly string _dotnetIsolated60Path = Path.GetFullPath($@"..\..\DotNetIsolated60\{TestHelpers.BuildConfig}");
5555
private static readonly string _dotnetIsolatedUnsuppportedPath = Path.GetFullPath($@"..\..\DotNetIsolatedUnsupportedWorker\{TestHelpers.BuildConfig}");
5656
private static readonly string _dotnetIsolatedEmptyScriptRoot = Path.GetFullPath(@"..\..\..\..\EmptyScriptRoot");
57+
private static readonly string _dotnetCustomHandlerPath = Path.GetFullPath($@"..\..\DotNetCustomHandler\{TestHelpers.BuildConfig}");
5758

5859
private static Action<IServiceCollection> _customizeScriptHostServices;
5960

@@ -800,6 +801,57 @@ public async Task Specialization_JobHostInternalStorageOptionsUpdatesWithActiveH
800801
}
801802
}
802803

804+
[Theory]
805+
[InlineData(ScriptConstants.FlexConsumptionSku, ScriptConstants.FeatureFlagEnableMcpCustomHandlerPreview, true)]
806+
[InlineData(ScriptConstants.FlexConsumptionSku, $"Feature1,{ScriptConstants.FeatureFlagEnableMcpCustomHandlerPreview}", true)]
807+
[InlineData(ScriptConstants.FlexConsumptionSku, "Feature1", false)]
808+
[InlineData(ScriptConstants.FlexConsumptionSku, null, false)]
809+
[InlineData(ScriptConstants.DynamicSku, null, false)]
810+
[InlineData(ScriptConstants.DynamicSku, ScriptConstants.FeatureFlagEnableMcpCustomHandlerPreview, false)]
811+
[InlineData(ScriptConstants.DynamicSku, $"Feature1,{ScriptConstants.FeatureFlagEnableMcpCustomHandlerPreview}", false)]
812+
[InlineData(ScriptConstants.ElasticPremiumSku, null, false)]
813+
[InlineData(ScriptConstants.ElasticPremiumSku, $"Feature1,{ScriptConstants.FeatureFlagEnableMcpCustomHandlerPreview}", false)]
814+
[InlineData("", null, false)]
815+
public async Task Specialization_FlexSku_McpPreview_SetsWorkerRuntimeToCustom(string websiteSku, string featureFlags, bool isExpectedToResetWorkerRuntime)
816+
{
817+
var environmentVariables = new Dictionary<string, string>
818+
{
819+
{ EnvironmentSettingNames.AzureWebsiteSku, websiteSku }
820+
};
821+
var builder = InitializeDotNetIsolatedPlaceholderBuilder(_dotnetCustomHandlerPath, environmentVariables, "SimpleHttpTrigger");
822+
823+
using var testServer = new TestServer(builder);
824+
var client = testServer.CreateClient();
825+
var response = await client.GetAsync("api/warmup");
826+
response.EnsureSuccessStatusCode();
827+
828+
// Validate that the channel is set up with native worker
829+
var webChannelManager = testServer.Services.GetService<IWebHostRpcWorkerChannelManager>();
830+
var placeholderChannel = await webChannelManager.GetChannels("dotnet-isolated").Single().Value.Task;
831+
Assert.Contains("FunctionsNetHost.exe", placeholderChannel.WorkerProcess.Process.StartInfo.FileName);
832+
Assert.NotNull(placeholderChannel.WorkerProcess.Process.Id);
833+
834+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, featureFlags);
835+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1");
836+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
837+
838+
response = await client.GetAsync("api/SimpleHttpTrigger");
839+
response.EnsureSuccessStatusCode();
840+
841+
var log = _loggerProvider.GetLog();
842+
843+
if (isExpectedToResetWorkerRuntime)
844+
{
845+
// Verify expected logs when running the custom handler executable.
846+
Assert.Contains("MCP custom handler preview is enabled. Setting FUNCTIONS_WORKER_RUNTIME to 'custom'", log);
847+
Assert.Contains("Mapped function route 'api/SimpleHttpTrigger'", log);
848+
}
849+
else
850+
{
851+
Assert.DoesNotContain("MCP custom handler preview is enabled. Setting FUNCTIONS_WORKER_RUNTIME to 'custom'", log);
852+
}
853+
}
854+
803855
[Fact]
804856
public async Task DotNetIsolated_PlaceholderHit()
805857
{
@@ -865,7 +917,7 @@ public async Task ResponseCompressionWorksAfterSpecialization(string acceptEncod
865917

866918
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1");
867919
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
868-
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags , ScriptConstants.FeatureFlagEnableResponseCompression);
920+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableResponseCompression);
869921

870922
response = await client.GetAsync("api/HttpRequestDataFunction");
871923
response.EnsureSuccessStatusCode();
@@ -1216,12 +1268,25 @@ private async Task DotNetIsolatedPlaceholderMiss(string scriptRootPath, Action a
12161268
}
12171269

12181270
private IWebHostBuilder InitializeDotNetIsolatedPlaceholderBuilder(string scriptRootPath, params string[] functions)
1271+
{
1272+
return InitializeDotNetIsolatedPlaceholderBuilder(scriptRootPath, null, functions);
1273+
}
1274+
1275+
private IWebHostBuilder InitializeDotNetIsolatedPlaceholderBuilder(string scriptRootPath, Dictionary<string, string> environmentVariables, params string[] functions)
12191276
{
12201277
_environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "dotnet-isolated");
12211278
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1");
12221279
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing);
12231280
_environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "6.0");
12241281

1282+
if (environmentVariables is not null)
1283+
{
1284+
foreach (var (key, value) in environmentVariables)
1285+
{
1286+
_environment.SetEnvironmentVariable(key, value);
1287+
}
1288+
}
1289+
12251290
var builder = CreateStandbyHostBuilder(functions);
12261291

12271292
builder.ConfigureAppConfiguration(config =>

0 commit comments

Comments
 (0)