From 6cedc6fcba64962ffe28e82195288685da5835a2 Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Fri, 5 Dec 2025 17:36:30 -0800 Subject: [PATCH 1/2] app exec alias support --- dotnet/CLI.md | 4 + .../mcpb.Tests/CliPackFileValidationTests.cs | 193 +++++++++++++++--- .../mcpb/Commands/ManifestCommandHelpers.cs | 188 +++++++++++++++-- dotnet/mcpb/mcpb.csproj | 2 +- 4 files changed, 340 insertions(+), 47 deletions(-) diff --git a/dotnet/CLI.md b/dotnet/CLI.md index 234e7ad..85645d4 100644 --- a/dotnet/CLI.md +++ b/dotnet/CLI.md @@ -166,6 +166,10 @@ Before launching the server or writing the archive, `mcpb pack` now validates th If any of these files are missing, packing fails immediately with an error like `Missing icon file: icon.png`. This happens before dynamic capability discovery so you get fast feedback on manifest inaccuracies. +On Windows, `.exe` entry points or path-like commands can be satisfied by [App Execution Aliases](https://learn.microsoft.com/windows/uwp/launch-resume/run-desktop-applications-as-uwp-apps). When a referenced `.exe` is not present under your extension directory, the CLI automatically checks `%LOCALAPPDATA%\Microsoft\WindowsApps` (the folder where aliases surface). To point discovery/validation at custom alias locations—or to simulate aliases in CI—set `MCPB_WINDOWS_APP_ALIAS_DIRS` to a path-separated list of directories. + +When discovery launches your server it resolves the executable using the same logic, so an alias that passes validation is the exact binary that will be executed. + Commands (e.g. `node`, `python`) that are not path-like are not validated—they are treated as executables resolved by the environment. Examples: diff --git a/dotnet/mcpb.Tests/CliPackFileValidationTests.cs b/dotnet/mcpb.Tests/CliPackFileValidationTests.cs index 13d5f5a..9c13ca4 100644 --- a/dotnet/mcpb.Tests/CliPackFileValidationTests.cs +++ b/dotnet/mcpb.Tests/CliPackFileValidationTests.cs @@ -1,7 +1,8 @@ -using System.Text.Json; -using Xunit; +using System; using System.IO; +using System.Text.Json; using Mcpb.Json; +using Xunit; namespace Mcpb.Tests; @@ -9,12 +10,19 @@ public class CliPackFileValidationTests { private string CreateTempDir() { - var dir = Path.Combine(Path.GetTempPath(), "mcpb_cli_pack_files_" + Guid.NewGuid().ToString("N")); + var dir = Path.Combine( + Path.GetTempPath(), + "mcpb_cli_pack_files_" + Guid.NewGuid().ToString("N") + ); Directory.CreateDirectory(dir); Directory.CreateDirectory(Path.Combine(dir, "server")); return dir; } - private (int exitCode, string stdout, string stderr) InvokeCli(string workingDir, params string[] args) + + private (int exitCode, string stdout, string stderr) InvokeCli( + string workingDir, + params string[] args + ) { var root = Mcpb.Commands.CliRoot.Build(); var prev = Directory.GetCurrentDirectory(); @@ -26,23 +34,31 @@ private string CreateTempDir() var code = CommandRunner.Invoke(root, args, swOut, swErr); return (code, swOut.ToString(), swErr.ToString()); } - finally { Directory.SetCurrentDirectory(prev); } + finally + { + Directory.SetCurrentDirectory(prev); + } } - private Mcpb.Core.McpbManifest BaseManifest() => new Mcpb.Core.McpbManifest - { - Name = "demo", - Description = "desc", - Author = new Mcpb.Core.McpbManifestAuthor { Name = "A" }, - Icon = "icon.png", - Screenshots = new List { "shots/s1.png" }, - Server = new Mcpb.Core.McpbManifestServer + private Mcpb.Core.McpbManifest BaseManifest() => + new Mcpb.Core.McpbManifest { - Type = "node", - EntryPoint = "server/index.js", - McpConfig = new Mcpb.Core.McpServerConfigWithOverrides { Command = "node", Args = new List { "${__dirname}/server/index.js" } } - } - }; + Name = "demo", + Description = "desc", + Author = new Mcpb.Core.McpbManifestAuthor { Name = "A" }, + Icon = "icon.png", + Screenshots = new List { "shots/s1.png" }, + Server = new Mcpb.Core.McpbManifestServer + { + Type = "node", + EntryPoint = "server/index.js", + McpConfig = new Mcpb.Core.McpServerConfigWithOverrides + { + Command = "node", + Args = new List { "${__dirname}/server/index.js" }, + }, + }, + }; [Fact] public void Pack_MissingIcon_Fails() @@ -52,7 +68,10 @@ public void Pack_MissingIcon_Fails() Directory.CreateDirectory(Path.Combine(dir, "shots")); File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake"); var manifest = BaseManifest(); - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.NotEqual(0, code); Assert.Contains("Missing icon file", stderr); @@ -66,7 +85,10 @@ public void Pack_MissingEntryPoint_Fails() Directory.CreateDirectory(Path.Combine(dir, "shots")); File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake"); var manifest = BaseManifest(); - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.NotEqual(0, code); Assert.Contains("Missing entry_point file", stderr); @@ -79,7 +101,10 @@ public void Pack_MissingScreenshot_Fails() File.WriteAllText(Path.Combine(dir, "icon.png"), "fake"); File.WriteAllText(Path.Combine(dir, "server", "index.js"), "// js"); var manifest = BaseManifest(); - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.NotEqual(0, code); Assert.Contains("Missing screenshot file", stderr); @@ -96,12 +121,103 @@ public void Pack_PathLikeCommandMissing_Fails() var manifest = BaseManifest(); // Make command path-like to trigger validation manifest.Server.McpConfig.Command = "${__dirname}/server/missing.js"; - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.NotEqual(0, code); Assert.Contains("Missing server.command file", stderr); } + [Fact] + public void Pack_CommandWindowsAlias_Succeeds() + { + var aliasDir = Path.Combine( + Path.GetTempPath(), + "mcpb_windows_alias_" + Guid.NewGuid().ToString("N") + ); + Directory.CreateDirectory(aliasDir); + var aliasName = "alias-command.exe"; + File.WriteAllText(Path.Combine(aliasDir, aliasName), "alias"); + var previousAliases = Environment.GetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS"); + Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", aliasDir); + try + { + var dir = CreateTempDir(); + File.WriteAllText(Path.Combine(dir, "icon.png"), "fake"); + File.WriteAllText(Path.Combine(dir, "server", "index.js"), "// js"); + Directory.CreateDirectory(Path.Combine(dir, "shots")); + File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake"); + var manifest = BaseManifest(); + manifest.Server.McpConfig.Command = aliasName; + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); + Assert.Equal(0, code); + Assert.Contains("demo@", stdout); + Assert.DoesNotContain("Missing server.command", stderr); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", previousAliases); + try + { + Directory.Delete(aliasDir, true); + } + catch + { + // Ignore cleanup failures in tests + } + } + } + + [Fact] + public void Pack_EntryPointWindowsAlias_Succeeds() + { + var aliasDir = Path.Combine( + Path.GetTempPath(), + "mcpb_windows_alias_" + Guid.NewGuid().ToString("N") + ); + Directory.CreateDirectory(aliasDir); + var aliasName = "alias-entry.exe"; + File.WriteAllText(Path.Combine(aliasDir, aliasName), "alias"); + var previousAliases = Environment.GetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS"); + Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", aliasDir); + try + { + var dir = CreateTempDir(); + File.WriteAllText(Path.Combine(dir, "icon.png"), "fake"); + File.WriteAllText(Path.Combine(dir, "server", "index.js"), "// js"); + Directory.CreateDirectory(Path.Combine(dir, "shots")); + File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake"); + var manifest = BaseManifest(); + manifest.Server.EntryPoint = aliasName; + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); + Assert.Equal(0, code); + Assert.Contains("demo@", stdout); + Assert.DoesNotContain("Missing entry_point", stderr); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", previousAliases); + try + { + Directory.Delete(aliasDir, true); + } + catch + { + // Ignore cleanup failures in tests + } + } + } + [Fact] public void Pack_AllFilesPresent_Succeeds() { @@ -112,7 +228,10 @@ public void Pack_AllFilesPresent_Succeeds() File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake"); var manifest = BaseManifest(); // Ensure command not path-like (node) so validation doesn't require it to exist as file - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.Equal(0, code); Assert.Contains("demo@", stdout); @@ -129,9 +248,12 @@ public void Pack_MissingIconsFile_Fails() manifest.ManifestVersion = "0.3"; manifest.Icons = new List { - new() { Src = "icon-16.png", Size = "16x16" } + new() { Src = "icon-16.png", Size = "16x16" }, }; - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.NotEqual(0, code); Assert.Contains("Missing icons[0] file", stderr); @@ -149,9 +271,12 @@ public void Pack_IconsFilePresent_Succeeds() manifest.Screenshots = null; // Remove screenshots requirement for this test manifest.Icons = new List { - new() { Src = "icon-16.png", Size = "16x16" } + new() { Src = "icon-16.png", Size = "16x16" }, }; - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.True(code == 0, $"Pack failed with code {code}. Stderr: {stderr}"); Assert.Contains("demo@", stdout); @@ -168,9 +293,12 @@ public void Pack_MissingLocalizationResources_Fails() manifest.Localization = new Mcpb.Core.McpbManifestLocalization { Resources = "locales/${locale}/messages.json", - DefaultLocale = "en-US" + DefaultLocale = "en-US", }; - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.NotEqual(0, code); Assert.Contains("Missing localization resources", stderr); @@ -190,9 +318,12 @@ public void Pack_LocalizationResourcesPresent_Succeeds() manifest.Localization = new Mcpb.Core.McpbManifestLocalization { Resources = "locales/${locale}/messages.json", - DefaultLocale = "en-US" + DefaultLocale = "en-US", }; - File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + File.WriteAllText( + Path.Combine(dir, "manifest.json"), + JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions) + ); var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover"); Assert.True(code == 0, $"Pack failed with code {code}. Stderr: {stderr}"); Assert.Contains("demo@", stdout); diff --git a/dotnet/mcpb/Commands/ManifestCommandHelpers.cs b/dotnet/mcpb/Commands/ManifestCommandHelpers.cs index 3496566..c40276d 100644 --- a/dotnet/mcpb/Commands/ManifestCommandHelpers.cs +++ b/dotnet/mcpb/Commands/ManifestCommandHelpers.cs @@ -18,10 +18,15 @@ namespace Mcpb.Commands; internal static class ManifestCommandHelpers { + private const string WindowsAppAliasDirectoriesEnvVar = "MCPB_WINDOWS_APP_ALIAS_DIRS"; private static readonly TimeSpan DiscoveryTimeout = TimeSpan.FromSeconds(30); private static readonly TimeSpan DiscoveryInitializationTimeout = TimeSpan.FromSeconds(15); - private static readonly IReadOnlyDictionary> EmptyUserConfigOverrides = - new Dictionary>(StringComparer.Ordinal); + private static readonly IReadOnlyDictionary< + string, + IReadOnlyList + > EmptyUserConfigOverrides = new Dictionary>( + StringComparer.Ordinal + ); private static readonly Regex UserConfigTokenRegex = new( "\\$\\{user_config\\.([^}]+)\\}", RegexOptions.IgnoreCase | RegexOptions.Compiled @@ -198,7 +203,7 @@ string Resolve(string rel) return Path.Combine(baseDir, normalized.Replace('/', Path.DirectorySeparatorChar)); } - void CheckFile(string? relativePath, string category) + void CheckFile(string? relativePath, string category, bool allowAliasResolution = false) { if (string.IsNullOrWhiteSpace(relativePath)) return; @@ -209,7 +214,20 @@ void CheckFile(string? relativePath, string category) verboseLog?.Invoke($"Ensuring {category} file exists: {relativePath} -> {resolved}"); if (!File.Exists(resolved)) { - errors.Add($"Missing {category} file: {relativePath}"); + var trimmed = relativePath.Trim(); + if ( + allowAliasResolution + && TryResolveWindowsAppExecutionAlias(trimmed, out var aliasPath) + ) + { + verboseLog?.Invoke( + $"Resolved {category} '{trimmed}' via Windows app execution alias: {aliasPath}" + ); + } + else + { + errors.Add($"Missing {category} file: {relativePath}"); + } } } @@ -221,7 +239,7 @@ void CheckFile(string? relativePath, string category) if (!string.IsNullOrWhiteSpace(manifest.Server.EntryPoint)) { verboseLog?.Invoke($"Checking server entry point {manifest.Server.EntryPoint}"); - CheckFile(manifest.Server.EntryPoint, "entry_point"); + CheckFile(manifest.Server.EntryPoint, "entry_point", allowAliasResolution: true); } var command = manifest.Server.McpConfig?.Command; @@ -229,15 +247,7 @@ void CheckFile(string? relativePath, string category) { var cmd = command!; verboseLog?.Invoke($"Resolving server command {cmd}"); - bool pathLike = - cmd.Contains('/') - || cmd.Contains('\\') - || cmd.StartsWith("${__dirname}", StringComparison.OrdinalIgnoreCase) - || cmd.StartsWith("./") - || cmd.StartsWith("..") - || cmd.EndsWith(".js", StringComparison.OrdinalIgnoreCase) - || cmd.EndsWith(".py", StringComparison.OrdinalIgnoreCase) - || cmd.EndsWith(".exe", StringComparison.OrdinalIgnoreCase); + bool pathLike = IsCommandPathLike(cmd); if (pathLike) { var expanded = ExpandToken(cmd, baseDir); @@ -250,7 +260,16 @@ void CheckFile(string? relativePath, string category) verboseLog?.Invoke($"Ensuring server command file exists: {resolved}"); if (!File.Exists(resolved)) { - errors.Add($"Missing server.command file: {command}"); + if (TryResolveWindowsAppExecutionAlias(cmd, out var aliasPath)) + { + verboseLog?.Invoke( + $"Resolved server command '{cmd}' via Windows app execution alias: {aliasPath}" + ); + } + else + { + errors.Add($"Missing server.command file: {command}"); + } } } } @@ -309,6 +328,117 @@ void CheckFile(string? relativePath, string category) return errors; } + private static bool TryResolveWindowsAppExecutionAlias( + string? candidate, + out string resolvedPath + ) + { + resolvedPath = string.Empty; + if (string.IsNullOrWhiteSpace(candidate)) + return false; + + var trimmed = candidate.Trim(); + if (trimmed.Length == 0) + return false; + + if ( + trimmed.IndexOf('/') >= 0 + || trimmed.IndexOf('\\') >= 0 + || !trimmed.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + ) + { + return false; + } + + foreach (var directory in EnumerateWindowsAppAliasDirectories()) + { + try + { + var path = Path.Combine(directory, trimmed); + if (File.Exists(path)) + { + resolvedPath = path; + return true; + } + } + catch + { + // Ignore issues accessing alias directories + } + } + + return false; + } + + private static IEnumerable EnumerateWindowsAppAliasDirectories() + { + var overrideValue = Environment.GetEnvironmentVariable(WindowsAppAliasDirectoriesEnvVar); + var reported = new HashSet(StringComparer.OrdinalIgnoreCase); + + void Add(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + try + { + var full = Path.GetFullPath(path); + if (!Directory.Exists(full)) + return; + full = full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (reported.Add(full)) + { + // nothing else to do + } + } + catch + { + // Ignore invalid directories + } + } + + if (!string.IsNullOrWhiteSpace(overrideValue)) + { + var splits = overrideValue.Split( + Path.PathSeparator, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + foreach (var part in splits) + { + Add(part); + } + } + else if (OperatingSystem.IsWindows()) + { + var localAppData = Environment.GetFolderPath( + Environment.SpecialFolder.LocalApplicationData + ); + if (!string.IsNullOrWhiteSpace(localAppData)) + { + Add(Path.Combine(localAppData, "Microsoft", "WindowsApps")); + } + } + + foreach (var dir in reported) + { + yield return dir; + } + } + + private static bool IsCommandPathLike(string command) + { + if (string.IsNullOrWhiteSpace(command)) + return false; + var trimmed = command.Trim(); + return trimmed.Contains('/') + || trimmed.Contains('\\') + || trimmed.StartsWith("${__dirname}", StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith("./") + || trimmed.StartsWith("..") + || trimmed.EndsWith(".js", StringComparison.OrdinalIgnoreCase) + || trimmed.EndsWith(".py", StringComparison.OrdinalIgnoreCase) + || trimmed.EndsWith(".exe", StringComparison.OrdinalIgnoreCase); + } + internal static List ValidateLocalizationCompleteness( McpbManifest manifest, string baseDir, @@ -595,6 +725,7 @@ internal static async Task DiscoverCapabilitiesAsync( var providedUserConfig = userConfigOverrides ?? EmptyUserConfigOverrides; EnsureRequiredUserConfigProvided(manifest, command, rawArgs, providedUserConfig); + var originalCommand = command; command = ExpandToken(command, dir, providedUserConfig); var args = new List(); foreach (var rawArg in rawArgs) @@ -611,6 +742,33 @@ internal static async Task DiscoverCapabilitiesAsync( for (int i = 0; i < args.Count; i++) args[i] = NormalizePathForPlatform(args[i]); + if (IsCommandPathLike(originalCommand)) + { + var resolved = command; + if (!Path.IsPathRooted(resolved)) + { + resolved = Path.Combine(dir, resolved); + } + + if (File.Exists(resolved)) + { + command = resolved; + } + else if (TryResolveWindowsAppExecutionAlias(originalCommand, out var aliasPath)) + { + logInfo?.Invoke( + $"Resolved server command '{originalCommand}' via Windows app execution alias: {aliasPath}" + ); + command = aliasPath; + } + else + { + throw new InvalidOperationException( + $"Unable to locate server.mcp_config.command executable: {originalCommand}" + ); + } + } + Dictionary? env = null; if (cfg.Env != null && cfg.Env.Count > 0) { diff --git a/dotnet/mcpb/mcpb.csproj b/dotnet/mcpb/mcpb.csproj index 656d4dc..91cd4a8 100644 --- a/dotnet/mcpb/mcpb.csproj +++ b/dotnet/mcpb/mcpb.csproj @@ -9,7 +9,7 @@ true mcpb Mcpb.Cli - 0.3.4 + 0.3.5 Alexander Sklar CLI tool for building MCP Bundles (.mcpb) MCP;MCPB;CLI;bundles;DXT;ModelContextProtocol From 433d06c9b279c9faaf943d7bdecda43115b1e6f4 Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Fri, 5 Dec 2025 17:42:31 -0800 Subject: [PATCH 2/2] fix: update link for App Execution Aliases documentation --- dotnet/CLI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/CLI.md b/dotnet/CLI.md index 85645d4..ff03fdc 100644 --- a/dotnet/CLI.md +++ b/dotnet/CLI.md @@ -166,7 +166,7 @@ Before launching the server or writing the archive, `mcpb pack` now validates th If any of these files are missing, packing fails immediately with an error like `Missing icon file: icon.png`. This happens before dynamic capability discovery so you get fast feedback on manifest inaccuracies. -On Windows, `.exe` entry points or path-like commands can be satisfied by [App Execution Aliases](https://learn.microsoft.com/windows/uwp/launch-resume/run-desktop-applications-as-uwp-apps). When a referenced `.exe` is not present under your extension directory, the CLI automatically checks `%LOCALAPPDATA%\Microsoft\WindowsApps` (the folder where aliases surface). To point discovery/validation at custom alias locations—or to simulate aliases in CI—set `MCPB_WINDOWS_APP_ALIAS_DIRS` to a path-separated list of directories. +On Windows, `.exe` entry points or path-like commands can be satisfied by [App Execution Aliases](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions). When a referenced `.exe` is not present under your extension directory, the CLI automatically checks `%LOCALAPPDATA%\Microsoft\WindowsApps` (the folder where aliases surface). To point discovery/validation at custom alias locations—or to simulate aliases in CI—set `MCPB_WINDOWS_APP_ALIAS_DIRS` to a path-separated list of directories. When discovery launches your server it resolves the executable using the same logic, so an alias that passes validation is the exact binary that will be executed.