Skip to content

Commit b69dcab

Browse files
authored
Merge pull request #7 from asklar/temp-stash
app exec alias support
2 parents 37cc371 + 433d06c commit b69dcab

File tree

4 files changed

+340
-47
lines changed

4 files changed

+340
-47
lines changed

dotnet/CLI.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ Before launching the server or writing the archive, `mcpb pack` now validates th
166166

167167
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.
168168

169+
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.
170+
171+
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.
172+
169173
Commands (e.g. `node`, `python`) that are not path-like are not validated—they are treated as executables resolved by the environment.
170174

171175
Examples:

dotnet/mcpb.Tests/CliPackFileValidationTests.cs

Lines changed: 162 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
using System.Text.Json;
2-
using Xunit;
1+
using System;
32
using System.IO;
3+
using System.Text.Json;
44
using Mcpb.Json;
5+
using Xunit;
56

67
namespace Mcpb.Tests;
78

89
public class CliPackFileValidationTests
910
{
1011
private string CreateTempDir()
1112
{
12-
var dir = Path.Combine(Path.GetTempPath(), "mcpb_cli_pack_files_" + Guid.NewGuid().ToString("N"));
13+
var dir = Path.Combine(
14+
Path.GetTempPath(),
15+
"mcpb_cli_pack_files_" + Guid.NewGuid().ToString("N")
16+
);
1317
Directory.CreateDirectory(dir);
1418
Directory.CreateDirectory(Path.Combine(dir, "server"));
1519
return dir;
1620
}
17-
private (int exitCode, string stdout, string stderr) InvokeCli(string workingDir, params string[] args)
21+
22+
private (int exitCode, string stdout, string stderr) InvokeCli(
23+
string workingDir,
24+
params string[] args
25+
)
1826
{
1927
var root = Mcpb.Commands.CliRoot.Build();
2028
var prev = Directory.GetCurrentDirectory();
@@ -26,23 +34,31 @@ private string CreateTempDir()
2634
var code = CommandRunner.Invoke(root, args, swOut, swErr);
2735
return (code, swOut.ToString(), swErr.ToString());
2836
}
29-
finally { Directory.SetCurrentDirectory(prev); }
37+
finally
38+
{
39+
Directory.SetCurrentDirectory(prev);
40+
}
3041
}
3142

32-
private Mcpb.Core.McpbManifest BaseManifest() => new Mcpb.Core.McpbManifest
33-
{
34-
Name = "demo",
35-
Description = "desc",
36-
Author = new Mcpb.Core.McpbManifestAuthor { Name = "A" },
37-
Icon = "icon.png",
38-
Screenshots = new List<string> { "shots/s1.png" },
39-
Server = new Mcpb.Core.McpbManifestServer
43+
private Mcpb.Core.McpbManifest BaseManifest() =>
44+
new Mcpb.Core.McpbManifest
4045
{
41-
Type = "node",
42-
EntryPoint = "server/index.js",
43-
McpConfig = new Mcpb.Core.McpServerConfigWithOverrides { Command = "node", Args = new List<string> { "${__dirname}/server/index.js" } }
44-
}
45-
};
46+
Name = "demo",
47+
Description = "desc",
48+
Author = new Mcpb.Core.McpbManifestAuthor { Name = "A" },
49+
Icon = "icon.png",
50+
Screenshots = new List<string> { "shots/s1.png" },
51+
Server = new Mcpb.Core.McpbManifestServer
52+
{
53+
Type = "node",
54+
EntryPoint = "server/index.js",
55+
McpConfig = new Mcpb.Core.McpServerConfigWithOverrides
56+
{
57+
Command = "node",
58+
Args = new List<string> { "${__dirname}/server/index.js" },
59+
},
60+
},
61+
};
4662

4763
[Fact]
4864
public void Pack_MissingIcon_Fails()
@@ -52,7 +68,10 @@ public void Pack_MissingIcon_Fails()
5268
Directory.CreateDirectory(Path.Combine(dir, "shots"));
5369
File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake");
5470
var manifest = BaseManifest();
55-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
71+
File.WriteAllText(
72+
Path.Combine(dir, "manifest.json"),
73+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
74+
);
5675
var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
5776
Assert.NotEqual(0, code);
5877
Assert.Contains("Missing icon file", stderr);
@@ -66,7 +85,10 @@ public void Pack_MissingEntryPoint_Fails()
6685
Directory.CreateDirectory(Path.Combine(dir, "shots"));
6786
File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake");
6887
var manifest = BaseManifest();
69-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
88+
File.WriteAllText(
89+
Path.Combine(dir, "manifest.json"),
90+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
91+
);
7092
var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
7193
Assert.NotEqual(0, code);
7294
Assert.Contains("Missing entry_point file", stderr);
@@ -79,7 +101,10 @@ public void Pack_MissingScreenshot_Fails()
79101
File.WriteAllText(Path.Combine(dir, "icon.png"), "fake");
80102
File.WriteAllText(Path.Combine(dir, "server", "index.js"), "// js");
81103
var manifest = BaseManifest();
82-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
104+
File.WriteAllText(
105+
Path.Combine(dir, "manifest.json"),
106+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
107+
);
83108
var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
84109
Assert.NotEqual(0, code);
85110
Assert.Contains("Missing screenshot file", stderr);
@@ -96,12 +121,103 @@ public void Pack_PathLikeCommandMissing_Fails()
96121
var manifest = BaseManifest();
97122
// Make command path-like to trigger validation
98123
manifest.Server.McpConfig.Command = "${__dirname}/server/missing.js";
99-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
124+
File.WriteAllText(
125+
Path.Combine(dir, "manifest.json"),
126+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
127+
);
100128
var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
101129
Assert.NotEqual(0, code);
102130
Assert.Contains("Missing server.command file", stderr);
103131
}
104132

133+
[Fact]
134+
public void Pack_CommandWindowsAlias_Succeeds()
135+
{
136+
var aliasDir = Path.Combine(
137+
Path.GetTempPath(),
138+
"mcpb_windows_alias_" + Guid.NewGuid().ToString("N")
139+
);
140+
Directory.CreateDirectory(aliasDir);
141+
var aliasName = "alias-command.exe";
142+
File.WriteAllText(Path.Combine(aliasDir, aliasName), "alias");
143+
var previousAliases = Environment.GetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS");
144+
Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", aliasDir);
145+
try
146+
{
147+
var dir = CreateTempDir();
148+
File.WriteAllText(Path.Combine(dir, "icon.png"), "fake");
149+
File.WriteAllText(Path.Combine(dir, "server", "index.js"), "// js");
150+
Directory.CreateDirectory(Path.Combine(dir, "shots"));
151+
File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake");
152+
var manifest = BaseManifest();
153+
manifest.Server.McpConfig.Command = aliasName;
154+
File.WriteAllText(
155+
Path.Combine(dir, "manifest.json"),
156+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
157+
);
158+
var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
159+
Assert.Equal(0, code);
160+
Assert.Contains("demo@", stdout);
161+
Assert.DoesNotContain("Missing server.command", stderr);
162+
}
163+
finally
164+
{
165+
Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", previousAliases);
166+
try
167+
{
168+
Directory.Delete(aliasDir, true);
169+
}
170+
catch
171+
{
172+
// Ignore cleanup failures in tests
173+
}
174+
}
175+
}
176+
177+
[Fact]
178+
public void Pack_EntryPointWindowsAlias_Succeeds()
179+
{
180+
var aliasDir = Path.Combine(
181+
Path.GetTempPath(),
182+
"mcpb_windows_alias_" + Guid.NewGuid().ToString("N")
183+
);
184+
Directory.CreateDirectory(aliasDir);
185+
var aliasName = "alias-entry.exe";
186+
File.WriteAllText(Path.Combine(aliasDir, aliasName), "alias");
187+
var previousAliases = Environment.GetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS");
188+
Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", aliasDir);
189+
try
190+
{
191+
var dir = CreateTempDir();
192+
File.WriteAllText(Path.Combine(dir, "icon.png"), "fake");
193+
File.WriteAllText(Path.Combine(dir, "server", "index.js"), "// js");
194+
Directory.CreateDirectory(Path.Combine(dir, "shots"));
195+
File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake");
196+
var manifest = BaseManifest();
197+
manifest.Server.EntryPoint = aliasName;
198+
File.WriteAllText(
199+
Path.Combine(dir, "manifest.json"),
200+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
201+
);
202+
var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
203+
Assert.Equal(0, code);
204+
Assert.Contains("demo@", stdout);
205+
Assert.DoesNotContain("Missing entry_point", stderr);
206+
}
207+
finally
208+
{
209+
Environment.SetEnvironmentVariable("MCPB_WINDOWS_APP_ALIAS_DIRS", previousAliases);
210+
try
211+
{
212+
Directory.Delete(aliasDir, true);
213+
}
214+
catch
215+
{
216+
// Ignore cleanup failures in tests
217+
}
218+
}
219+
}
220+
105221
[Fact]
106222
public void Pack_AllFilesPresent_Succeeds()
107223
{
@@ -112,7 +228,10 @@ public void Pack_AllFilesPresent_Succeeds()
112228
File.WriteAllText(Path.Combine(dir, "shots", "s1.png"), "fake");
113229
var manifest = BaseManifest();
114230
// Ensure command not path-like (node) so validation doesn't require it to exist as file
115-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
231+
File.WriteAllText(
232+
Path.Combine(dir, "manifest.json"),
233+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
234+
);
116235
var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
117236
Assert.Equal(0, code);
118237
Assert.Contains("demo@", stdout);
@@ -129,9 +248,12 @@ public void Pack_MissingIconsFile_Fails()
129248
manifest.ManifestVersion = "0.3";
130249
manifest.Icons = new List<Mcpb.Core.McpbManifestIcon>
131250
{
132-
new() { Src = "icon-16.png", Size = "16x16" }
251+
new() { Src = "icon-16.png", Size = "16x16" },
133252
};
134-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
253+
File.WriteAllText(
254+
Path.Combine(dir, "manifest.json"),
255+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
256+
);
135257
var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
136258
Assert.NotEqual(0, code);
137259
Assert.Contains("Missing icons[0] file", stderr);
@@ -149,9 +271,12 @@ public void Pack_IconsFilePresent_Succeeds()
149271
manifest.Screenshots = null; // Remove screenshots requirement for this test
150272
manifest.Icons = new List<Mcpb.Core.McpbManifestIcon>
151273
{
152-
new() { Src = "icon-16.png", Size = "16x16" }
274+
new() { Src = "icon-16.png", Size = "16x16" },
153275
};
154-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
276+
File.WriteAllText(
277+
Path.Combine(dir, "manifest.json"),
278+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
279+
);
155280
var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
156281
Assert.True(code == 0, $"Pack failed with code {code}. Stderr: {stderr}");
157282
Assert.Contains("demo@", stdout);
@@ -168,9 +293,12 @@ public void Pack_MissingLocalizationResources_Fails()
168293
manifest.Localization = new Mcpb.Core.McpbManifestLocalization
169294
{
170295
Resources = "locales/${locale}/messages.json",
171-
DefaultLocale = "en-US"
296+
DefaultLocale = "en-US",
172297
};
173-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
298+
File.WriteAllText(
299+
Path.Combine(dir, "manifest.json"),
300+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
301+
);
174302
var (code, _, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
175303
Assert.NotEqual(0, code);
176304
Assert.Contains("Missing localization resources", stderr);
@@ -190,9 +318,12 @@ public void Pack_LocalizationResourcesPresent_Succeeds()
190318
manifest.Localization = new Mcpb.Core.McpbManifestLocalization
191319
{
192320
Resources = "locales/${locale}/messages.json",
193-
DefaultLocale = "en-US"
321+
DefaultLocale = "en-US",
194322
};
195-
File.WriteAllText(Path.Combine(dir, "manifest.json"), JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions));
323+
File.WriteAllText(
324+
Path.Combine(dir, "manifest.json"),
325+
JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)
326+
);
196327
var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--no-discover");
197328
Assert.True(code == 0, $"Pack failed with code {code}. Stderr: {stderr}");
198329
Assert.Contains("demo@", stdout);

0 commit comments

Comments
 (0)