diff --git a/plugins/canvas-apps/.claude-plugin/plugin.json b/plugins/canvas-apps/.claude-plugin/plugin.json index 42cb07243..488d411be 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": "2.2.1", + "version": "2.2.2", "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 42cb07243..488d411be 100644 --- a/plugins/canvas-apps/.plugin/plugin.json +++ b/plugins/canvas-apps/.plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "canvas-apps", - "version": "2.2.1", + "version": "2.2.2", "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 3494b7e70..6e1be3560 100644 --- a/plugins/canvas-apps/AGENTS.md +++ b/plugins/canvas-apps/AGENTS.md @@ -23,6 +23,8 @@ claude --plugin-dir /path/to/plugins/canvas-apps .mcp.json ← MCP server config (canvas-authoring, auto-registered) AGENTS.md ← Plugin guidance for AI agents (this file) CLAUDE.md ← Symlink → AGENTS.md +scripts/ + check-version.cs ← .NET 10 file-based plugin update check references/ TechnicalGuide.md ← YAML syntax, control selection, layout strategies, Power Fx patterns DesignGuide.md ← Aesthetic guidelines, anti-patterns, design process @@ -80,3 +82,14 @@ The `canvas-authoring` MCP server exposes the following tools: 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 6b17c4462..61182f964 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.