Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/canvas-apps/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/canvas-apps/.plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
13 changes: 13 additions & 0 deletions plugins/canvas-apps/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
164 changes: 164 additions & 0 deletions plugins/canvas-apps/scripts/check-version.cs
Original file line number Diff line number Diff line change
@@ -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<int> 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<int> 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}"
);
}
Comment on lines +67 to +73

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);
}
4 changes: 3 additions & 1 deletion plugins/canvas-apps/skills/configure-canvas-mcp/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading