diff --git a/plugins/canvas-apps/.claude-plugin/plugin.json b/plugins/canvas-apps/.claude-plugin/plugin.json index 1e6e730e2..093778d61 100644 --- a/plugins/canvas-apps/.claude-plugin/plugin.json +++ b/plugins/canvas-apps/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "canvas-apps", - "version": "3.0.3", + "version": "3.0.4", "description": "Build Power Apps Canvas Apps using the Canvas Authoring MCP server.", "author": { "name": "Microsoft", diff --git a/plugins/canvas-apps/.plugin/plugin.json b/plugins/canvas-apps/.plugin/plugin.json index 1e6e730e2..093778d61 100644 --- a/plugins/canvas-apps/.plugin/plugin.json +++ b/plugins/canvas-apps/.plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "canvas-apps", - "version": "3.0.3", + "version": "3.0.4", "description": "Build Power Apps Canvas Apps using the Canvas Authoring MCP server.", "author": { "name": "Microsoft", diff --git a/plugins/canvas-apps/AGENTS.md b/plugins/canvas-apps/AGENTS.md index db8309cf1..d23c338f7 100644 --- a/plugins/canvas-apps/AGENTS.md +++ b/plugins/canvas-apps/AGENTS.md @@ -28,6 +28,8 @@ hooks.json ← Copilot-format hooks: userPromptTransformed hooks/ hooks.json ← Claude-format hooks: UserPromptSubmit inject-sync-reminder.cs ← File-based .NET app that emits the sync reminder for both hosts +scripts/ + check-version.cs ← .NET 10 file-based plugin update check references/ YamlSyntax.md ← .pa.yaml structure, syntax rules, and parse-error triage ControlGuide.md ← Control selection, property contracts, enums, and versions @@ -115,3 +117,14 @@ source, which also disables MCP. Before the MCP server will start, you need: **.NET 10 SDK** — [Download from Microsoft](https://dotnet.microsoft.com/download/dotnet/10.0) + +## Plugin Version Check + +`configure-canvas-mcp` runs the .NET 10 file-based app at +`scripts/check-version.cs` before starting its workflow. The checker compares +the bundled `.plugin/plugin.json` version with the canonical manifest on +`main`, prints update commands only when a newer version exists, and exits +silently for current versions or expected network, filesystem, and manifest +errors so update discovery never blocks MCP configuration. + +Keep the check immediately after the skill frontmatter. diff --git a/plugins/canvas-apps/scripts/check-version.cs b/plugins/canvas-apps/scripts/check-version.cs new file mode 100644 index 000000000..d44b8370d --- /dev/null +++ b/plugins/canvas-apps/scripts/check-version.cs @@ -0,0 +1,164 @@ +using System.Text.Json; + +internal static class Program +{ + private const string MarketplaceName = "power-platform-skills"; + private const string DefaultManifestUrl = + "https://raw.githubusercontent.com/microsoft/power-platform-skills/main/plugins/canvas-apps/.plugin/plugin.json"; + + private static async Task Main(string[] args) + { + try + { + return await CheckVersion(args); + } + catch (Exception) + { + // Version discovery is advisory and must never block MCP configuration. + return 0; + } + } + + private static async Task CheckVersion(string[] args) + { + string pluginRoot = GetRequiredOption(args, "--plugin-root"); + string manifestUrl = GetOption(args, "--manifest-url") ?? DefaultManifestUrl; + PluginManifest local = ReadManifest( + Path.Combine(pluginRoot, ".plugin", "plugin.json") + ); + + using var httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(5), + }; + string remoteJson = await httpClient.GetStringAsync(manifestUrl); + PluginManifest remote = ParseManifest(remoteJson); + + if (CompareVersions(local.Version, remote.Version) >= 0) + { + return 0; + } + + Console.WriteLine( + $"Plugin update available: {local.Name} {local.Version} -> {remote.Version}." + ); + WriteUpdateCommands(local.Name); + + return 0; + } + + private static void WriteUpdateCommands(string pluginName) + { + string? cli = DetectPluginCli(); + if (cli is not null) + { + Console.WriteLine("Run:"); + WriteUpdateCommands(cli, pluginName, " "); + return; + } + + Console.WriteLine("Run the commands for your CLI:"); + Console.WriteLine(" Claude Code:"); + WriteUpdateCommands("claude", pluginName, " "); + Console.WriteLine(" GitHub Copilot CLI:"); + WriteUpdateCommands("copilot", pluginName, " "); + } + + private static void WriteUpdateCommands(string cli, string pluginName, string indent) + { + Console.WriteLine($"{indent}{cli} plugin marketplace update {MarketplaceName}"); + Console.WriteLine( + $"{indent}{cli} plugin update {pluginName}@{MarketplaceName}" + ); + } + + private static string? DetectPluginCli() + { + // Match the host detection contract used by shared telemetry. Claude + // wins if both markers are present, which avoids ambiguous instructions. + if (IsTruthyEnvironmentVariable("CLAUDECODE")) + { + return "claude"; + } + + return IsTruthyEnvironmentVariable("COPILOT_CLI") ? "copilot" : null; + } + + private static bool IsTruthyEnvironmentVariable(string name) + { + string? value = Environment.GetEnvironmentVariable(name); + if (value is null) + { + return false; + } + + string normalized = value.Trim().ToLowerInvariant(); + return normalized is not ("" or "0" or "false"); + } + + private static PluginManifest ReadManifest(string path) => + ParseManifest(File.ReadAllText(path)); + + private static PluginManifest ParseManifest(string json) + { + using JsonDocument document = JsonDocument.Parse(json); + JsonElement root = document.RootElement; + return new PluginManifest( + root.GetProperty("name").GetString() + ?? throw new InvalidDataException("Plugin name is missing."), + root.GetProperty("version").GetString() + ?? throw new InvalidDataException("Plugin version is missing.") + ); + } + + private static string GetRequiredOption(string[] args, string option) => + GetOption(args, option) + ?? throw new ArgumentException($"Missing required option: {option}"); + + private static string? GetOption(string[] args, string option) + { + for (int index = 0; index < args.Length - 1; index++) + { + if (args[index] == option) + { + return args[index + 1]; + } + } + + return null; + } + + private static int CompareVersions(string left, string right) + { + string[] leftSegments = left.Split('.'); + string[] rightSegments = right.Split('.'); + int segmentCount = Math.Max(leftSegments.Length, rightSegments.Length); + + for (int index = 0; index < segmentCount; index++) + { + int leftSegment = ParseSegment(leftSegments, index, left); + int rightSegment = ParseSegment(rightSegments, index, right); + int comparison = leftSegment.CompareTo(rightSegment); + if (comparison != 0) + { + return comparison; + } + } + + return 0; + } + + private static int ParseSegment(string[] segments, int index, string version) + { + if (index >= segments.Length) + { + return 0; + } + + return int.TryParse(segments[index], out int value) + ? value + : throw new FormatException($"Invalid plugin version: {version}"); + } + + private sealed record PluginManifest(string Name, string Version); +} diff --git a/plugins/canvas-apps/skills/configure-canvas-mcp/SKILL.md b/plugins/canvas-apps/skills/configure-canvas-mcp/SKILL.md index b601ac742..a3993098d 100644 --- a/plugins/canvas-apps/skills/configure-canvas-mcp/SKILL.md +++ b/plugins/canvas-apps/skills/configure-canvas-mcp/SKILL.md @@ -1,12 +1,14 @@ --- name: configure-canvas-mcp -version: 2.1.0 +version: 2.1.1 description: Configure the Canvas Authoring MCP server for the current coauthoring session. USE WHEN "configure MCP", "set up MCP server", "MCP not working", "connect Canvas Apps MCP", "canvas-authoring not available", "MCP not configured", "set up canvas apps". author: Microsoft Corporation user-invocable: true allowed-tools: Bash, AskUserQuestion, mcp__canvas-authoring__connect --- +> **Plugin check**: Run `dotnet run --file "${PLUGIN_ROOT}/scripts/check-version.cs" --verbosity quiet -- --plugin-root "${PLUGIN_ROOT}"` — if it outputs a message, show it to the user before proceeding. + # Configure the Canvas Authoring MCP Server This skill configures the Canvas Authoring MCP server for the user's current Power Apps coauthoring session. The MCP server is auto-registered by the plugin — this skill connects it to a specific app session.