diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 404fee99df..a7d68f0523 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -54,7 +54,6 @@ "PowerToys.AwakeModuleInterface.dll", "PowerToys.Awake.exe", "PowerToys.Awake.dll", - "PowerToys.McpServer.exe", "PowerToys.FancyZonesEditor.exe", "PowerToys.FancyZonesEditor.dll", @@ -238,7 +237,9 @@ "PowerToys.DSC.dll", "PowerToys.DSC.exe", - "PowerToysSparse.msix" + "PowerToysSparse.msix", + "PowerToys.McpServer.dll", + "PowerToys.McpServer.exe" ], "SigningInfo": { "Operations": [ diff --git a/.vscode/mcp.json b/.vscode/mcp.json deleted file mode 100644 index b51b87eef0..0000000000 --- a/.vscode/mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "servers": { - "powertoys-mcp": { - "command": "C:/PowerToys/x64/Release/PowerToys.McpServer.exe", - "args": [], - "env": { - "NODE_ENV": "production" - } - } - } -} \ No newline at end of file diff --git a/PowerToys.sln b/PowerToys.sln index 881e4ff36c..7e4a4ce341 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -330,11 +330,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AwakeModuleInterface", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Awake", "src\modules\awake\Awake\Awake.csproj", "{D940E07F-532C-4FF3-883F-790DA014F19A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mcp", "mcp", "{7A0ECF79-2E61-4EC8-9B79-3C438962C0F0}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "McpModuleInterface", "src\modules\mcp\McpModuleInterface\McpModuleInterface.vcxproj", "{8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.McpServer", "src\modules\mcp\McpServer\PowerToys.McpServer.csproj", "{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.McpServer", "src\McpServer\PowerToys.McpServer.csproj", "{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.csproj", "{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}" EndProject @@ -1452,14 +1448,6 @@ Global {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|ARM64.Build.0 = Release|ARM64 {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.ActiveCfg = Release|x64 {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.Build.0 = Release|x64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Debug|ARM64.Build.0 = Debug|ARM64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Debug|x64.ActiveCfg = Debug|x64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Debug|x64.Build.0 = Debug|x64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Release|ARM64.ActiveCfg = Release|ARM64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Release|ARM64.Build.0 = Release|ARM64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Release|x64.ActiveCfg = Release|x64 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}.Release|x64.Build.0 = Release|x64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|ARM64.Build.0 = Debug|ARM64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.ActiveCfg = Debug|x64 @@ -3367,9 +3355,7 @@ Global {F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94} {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {7A0ECF79-2E61-4EC8-9B79-3C438962C0F0} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A} = {7A0ECF79-2E61-4EC8-9B79-3C438962C0F0} - {8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D} = {7A0ECF79-2E61-4EC8-9B79-3C438962C0F0} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/doc/devdocs/modules/readme.md b/doc/devdocs/modules/readme.md index 79584de10b..0a2b48f098 100644 --- a/doc/devdocs/modules/readme.md +++ b/doc/devdocs/modules/readme.md @@ -9,7 +9,6 @@ This section contains documentation for individual PowerToys modules, including | [Advanced Paste](advancedpaste.md) | Tool for enhanced clipboard pasting with formatting options | | [Always on Top](alwaysontop.md) | Tool for pinning windows to stay on top of other windows | | [Awake](awake.md) | Tool to keep your computer awake without modifying power settings | -| [MCP Server](mcp.md) | Model Context Protocol bridge exposing PowerToys tools to AI agents | | [Color Picker](colorpicker.md) | Tool for selecting and managing colors from the screen | | [Command Not Found](commandnotfound.md) | Tool suggesting package installations for missing commands | | [Crop and Lock](cropandlock.md) | Tool for cropping application windows into smaller windows or thumbnails | diff --git a/src/modules/mcp/McpServer/PowerToys.McpServer.csproj b/src/McpServer/PowerToys.McpServer.csproj similarity index 69% rename from src/modules/mcp/McpServer/PowerToys.McpServer.csproj rename to src/McpServer/PowerToys.McpServer.csproj index 067e1b8873..ba37dadbe7 100644 --- a/src/modules/mcp/McpServer/PowerToys.McpServer.csproj +++ b/src/McpServer/PowerToys.McpServer.csproj @@ -1,13 +1,13 @@ - - + + Exe PowerToys.McpServer PowerToys.McpServer enable - ..\..\..\..\$(Platform)\$(Configuration) + ..\..\$(Platform)\$(Configuration) false false @@ -26,7 +26,7 @@ - - + + diff --git a/src/modules/mcp/McpServer/PowerToys.McpServer.dev.manifest b/src/McpServer/PowerToys.McpServer.dev.manifest similarity index 100% rename from src/modules/mcp/McpServer/PowerToys.McpServer.dev.manifest rename to src/McpServer/PowerToys.McpServer.dev.manifest diff --git a/src/modules/mcp/McpServer/PowerToys.McpServer.prod.manifest b/src/McpServer/PowerToys.McpServer.prod.manifest similarity index 100% rename from src/modules/mcp/McpServer/PowerToys.McpServer.prod.manifest rename to src/McpServer/PowerToys.McpServer.prod.manifest diff --git a/src/modules/mcp/McpServer/Program.cs b/src/McpServer/Program.cs similarity index 100% rename from src/modules/mcp/McpServer/Program.cs rename to src/McpServer/Program.cs diff --git a/src/modules/mcp/McpServer/README.md b/src/McpServer/README.md similarity index 100% rename from src/modules/mcp/McpServer/README.md rename to src/McpServer/README.md diff --git a/src/McpServer/Tools/AwakeTools.cs b/src/McpServer/Tools/AwakeTools.cs new file mode 100644 index 0000000000..0e1d32f4fa --- /dev/null +++ b/src/McpServer/Tools/AwakeTools.cs @@ -0,0 +1,649 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text.Json.Nodes; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using ModelContextProtocol.Server; +using Lock = System.Threading.Lock; + +namespace PowerToys.McpServer.Tools +{ + /// + /// MCP tools for PowerToys Awake module. + /// + [McpServerToolType] + public static class AwakeTools + { + private static readonly SettingsUtils SettingsUtils = new SettingsUtils(); + private const string PowerToysProcessName = "PowerToys"; + private const string AwakeExecutableName = "PowerToys.Awake.exe"; + private static readonly string[] AwakeRelativeSearchPaths = + [ + AwakeExecutableName, + Path.Combine("modules", "Awake", AwakeExecutableName), + ]; + + /// + /// Gets the current Awake mode and configuration. + /// + /// JSON object with current Awake status. + [McpServerTool] + [Description("Get the current Awake mode and configuration from the PowerToys settings store.")] + public static JsonObject GetAwakeStatus() + { + try + { + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + if (IsAwakeProcessRunning()) + { + string errorMessage = "Awake is already running via CLI. Use force=true to override."; + + return AwakeStatusPayload.CreateError( + errorMessage, + powerToysRunning: powerToysRunning, + launchedViaCli: true).ToJsonObject(); + } + + return AwakeStatusPayload.CreateInactive().ToJsonObject(); + } + + // PowerToys is running and Awake module is enabled + bool awakeProcessRunning = IsAwakeProcessRunning(); + + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + string summary = FormatAwakeDescription(settings); + + if (awakeProcessRunning) + { + summary = $"{summary} An Awake process is already running with the current configuration. To override the active session and apply new settings, use force=true."; + } + + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, summary); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = !awakeModuleEnabled && awakeProcessRunning; + payload.AwakeProcessActive = awakeProcessRunning; + Logger.LogInfo("[MCP] Retrieved Awake status via SDK tool."); + return payload.ToJsonObject(); + } + catch (Exception ex) + { + Logger.LogError("[MCP] Failed to read Awake status.", ex); + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); + } + } + + /// + /// Sets the Awake mode to passive (allow system sleep). + /// + /// JSON object with updated Awake status. + [McpServerTool] + [Description("Set Awake to passive mode (allow system to sleep normally).")] + public static JsonObject SetAwakePassive() + { + try + { + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + StopAwakeProcesses(); + Logger.LogInfo("[MCP] Stopped all Awake processes because PowerToys is not running."); + return AwakeStatusPayload.CreateInactive().ToJsonObject(); + } + + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + settings.Properties.Mode = AwakeMode.PASSIVE; + settings.Properties.ProcessId = 0; + settings.Properties.KeepDisplayOn = false; + settings.Properties.IntervalHours = 0; + settings.Properties.IntervalMinutes = 0; + SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName); + + string confirmation = FormatAwakeDescription(settings); + Logger.LogInfo($"[MCP] {confirmation}"); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = false; + payload.AwakeProcessActive = false; + return payload.ToJsonObject(); + } + catch (Exception ex) + { + Logger.LogError("[MCP] Failed to set Awake to passive.", ex); + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); + } + } + + /// + /// Sets the Awake mode to indefinite (keep system awake forever). + /// + /// Whether to keep the display on. Default is true. + /// JSON object with updated Awake status. + [McpServerTool] + [Description("Set Awake to indefinite mode (keep system awake until manually changed).")] + public static JsonObject SetAwakeIndefinite( + [Description("Whether to keep the display on")] bool keepDisplayOn = true, + [Description("Force the change even if Awake is already running")] bool force = false) + { + try + { + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + return HandleCliScenario(AwakeMode.INDEFINITE, keepDisplayOn, 0, force); + } + + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + if (!force && IsAwakeActive(settings)) + { + return BuildActiveProcessResponse(settings, true, false); + } + + settings.Properties.Mode = AwakeMode.INDEFINITE; + settings.Properties.ProcessId = 0; + settings.Properties.KeepDisplayOn = keepDisplayOn; + settings.Properties.IntervalHours = 0; + settings.Properties.IntervalMinutes = 0; + SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName); + + string confirmation = FormatAwakeDescription(settings); + Logger.LogInfo($"[MCP] {confirmation}"); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = false; + payload.AwakeProcessActive = true; + return payload.ToJsonObject(); + } + catch (Exception ex) + { + Logger.LogError("[MCP] Failed to set Awake to indefinite.", ex); + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); + } + } + + /// + /// Sets the Awake mode to timed (keep system awake for a specific duration). + /// + /// Duration in seconds (minimum 60). + /// Whether to keep the display on. Default is true. + /// JSON object with updated Awake status. + [McpServerTool] + [Description("Set Awake to timed mode (keep system awake for a specific duration).")] + public static JsonObject SetAwakeTimed( + [Description("Duration in seconds (minimum 60)")] int durationSeconds, + [Description("Whether to keep the display on")] bool keepDisplayOn = true, + [Description("Force the change even if Awake is already running")] bool force = false) + { + try + { + if (durationSeconds < 60) + { + durationSeconds = 60; + } + + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + return HandleCliScenario(AwakeMode.TIMED, keepDisplayOn, (uint)durationSeconds, force); + } + + TimeSpan timeSpan = TimeSpan.FromSeconds(durationSeconds); + uint hours = (uint)timeSpan.TotalHours; + uint minutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60); + if (hours == 0 && minutes == 0) + { + minutes = 1; + } + + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + if (!force && IsAwakeActive(settings)) + { + return BuildActiveProcessResponse(settings, true, false); + } + + settings.Properties.Mode = AwakeMode.TIMED; + settings.Properties.ProcessId = 0; + settings.Properties.KeepDisplayOn = keepDisplayOn; + settings.Properties.IntervalHours = hours; + settings.Properties.IntervalMinutes = minutes; + settings.Properties.ExpirationDateTime = DateTimeOffset.Now.Add(timeSpan); + SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName); + + string confirmation = FormatAwakeDescription(settings); + Logger.LogInfo($"[MCP] {confirmation}"); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = false; + payload.AwakeProcessActive = true; + return payload.ToJsonObject(); + } + catch (Exception ex) + { + Logger.LogError("[MCP] Failed to set Awake to timed mode.", ex); + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); + } + } + + private static string FormatAwakeDescription(AwakeSettings settings) + { + var mode = settings.Properties.Mode.ToString().ToLowerInvariant(); + var display = settings.Properties.KeepDisplayOn ? "display on" : "display off"; + + if (settings.Properties.ProcessId > 0) + { + return $"Awake mode: process-bound (PID={settings.Properties.ProcessId}), {display}"; + } + + return settings.Properties.Mode switch + { + AwakeMode.PASSIVE => "Awake mode: passive (system sleep allowed)", + AwakeMode.INDEFINITE => $"Awake mode: indefinite, {display}", + AwakeMode.TIMED => $"Awake mode: timed ({settings.Properties.IntervalHours}h {settings.Properties.IntervalMinutes}m), {display}", + AwakeMode.EXPIRABLE => $"Awake mode: expirable (until {settings.Properties.ExpirationDateTime:yyyy-MM-dd HH:mm}), {display}", + _ => $"Awake mode: {mode}", + }; + } + + private static JsonObject BuildActiveProcessResponse(AwakeSettings settings, bool powerToysRunning, bool launchedViaCli) + { + return AwakeStatusPayload.CreateError( + "Awake is already running. Use force=true to override.", + settings, + powerToysRunning, + launchedViaCli).ToJsonObject(); + } + + private static bool IsPowerToysRunning() + { + try + { + return Process.GetProcessesByName(PowerToysProcessName).Length > 0; + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Unable to determine PowerToys runner status: {ex.Message}"); + return true; + } + } + + /// + /// Gets whether the Awake module is enabled in PowerToys settings. + /// + /// True if Awake module is enabled, false otherwise + private static bool IsAwakeModuleEnabled() + { + try + { + var generalSettings = SettingsUtils.GetSettings(); + return generalSettings?.Enabled?.Awake == true; + } + catch + { + // If we can't read settings, assume disabled + return false; + } + } + + /// + /// Checks PowerToys and Awake module status. + /// + /// Tuple containing (powerToysRunning, awakeModuleEnabled) + private static (bool PowerToysRunning, bool AwakeModuleEnabled) CheckPowerToysAndAwakeStatus() + { + bool powerToysRunning = IsPowerToysRunning(); + bool awakeModuleEnabled = powerToysRunning && IsAwakeModuleEnabled(); + + return (powerToysRunning, awakeModuleEnabled); + } + + /// + /// Handles CLI scenario when PowerToys is not running or Awake module is disabled. + /// + /// The Awake mode to set + /// Whether to keep display on + /// Duration in seconds (0 for indefinite) + /// Whether to force override existing process + /// JSON response for CLI scenario + private static JsonObject HandleCliScenario(AwakeMode mode, bool keepDisplayOn, uint durationSeconds, bool force) + { + if (!force && IsAwakeProcessRunning()) + { + return AwakeStatusPayload.CreateError( + "Awake is already running and PowerToys is not active. Use force=true to override.", + powerToysRunning: false, + launchedViaCli: false).ToJsonObject(); + } + + if (IsAwakeProcessRunning()) + { + StopAwakeProcesses(); + } + + JsonObject cliPayload = StartAwakeCliProcess(mode, keepDisplayOn, durationSeconds); + return cliPayload; + } + + private static JsonObject StartAwakeCliProcess(AwakeMode mode, bool keepDisplayOn, uint durationSeconds) + { + try + { + if (!TryResolveAwakeExecutable(out string executablePath)) + { + throw new FileNotFoundException("PowerToys.Awake.exe was not found near the MCP server executable."); + } + + ProcessStartInfo startInfo = CreateSimpleStartInfo(executablePath, mode, keepDisplayOn, durationSeconds); + Process? launchedProcess = Process.Start(startInfo); + if (launchedProcess is null) + { + throw new InvalidOperationException("Failed to start PowerToys.Awake.exe."); + } + + // No tracking, just launch and forget + launchedProcess.Dispose(); + + AwakeSettings snapshot = BuildAwakeSnapshot(mode, keepDisplayOn, durationSeconds); + string confirmation = FormatAwakeDescription(snapshot); + Logger.LogInfo($"[MCP] Launched Awake CLI for mode {mode} (PowerToys not running)."); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(snapshot, confirmation); + payload.AwakeProcessActive = true; + payload.LaunchedViaCli = true; + payload.PowerToysRunning = false; + return payload.ToJsonObject(); + } + catch (Exception ex) + { + Logger.LogError("[MCP] Failed to start Awake CLI.", ex); + return AwakeStatusPayload.CreateError( + ex.Message, + powerToysRunning: false, + launchedViaCli: false).ToJsonObject(); + } + } + + private static ProcessStartInfo CreateSimpleStartInfo(string executablePath, AwakeMode mode, bool keepDisplayOn, uint durationSeconds) + { + string workingDirectory = Path.GetDirectoryName(executablePath) ?? AppDomain.CurrentDomain.BaseDirectory; + var startInfo = new ProcessStartInfo(executablePath) + { + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = string.IsNullOrEmpty(workingDirectory) ? AppDomain.CurrentDomain.BaseDirectory : workingDirectory, + }; + + startInfo.ArgumentList.Add("--display-on"); + startInfo.ArgumentList.Add(keepDisplayOn ? "true" : "false"); + + if (mode == AwakeMode.TIMED && durationSeconds > 0) + { + startInfo.ArgumentList.Add("--time-limit"); + startInfo.ArgumentList.Add(durationSeconds.ToString(CultureInfo.InvariantCulture)); + } + + return startInfo; + } + + private static void StopAwakeProcesses() + { + string processName = Path.GetFileNameWithoutExtension(AwakeExecutableName); + try + { + Process[] awakeProcesses = Process.GetProcessesByName(processName); + foreach (Process process in awakeProcesses) + { + try + { + if (!process.HasExited) + { + process.Kill(true); + process.WaitForExit(TimeSpan.FromSeconds(5)); + } + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Failed to terminate Awake process {process.Id}: {ex.Message}"); + } + finally + { + process.Dispose(); + } + } + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Failed to enumerate Awake processes: {ex.Message}"); + } + } + + private static AwakeSettings BuildAwakeSnapshot(AwakeMode mode, bool keepDisplayOn, uint durationSeconds) + { + var snapshot = new AwakeSettings(); + snapshot.Properties.Mode = mode; + snapshot.Properties.KeepDisplayOn = keepDisplayOn; + snapshot.Properties.ProcessId = 0; + + if (mode == AwakeMode.TIMED && durationSeconds > 0) + { + TimeSpan timeSpan = TimeSpan.FromSeconds(durationSeconds); + snapshot.Properties.IntervalHours = (uint)timeSpan.TotalHours; + snapshot.Properties.IntervalMinutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60); + snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now.AddSeconds(durationSeconds); + } + else + { + snapshot.Properties.IntervalHours = 0; + snapshot.Properties.IntervalMinutes = 0; + snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now; + } + + return snapshot; + } + + private static bool TryResolveAwakeExecutable(out string executablePath) + { + string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + if (TryResolveAwakeExecutableFrom(baseDirectory, out executablePath)) + { + return true; + } + + string? parentDirectory = Directory.GetParent(baseDirectory)?.FullName; + if (!string.IsNullOrEmpty(parentDirectory) && TryResolveAwakeExecutableFrom(parentDirectory, out executablePath)) + { + return true; + } + + executablePath = string.Empty; + return false; + } + + private static bool TryResolveAwakeExecutableFrom(string rootDirectory, out string executablePath) + { + foreach (string relativePath in AwakeRelativeSearchPaths) + { + string candidate = Path.Combine(rootDirectory, relativePath); + if (File.Exists(candidate)) + { + executablePath = candidate; + return true; + } + } + + executablePath = string.Empty; + return false; + } + + private static bool IsAwakeProcessRunning() + { + try + { + string processName = Path.GetFileNameWithoutExtension(AwakeExecutableName); + return Process.GetProcessesByName(processName).Length > 0; + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Unable to determine Awake process status: {ex.Message}"); + return false; + } + } + + private static bool IsAwakeActive(AwakeSettings settings) + { + // Only check if Awake module is enabled + return IsAwakeModuleEnabled(); + } + + private sealed class AwakeStatusPayload + { + internal string Mode { get; set; } = "unknown"; + + internal bool KeepDisplayOn { get; set; } + + internal bool IsProcessBound { get; set; } + + internal int ProcessId { get; set; } + + internal uint IntervalHours { get; set; } + + internal uint IntervalMinutes { get; set; } + + internal string ExpirationDateTime { get; set; } = string.Empty; + + internal string Summary { get; set; } = string.Empty; + + internal bool PowerToysRunning { get; set; } + + internal bool AwakeProcessActive { get; set; } + + internal bool LaunchedViaCli { get; set; } + + internal bool Success { get; set; } = true; + + internal string? ErrorMessage { get; set; } + + internal JsonObject ToJsonObject() + { + var result = new JsonObject + { + ["mode"] = Mode, + ["keepDisplayOn"] = KeepDisplayOn, + ["isProcessBound"] = IsProcessBound, + ["processId"] = ProcessId, + ["intervalHours"] = IntervalHours, + ["intervalMinutes"] = IntervalMinutes, + ["expirationDateTime"] = ExpirationDateTime, + ["summary"] = Summary, + ["powerToysRunning"] = PowerToysRunning, + ["awakeProcessActive"] = AwakeProcessActive, + ["launchedViaCli"] = LaunchedViaCli, + }; + + // Add error handling properties + if (!Success) + { + result["success"] = false; + if (!string.IsNullOrEmpty(ErrorMessage)) + { + result["error"] = ErrorMessage; + } + } + + return result; + } + + internal static AwakeStatusPayload FromSettings(AwakeSettings settings, string summary) + { + return new AwakeStatusPayload + { + Mode = settings.Properties.Mode.ToString().ToLowerInvariant(), + KeepDisplayOn = settings.Properties.KeepDisplayOn, + IsProcessBound = settings.Properties.ProcessId > 0, + ProcessId = (int)settings.Properties.ProcessId, + IntervalHours = settings.Properties.IntervalHours, + IntervalMinutes = settings.Properties.IntervalMinutes, + ExpirationDateTime = settings.Properties.ExpirationDateTime.ToString("O"), + Summary = summary, + }; + } + + internal static AwakeStatusPayload CreateInactive() + { + return new AwakeStatusPayload + { + Mode = "inactive", + KeepDisplayOn = false, + IsProcessBound = false, + ProcessId = 0, + IntervalHours = 0, + IntervalMinutes = 0, + ExpirationDateTime = string.Empty, + Summary = "PowerToys Awake is not running because PowerToys is not active.", + PowerToysRunning = false, + AwakeProcessActive = false, + LaunchedViaCli = false, + }; + } + + internal static AwakeStatusPayload CreateUnknownActive(bool powerToysRunning, bool launchedViaCli) + { + return new AwakeStatusPayload + { + Mode = "unknown", + KeepDisplayOn = false, + IsProcessBound = false, + ProcessId = 0, + IntervalHours = 0, + IntervalMinutes = 0, + ExpirationDateTime = string.Empty, + Summary = "An Awake process is currently running, but its configuration cannot be determined. To terminate the existing process and start with new settings, use force=true.", + PowerToysRunning = powerToysRunning, + AwakeProcessActive = true, + LaunchedViaCli = launchedViaCli, + }; + } + + internal static AwakeStatusPayload CreateError(string errorMessage, AwakeSettings? settings = null, bool powerToysRunning = false, bool launchedViaCli = false) + { + var payload = new AwakeStatusPayload + { + Success = false, + ErrorMessage = errorMessage, + PowerToysRunning = powerToysRunning, + LaunchedViaCli = launchedViaCli, + AwakeProcessActive = true, + }; + + if (settings != null) + { + payload.Mode = settings.Properties.Mode.ToString().ToLowerInvariant(); + payload.KeepDisplayOn = settings.Properties.KeepDisplayOn; + payload.IsProcessBound = settings.Properties.ProcessId > 0; + payload.ProcessId = (int)settings.Properties.ProcessId; + payload.IntervalHours = settings.Properties.IntervalHours; + payload.IntervalMinutes = settings.Properties.IntervalMinutes; + payload.ExpirationDateTime = settings.Properties.ExpirationDateTime.ToString("O"); + payload.Summary = "An Awake session is already active with the current settings. To override and change the configuration, use force=true."; + } + else + { + payload.Mode = "unknown"; + payload.Summary = "An Awake process is currently running. To terminate the existing process and start with new settings, use force=true."; + } + + return payload; + } + } + } +} diff --git a/src/modules/mcp/McpModuleInterface/McpConstants.h b/src/modules/mcp/McpModuleInterface/McpConstants.h deleted file mode 100644 index 3fec1635f9..0000000000 --- a/src/modules/mcp/McpModuleInterface/McpConstants.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace McpConstants -{ - inline const wchar_t* ModuleKey = L"MCP"; -} diff --git a/src/modules/mcp/McpModuleInterface/McpModuleInterface.rc b/src/modules/mcp/McpModuleInterface/McpModuleInterface.rc deleted file mode 100644 index 3f9bc0b70c..0000000000 --- a/src/modules/mcp/McpModuleInterface/McpModuleInterface.rc +++ /dev/null @@ -1,71 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// String Table -// - -STRINGTABLE -BEGIN - IDS_MCP_NAME "Model Context Protocol" -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/src/modules/mcp/McpModuleInterface/McpModuleInterface.vcxproj b/src/modules/mcp/McpModuleInterface/McpModuleInterface.vcxproj deleted file mode 100644 index f860148c73..0000000000 --- a/src/modules/mcp/McpModuleInterface/McpModuleInterface.vcxproj +++ /dev/null @@ -1,79 +0,0 @@ - - - - - 15.0 - {8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A} - Win32Proj - McpServer - McpModuleInterface - PowerToys.McpModuleInterface - v143 - - - - DynamicLibrary - - - - - - - - - - - - ..\..\..\..\$(Platform)\$(Configuration)\ - - - - EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) - - - $(OutDir)$(TargetName)$(TargetExt) - - - - - - - - - - - - Create - - - - - - {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} - - - {6955446d-23f7-4023-9bb3-8657f904af99} - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - diff --git a/src/modules/mcp/McpModuleInterface/McpModuleInterface.vcxproj.filters b/src/modules/mcp/McpModuleInterface/McpModuleInterface.vcxproj.filters deleted file mode 100644 index 6a0e29db45..0000000000 --- a/src/modules/mcp/McpModuleInterface/McpModuleInterface.vcxproj.filters +++ /dev/null @@ -1,50 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - - - Source Files - - - Source Files - - - Source Files - - - - - Resource Files - - - - - - diff --git a/src/modules/mcp/McpModuleInterface/dllmain.cpp b/src/modules/mcp/McpModuleInterface/dllmain.cpp deleted file mode 100644 index 1ee2cfb902..0000000000 --- a/src/modules/mcp/McpModuleInterface/dllmain.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "pch.h" -#include -#include -#include -#include "trace.h" -#include "resource.h" -#include "McpConstants.h" -#include -#include - -#include -#include -#include -#include - -#include - -BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) -{ - switch (ul_reason_for_call) - { - case DLL_PROCESS_ATTACH: - Trace::RegisterProvider(); - break; - case DLL_THREAD_ATTACH: - case DLL_THREAD_DETACH: - break; - case DLL_PROCESS_DETACH: - Trace::UnregisterProvider(); - break; - } - return TRUE; -} - -const static wchar_t* MODULE_NAME = L"Model Context Protocol"; -const static wchar_t* MODULE_DESC = L"Exposes PowerToys functionality via MCP protocol for AI assistants."; - -class McpServer : public PowertoyModuleIface -{ - std::wstring app_name; - std::wstring app_key; - -private: - bool m_enabled = false; - PROCESS_INFORMATION p_info = {}; - - bool is_process_running() - { - return WaitForSingleObject(p_info.hProcess, 0) == WAIT_TIMEOUT; - } - - void launch_process() - { - Logger::trace(L"Launching PowerToys MCP Server process"); - unsigned long powertoys_pid = GetCurrentProcessId(); - - std::wstring executable_args = L"--pid " + std::to_wstring(powertoys_pid); - std::wstring application_path = L"PowerToys.McpServer.exe"; - std::wstring full_command_path = application_path + L" " + executable_args.data(); - Logger::trace(L"PowerToys MCP Server launching with parameters: " + executable_args); - - STARTUPINFO info = { sizeof(info) }; - - if (!CreateProcess(application_path.c_str(), full_command_path.data(), NULL, NULL, true, NULL, NULL, NULL, &info, &p_info)) - { - DWORD error = GetLastError(); - std::wstring message = L"PowerToys MCP Server failed to start with error: "; - message += std::to_wstring(error); - Logger::error(message); - } - } - -public: - McpServer() - { - app_name = GET_RESOURCE_STRING(IDS_MCP_NAME); - app_key = McpConstants::ModuleKey; - std::filesystem::path logFilePath(PTSettingsHelper::get_module_save_folder_location(this->app_key)); - logFilePath.append(L"Logs"); - Logger::init("McpModuleInterface", logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location()); - Logger::info("McpServer module interface is constructing"); - }; - - // Return the configured status for the gpo policy for the module - virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override - { - return powertoys_gpo::gpo_rule_configured_not_configured; - } - - virtual void destroy() override - { - delete this; - } - - virtual const wchar_t* get_name() override - { - return MODULE_NAME; - } - - virtual bool get_config(wchar_t* buffer, int* buffer_size) override - { - HINSTANCE hinstance = reinterpret_cast(&__ImageBase); - - PowerToysSettings::Settings settings(hinstance, get_name()); - settings.set_description(MODULE_DESC); - - return settings.serialize_to_buffer(buffer, buffer_size); - } - - virtual const wchar_t* get_key() override - { - return app_key.c_str(); - } - - virtual void set_config(const wchar_t* config) override - { - try - { - // Parse the input JSON string. - PowerToysSettings::PowerToyValues values = - PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); - - // Persist the values. - values.save_to_settings_file(); - } - catch (std::exception&) - { - // Improper JSON. - } - } - - virtual void enable() - { - Trace::EnableMCP(true); - launch_process(); - m_enabled = true; - }; - - virtual void disable() - { - if (m_enabled) - { - Trace::EnableMCP(false); - Logger::trace(L"Disabling MCP Server..."); - - // Terminate the MCP server process - if (p_info.hProcess) - { - TerminateProcess(p_info.hProcess, 0); - CloseHandle(p_info.hProcess); - CloseHandle(p_info.hThread); - } - } - - m_enabled = false; - } - - virtual bool is_enabled() override - { - return m_enabled; - } -}; - -extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() -{ - return new McpServer(); -} diff --git a/src/modules/mcp/McpModuleInterface/packages.config b/src/modules/mcp/McpModuleInterface/packages.config deleted file mode 100644 index 8946351a67..0000000000 --- a/src/modules/mcp/McpModuleInterface/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/modules/mcp/McpModuleInterface/pch.cpp b/src/modules/mcp/McpModuleInterface/pch.cpp deleted file mode 100644 index 1d9f38c57d..0000000000 --- a/src/modules/mcp/McpModuleInterface/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "pch.h" diff --git a/src/modules/mcp/McpModuleInterface/pch.h b/src/modules/mcp/McpModuleInterface/pch.h deleted file mode 100644 index 14e722860e..0000000000 --- a/src/modules/mcp/McpModuleInterface/pch.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#define WIN32_LEAN_AND_MEAN -#include -#include diff --git a/src/modules/mcp/McpModuleInterface/resource.h b/src/modules/mcp/McpModuleInterface/resource.h deleted file mode 100644 index 4df4d15aa1..0000000000 --- a/src/modules/mcp/McpModuleInterface/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by McpModuleInterface.rc -// -#define IDS_MCP_NAME 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/src/modules/mcp/McpModuleInterface/trace.cpp b/src/modules/mcp/McpModuleInterface/trace.cpp deleted file mode 100644 index ee9a6a7b8a..0000000000 --- a/src/modules/mcp/McpModuleInterface/trace.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "pch.h" -#include "trace.h" -#include -#include - -TRACELOGGING_DEFINE_PROVIDER( - g_hProvider, - "Microsoft.PowerToys", - (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x3b, 0x77), - TraceLoggingOptionProjectTelemetry()); - -void Trace::RegisterProvider() noexcept -{ - TraceLoggingRegister(g_hProvider); -} - -void Trace::UnregisterProvider() noexcept -{ - TraceLoggingUnregister(g_hProvider); -} - -void Trace::EnableMCP(bool enabled) noexcept -{ - TraceLoggingWrite( - g_hProvider, - "MCP_EnableMCP", - ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), - TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), - TraceLoggingBoolean(enabled, "Enabled")); -} diff --git a/src/modules/mcp/McpModuleInterface/trace.h b/src/modules/mcp/McpModuleInterface/trace.h deleted file mode 100644 index b3ace77fd6..0000000000 --- a/src/modules/mcp/McpModuleInterface/trace.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -namespace Trace -{ - void RegisterProvider() noexcept; - void UnregisterProvider() noexcept; - void EnableMCP(bool enabled) noexcept; -} diff --git a/src/modules/mcp/McpServer/Tools/AwakeTools.cs b/src/modules/mcp/McpServer/Tools/AwakeTools.cs index 468c0f50d8..0e1d32f4fa 100644 --- a/src/modules/mcp/McpServer/Tools/AwakeTools.cs +++ b/src/modules/mcp/McpServer/Tools/AwakeTools.cs @@ -4,10 +4,14 @@ using System; using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; using System.Text.Json.Nodes; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using ModelContextProtocol.Server; +using Lock = System.Threading.Lock; namespace PowerToys.McpServer.Tools { @@ -18,27 +22,13 @@ namespace PowerToys.McpServer.Tools public static class AwakeTools { private static readonly SettingsUtils SettingsUtils = new SettingsUtils(); - - /// - /// Check if Awake module is enabled in MCP settings. - /// - private static void CheckModuleEnabled() - { - try - { - McpSettings mcpSettings = SettingsUtils.GetSettingsOrDefault(McpSettings.ModuleName); - bool isEnabled = mcpSettings.Properties.EnabledModules.TryGetValue("Awake", out bool enabled) ? enabled : true; - if (!isEnabled) - { - throw new InvalidOperationException("Awake module is disabled in MCP settings. Enable it in PowerToys Settings > MCP > Module Toggles."); - } - } - catch (Exception ex) when (ex is not InvalidOperationException) - { - // If we can't read MCP settings, assume enabled (backward compatibility) - Logger.LogWarning($"[MCP] Could not check MCP module status, assuming enabled: {ex.Message}"); - } - } + private const string PowerToysProcessName = "PowerToys"; + private const string AwakeExecutableName = "PowerToys.Awake.exe"; + private static readonly string[] AwakeRelativeSearchPaths = + [ + AwakeExecutableName, + Path.Combine("modules", "Awake", AwakeExecutableName), + ]; /// /// Gets the current Awake mode and configuration. @@ -50,21 +40,45 @@ namespace PowerToys.McpServer.Tools { try { - CheckModuleEnabled(); + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + if (IsAwakeProcessRunning()) + { + string errorMessage = "Awake is already running via CLI. Use force=true to override."; + + return AwakeStatusPayload.CreateError( + errorMessage, + powerToysRunning: powerToysRunning, + launchedViaCli: true).ToJsonObject(); + } + + return AwakeStatusPayload.CreateInactive().ToJsonObject(); + } + + // PowerToys is running and Awake module is enabled + bool awakeProcessRunning = IsAwakeProcessRunning(); + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); string summary = FormatAwakeDescription(settings); - JsonObject payload = FormatAwakeJson(settings, summary); + + if (awakeProcessRunning) + { + summary = $"{summary} An Awake process is already running with the current configuration. To override the active session and apply new settings, use force=true."; + } + + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, summary); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = !awakeModuleEnabled && awakeProcessRunning; + payload.AwakeProcessActive = awakeProcessRunning; Logger.LogInfo("[MCP] Retrieved Awake status via SDK tool."); - return payload; + return payload.ToJsonObject(); } catch (Exception ex) { Logger.LogError("[MCP] Failed to read Awake status.", ex); - return new JsonObject - { - ["error"] = ex.Message, - ["mode"] = "unknown", - }; + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); } } @@ -78,7 +92,15 @@ namespace PowerToys.McpServer.Tools { try { - CheckModuleEnabled(); + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + StopAwakeProcesses(); + Logger.LogInfo("[MCP] Stopped all Awake processes because PowerToys is not running."); + return AwakeStatusPayload.CreateInactive().ToJsonObject(); + } + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); settings.Properties.Mode = AwakeMode.PASSIVE; settings.Properties.ProcessId = 0; @@ -89,16 +111,16 @@ namespace PowerToys.McpServer.Tools string confirmation = FormatAwakeDescription(settings); Logger.LogInfo($"[MCP] {confirmation}"); - return FormatAwakeJson(settings, confirmation); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = false; + payload.AwakeProcessActive = false; + return payload.ToJsonObject(); } catch (Exception ex) { Logger.LogError("[MCP] Failed to set Awake to passive.", ex); - return new JsonObject - { - ["error"] = ex.Message, - ["success"] = false, - }; + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); } } @@ -110,12 +132,24 @@ namespace PowerToys.McpServer.Tools [McpServerTool] [Description("Set Awake to indefinite mode (keep system awake until manually changed).")] public static JsonObject SetAwakeIndefinite( - [Description("Whether to keep the display on")] bool keepDisplayOn = true) + [Description("Whether to keep the display on")] bool keepDisplayOn = true, + [Description("Force the change even if Awake is already running")] bool force = false) { try { - CheckModuleEnabled(); + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + return HandleCliScenario(AwakeMode.INDEFINITE, keepDisplayOn, 0, force); + } + AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + if (!force && IsAwakeActive(settings)) + { + return BuildActiveProcessResponse(settings, true, false); + } + settings.Properties.Mode = AwakeMode.INDEFINITE; settings.Properties.ProcessId = 0; settings.Properties.KeepDisplayOn = keepDisplayOn; @@ -125,16 +159,16 @@ namespace PowerToys.McpServer.Tools string confirmation = FormatAwakeDescription(settings); Logger.LogInfo($"[MCP] {confirmation}"); - return FormatAwakeJson(settings, confirmation); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = false; + payload.AwakeProcessActive = true; + return payload.ToJsonObject(); } catch (Exception ex) { Logger.LogError("[MCP] Failed to set Awake to indefinite.", ex); - return new JsonObject - { - ["error"] = ex.Message, - ["success"] = false, - }; + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); } } @@ -148,16 +182,23 @@ namespace PowerToys.McpServer.Tools [Description("Set Awake to timed mode (keep system awake for a specific duration).")] public static JsonObject SetAwakeTimed( [Description("Duration in seconds (minimum 60)")] int durationSeconds, - [Description("Whether to keep the display on")] bool keepDisplayOn = true) + [Description("Whether to keep the display on")] bool keepDisplayOn = true, + [Description("Force the change even if Awake is already running")] bool force = false) { try { - CheckModuleEnabled(); if (durationSeconds < 60) { durationSeconds = 60; } + (bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus(); + + if (!powerToysRunning || !awakeModuleEnabled) + { + return HandleCliScenario(AwakeMode.TIMED, keepDisplayOn, (uint)durationSeconds, force); + } + TimeSpan timeSpan = TimeSpan.FromSeconds(durationSeconds); uint hours = (uint)timeSpan.TotalHours; uint minutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60); @@ -167,6 +208,11 @@ namespace PowerToys.McpServer.Tools } AwakeSettings settings = SettingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + if (!force && IsAwakeActive(settings)) + { + return BuildActiveProcessResponse(settings, true, false); + } + settings.Properties.Mode = AwakeMode.TIMED; settings.Properties.ProcessId = 0; settings.Properties.KeepDisplayOn = keepDisplayOn; @@ -177,16 +223,16 @@ namespace PowerToys.McpServer.Tools string confirmation = FormatAwakeDescription(settings); Logger.LogInfo($"[MCP] {confirmation}"); - return FormatAwakeJson(settings, confirmation); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation); + payload.PowerToysRunning = true; + payload.LaunchedViaCli = false; + payload.AwakeProcessActive = true; + return payload.ToJsonObject(); } catch (Exception ex) { Logger.LogError("[MCP] Failed to set Awake to timed mode.", ex); - return new JsonObject - { - ["error"] = ex.Message, - ["success"] = false, - }; + return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject(); } } @@ -210,19 +256,394 @@ namespace PowerToys.McpServer.Tools }; } - private static JsonObject FormatAwakeJson(AwakeSettings settings, string summary) + private static JsonObject BuildActiveProcessResponse(AwakeSettings settings, bool powerToysRunning, bool launchedViaCli) { - return new JsonObject + return AwakeStatusPayload.CreateError( + "Awake is already running. Use force=true to override.", + settings, + powerToysRunning, + launchedViaCli).ToJsonObject(); + } + + private static bool IsPowerToysRunning() + { + try { - ["mode"] = settings.Properties.Mode.ToString().ToLowerInvariant(), - ["keepDisplayOn"] = settings.Properties.KeepDisplayOn, - ["isProcessBound"] = settings.Properties.ProcessId > 0, - ["processId"] = settings.Properties.ProcessId, - ["intervalHours"] = settings.Properties.IntervalHours, - ["intervalMinutes"] = settings.Properties.IntervalMinutes, - ["expirationDateTime"] = settings.Properties.ExpirationDateTime.ToString("O"), - ["summary"] = summary, + return Process.GetProcessesByName(PowerToysProcessName).Length > 0; + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Unable to determine PowerToys runner status: {ex.Message}"); + return true; + } + } + + /// + /// Gets whether the Awake module is enabled in PowerToys settings. + /// + /// True if Awake module is enabled, false otherwise + private static bool IsAwakeModuleEnabled() + { + try + { + var generalSettings = SettingsUtils.GetSettings(); + return generalSettings?.Enabled?.Awake == true; + } + catch + { + // If we can't read settings, assume disabled + return false; + } + } + + /// + /// Checks PowerToys and Awake module status. + /// + /// Tuple containing (powerToysRunning, awakeModuleEnabled) + private static (bool PowerToysRunning, bool AwakeModuleEnabled) CheckPowerToysAndAwakeStatus() + { + bool powerToysRunning = IsPowerToysRunning(); + bool awakeModuleEnabled = powerToysRunning && IsAwakeModuleEnabled(); + + return (powerToysRunning, awakeModuleEnabled); + } + + /// + /// Handles CLI scenario when PowerToys is not running or Awake module is disabled. + /// + /// The Awake mode to set + /// Whether to keep display on + /// Duration in seconds (0 for indefinite) + /// Whether to force override existing process + /// JSON response for CLI scenario + private static JsonObject HandleCliScenario(AwakeMode mode, bool keepDisplayOn, uint durationSeconds, bool force) + { + if (!force && IsAwakeProcessRunning()) + { + return AwakeStatusPayload.CreateError( + "Awake is already running and PowerToys is not active. Use force=true to override.", + powerToysRunning: false, + launchedViaCli: false).ToJsonObject(); + } + + if (IsAwakeProcessRunning()) + { + StopAwakeProcesses(); + } + + JsonObject cliPayload = StartAwakeCliProcess(mode, keepDisplayOn, durationSeconds); + return cliPayload; + } + + private static JsonObject StartAwakeCliProcess(AwakeMode mode, bool keepDisplayOn, uint durationSeconds) + { + try + { + if (!TryResolveAwakeExecutable(out string executablePath)) + { + throw new FileNotFoundException("PowerToys.Awake.exe was not found near the MCP server executable."); + } + + ProcessStartInfo startInfo = CreateSimpleStartInfo(executablePath, mode, keepDisplayOn, durationSeconds); + Process? launchedProcess = Process.Start(startInfo); + if (launchedProcess is null) + { + throw new InvalidOperationException("Failed to start PowerToys.Awake.exe."); + } + + // No tracking, just launch and forget + launchedProcess.Dispose(); + + AwakeSettings snapshot = BuildAwakeSnapshot(mode, keepDisplayOn, durationSeconds); + string confirmation = FormatAwakeDescription(snapshot); + Logger.LogInfo($"[MCP] Launched Awake CLI for mode {mode} (PowerToys not running)."); + AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(snapshot, confirmation); + payload.AwakeProcessActive = true; + payload.LaunchedViaCli = true; + payload.PowerToysRunning = false; + return payload.ToJsonObject(); + } + catch (Exception ex) + { + Logger.LogError("[MCP] Failed to start Awake CLI.", ex); + return AwakeStatusPayload.CreateError( + ex.Message, + powerToysRunning: false, + launchedViaCli: false).ToJsonObject(); + } + } + + private static ProcessStartInfo CreateSimpleStartInfo(string executablePath, AwakeMode mode, bool keepDisplayOn, uint durationSeconds) + { + string workingDirectory = Path.GetDirectoryName(executablePath) ?? AppDomain.CurrentDomain.BaseDirectory; + var startInfo = new ProcessStartInfo(executablePath) + { + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = string.IsNullOrEmpty(workingDirectory) ? AppDomain.CurrentDomain.BaseDirectory : workingDirectory, }; + + startInfo.ArgumentList.Add("--display-on"); + startInfo.ArgumentList.Add(keepDisplayOn ? "true" : "false"); + + if (mode == AwakeMode.TIMED && durationSeconds > 0) + { + startInfo.ArgumentList.Add("--time-limit"); + startInfo.ArgumentList.Add(durationSeconds.ToString(CultureInfo.InvariantCulture)); + } + + return startInfo; + } + + private static void StopAwakeProcesses() + { + string processName = Path.GetFileNameWithoutExtension(AwakeExecutableName); + try + { + Process[] awakeProcesses = Process.GetProcessesByName(processName); + foreach (Process process in awakeProcesses) + { + try + { + if (!process.HasExited) + { + process.Kill(true); + process.WaitForExit(TimeSpan.FromSeconds(5)); + } + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Failed to terminate Awake process {process.Id}: {ex.Message}"); + } + finally + { + process.Dispose(); + } + } + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Failed to enumerate Awake processes: {ex.Message}"); + } + } + + private static AwakeSettings BuildAwakeSnapshot(AwakeMode mode, bool keepDisplayOn, uint durationSeconds) + { + var snapshot = new AwakeSettings(); + snapshot.Properties.Mode = mode; + snapshot.Properties.KeepDisplayOn = keepDisplayOn; + snapshot.Properties.ProcessId = 0; + + if (mode == AwakeMode.TIMED && durationSeconds > 0) + { + TimeSpan timeSpan = TimeSpan.FromSeconds(durationSeconds); + snapshot.Properties.IntervalHours = (uint)timeSpan.TotalHours; + snapshot.Properties.IntervalMinutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60); + snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now.AddSeconds(durationSeconds); + } + else + { + snapshot.Properties.IntervalHours = 0; + snapshot.Properties.IntervalMinutes = 0; + snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now; + } + + return snapshot; + } + + private static bool TryResolveAwakeExecutable(out string executablePath) + { + string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + if (TryResolveAwakeExecutableFrom(baseDirectory, out executablePath)) + { + return true; + } + + string? parentDirectory = Directory.GetParent(baseDirectory)?.FullName; + if (!string.IsNullOrEmpty(parentDirectory) && TryResolveAwakeExecutableFrom(parentDirectory, out executablePath)) + { + return true; + } + + executablePath = string.Empty; + return false; + } + + private static bool TryResolveAwakeExecutableFrom(string rootDirectory, out string executablePath) + { + foreach (string relativePath in AwakeRelativeSearchPaths) + { + string candidate = Path.Combine(rootDirectory, relativePath); + if (File.Exists(candidate)) + { + executablePath = candidate; + return true; + } + } + + executablePath = string.Empty; + return false; + } + + private static bool IsAwakeProcessRunning() + { + try + { + string processName = Path.GetFileNameWithoutExtension(AwakeExecutableName); + return Process.GetProcessesByName(processName).Length > 0; + } + catch (Exception ex) + { + Logger.LogWarning($"[MCP] Unable to determine Awake process status: {ex.Message}"); + return false; + } + } + + private static bool IsAwakeActive(AwakeSettings settings) + { + // Only check if Awake module is enabled + return IsAwakeModuleEnabled(); + } + + private sealed class AwakeStatusPayload + { + internal string Mode { get; set; } = "unknown"; + + internal bool KeepDisplayOn { get; set; } + + internal bool IsProcessBound { get; set; } + + internal int ProcessId { get; set; } + + internal uint IntervalHours { get; set; } + + internal uint IntervalMinutes { get; set; } + + internal string ExpirationDateTime { get; set; } = string.Empty; + + internal string Summary { get; set; } = string.Empty; + + internal bool PowerToysRunning { get; set; } + + internal bool AwakeProcessActive { get; set; } + + internal bool LaunchedViaCli { get; set; } + + internal bool Success { get; set; } = true; + + internal string? ErrorMessage { get; set; } + + internal JsonObject ToJsonObject() + { + var result = new JsonObject + { + ["mode"] = Mode, + ["keepDisplayOn"] = KeepDisplayOn, + ["isProcessBound"] = IsProcessBound, + ["processId"] = ProcessId, + ["intervalHours"] = IntervalHours, + ["intervalMinutes"] = IntervalMinutes, + ["expirationDateTime"] = ExpirationDateTime, + ["summary"] = Summary, + ["powerToysRunning"] = PowerToysRunning, + ["awakeProcessActive"] = AwakeProcessActive, + ["launchedViaCli"] = LaunchedViaCli, + }; + + // Add error handling properties + if (!Success) + { + result["success"] = false; + if (!string.IsNullOrEmpty(ErrorMessage)) + { + result["error"] = ErrorMessage; + } + } + + return result; + } + + internal static AwakeStatusPayload FromSettings(AwakeSettings settings, string summary) + { + return new AwakeStatusPayload + { + Mode = settings.Properties.Mode.ToString().ToLowerInvariant(), + KeepDisplayOn = settings.Properties.KeepDisplayOn, + IsProcessBound = settings.Properties.ProcessId > 0, + ProcessId = (int)settings.Properties.ProcessId, + IntervalHours = settings.Properties.IntervalHours, + IntervalMinutes = settings.Properties.IntervalMinutes, + ExpirationDateTime = settings.Properties.ExpirationDateTime.ToString("O"), + Summary = summary, + }; + } + + internal static AwakeStatusPayload CreateInactive() + { + return new AwakeStatusPayload + { + Mode = "inactive", + KeepDisplayOn = false, + IsProcessBound = false, + ProcessId = 0, + IntervalHours = 0, + IntervalMinutes = 0, + ExpirationDateTime = string.Empty, + Summary = "PowerToys Awake is not running because PowerToys is not active.", + PowerToysRunning = false, + AwakeProcessActive = false, + LaunchedViaCli = false, + }; + } + + internal static AwakeStatusPayload CreateUnknownActive(bool powerToysRunning, bool launchedViaCli) + { + return new AwakeStatusPayload + { + Mode = "unknown", + KeepDisplayOn = false, + IsProcessBound = false, + ProcessId = 0, + IntervalHours = 0, + IntervalMinutes = 0, + ExpirationDateTime = string.Empty, + Summary = "An Awake process is currently running, but its configuration cannot be determined. To terminate the existing process and start with new settings, use force=true.", + PowerToysRunning = powerToysRunning, + AwakeProcessActive = true, + LaunchedViaCli = launchedViaCli, + }; + } + + internal static AwakeStatusPayload CreateError(string errorMessage, AwakeSettings? settings = null, bool powerToysRunning = false, bool launchedViaCli = false) + { + var payload = new AwakeStatusPayload + { + Success = false, + ErrorMessage = errorMessage, + PowerToysRunning = powerToysRunning, + LaunchedViaCli = launchedViaCli, + AwakeProcessActive = true, + }; + + if (settings != null) + { + payload.Mode = settings.Properties.Mode.ToString().ToLowerInvariant(); + payload.KeepDisplayOn = settings.Properties.KeepDisplayOn; + payload.IsProcessBound = settings.Properties.ProcessId > 0; + payload.ProcessId = (int)settings.Properties.ProcessId; + payload.IntervalHours = settings.Properties.IntervalHours; + payload.IntervalMinutes = settings.Properties.IntervalMinutes; + payload.ExpirationDateTime = settings.Properties.ExpirationDateTime.ToString("O"); + payload.Summary = "An Awake session is already active with the current settings. To override and change the configuration, use force=true."; + } + else + { + payload.Mode = "unknown"; + payload.Summary = "An Awake process is currently running. To terminate the existing process and start with new settings, use force=true."; + } + + return payload; + } } } } diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 85021a71b7..4b29149f78 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -156,7 +156,6 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"PowerToys.ShortcutGuideModuleInterface.dll", L"PowerToys.ColorPicker.dll", L"PowerToys.AwakeModuleInterface.dll", - L"PowerToys.McpModuleInterface.dll", L"PowerToys.FindMyMouse.dll", L"PowerToys.MouseHighlighter.dll", L"PowerToys.MouseJump.dll", diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index 040ca01c08..977c03b839 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -530,23 +530,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool mcp = true; - - [JsonPropertyName("MCP")] - public bool MCP - { - get => mcp; - set - { - if (mcp != value) - { - LogTelemetryEvent(value); - mcp = value; - NotifyChange(); - } - } - } - private void NotifyChange() { notifyEnabledChangedAction?.Invoke(); diff --git a/src/settings-ui/Settings.UI.Library/McpProperties.cs b/src/settings-ui/Settings.UI.Library/McpProperties.cs deleted file mode 100644 index 774486c99c..0000000000 --- a/src/settings-ui/Settings.UI.Library/McpProperties.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.PowerToys.Settings.UI.Library -{ - public class McpProperties - { - public McpProperties() - { - RegisterToVSCode = false; - RegisterToWindowsCopilot = false; - EnabledModules = new Dictionary - { - { "Awake", true }, - }; - } - - [JsonPropertyName("registerToVSCode")] - public bool RegisterToVSCode { get; set; } - - [JsonPropertyName("registerToWindowsCopilot")] - public bool RegisterToWindowsCopilot { get; set; } - - [JsonPropertyName("enabledModules")] - public Dictionary EnabledModules { get; set; } - } -} diff --git a/src/settings-ui/Settings.UI.Library/McpSettings.cs b/src/settings-ui/Settings.UI.Library/McpSettings.cs deleted file mode 100644 index 310a0ed660..0000000000 --- a/src/settings-ui/Settings.UI.Library/McpSettings.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json.Serialization; - -using Microsoft.PowerToys.Settings.UI.Library.Interfaces; - -namespace Microsoft.PowerToys.Settings.UI.Library -{ - public class McpSettings : BasePTModuleSettings, ISettingsConfig, ICloneable - { - public const string ModuleName = "MCP"; - - public McpSettings() - { - Name = ModuleName; - Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - Properties = new McpProperties(); - } - - [JsonPropertyName("properties")] - public McpProperties Properties { get; set; } - - public object Clone() - { - return new McpSettings() - { - Name = Name, - Version = Version, - Properties = new McpProperties() - { - RegisterToVSCode = Properties.RegisterToVSCode, - RegisterToWindowsCopilot = Properties.RegisterToWindowsCopilot, - EnabledModules = Properties.EnabledModules.ToDictionary(entry => entry.Key, entry => entry.Value), - }, - }; - } - - public string GetModuleName() - { - return Name; - } - - public bool UpgradeSettingsConfiguration() - { - return false; - } - } -} diff --git a/src/settings-ui/Settings.UI.Library/Utilities/McpRegistrationHelper.cs b/src/settings-ui/Settings.UI.Library/Utilities/McpRegistrationHelper.cs deleted file mode 100644 index 61e2bfecc0..0000000000 --- a/src/settings-ui/Settings.UI.Library/Utilities/McpRegistrationHelper.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Microsoft.PowerToys.Settings.UI.Library.Utilities -{ - /// - /// Helper class for registering/unregistering MCP server to VS Code and Windows Copilot. - /// - public static class McpRegistrationHelper - { - private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions - { - WriteIndented = true, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - }; - - /// - /// Register or unregister MCP server to VS Code settings.json. - /// - /// True to register, false to unregister. - /// True if successful, false otherwise. - public static bool UpdateVSCodeRegistration(bool register) - { - try - { - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var settingsPath = Path.Combine(appData, "Code", "User", "settings.json"); - - if (!File.Exists(settingsPath)) - { - // Try VS Code Insiders - settingsPath = Path.Combine(appData, "Code - Insiders", "User", "settings.json"); - if (!File.Exists(settingsPath)) - { - // VS Code settings.json not found - return false; - } - } - - return UpdateVSCodeSettingsFile(settingsPath, register); - } - catch - { - return false; - } - } - - /// - /// Register or unregister MCP server to Windows Copilot. - /// - /// True to register, false to unregister. - /// True if successful, false otherwise. - public static bool UpdateWindowsCopilotRegistration(bool register) - { - // TODO: Implement Windows Copilot registration when API is available - _ = register; // Suppress unused parameter warning - return false; - } - - private static bool UpdateVSCodeSettingsFile(string settingsPath, bool register) - { - try - { - // Backup original file - var backupPath = settingsPath + ".bak"; - File.Copy(settingsPath, backupPath, true); - - // Read existing settings - var settingsJson = File.ReadAllText(settingsPath); - JsonNode rootNode = JsonNode.Parse(settingsJson); - - if (rootNode == null || rootNode is not JsonObject rootObject) - { - return false; - } - - // Get or create mcp.servers object - if (!rootObject.ContainsKey("mcp.servers")) - { - if (register) - { - rootObject["mcp.servers"] = new JsonObject(); - } - else - { - // Nothing to unregister - return true; - } - } - - var mcpServers = rootObject["mcp.servers"] as JsonObject; - if (mcpServers == null) - { - return false; - } - - if (register) - { - // Get PowerToys installation path - var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PowerToys.McpServer.exe"); - if (!File.Exists(exePath)) - { - return false; - } - - // Add powertoys server entry - var serverConfig = new JsonObject - { - ["command"] = exePath.Replace("\\", "/"), - }; - mcpServers["powertoys"] = serverConfig; - } - else - { - // Remove powertoys server entry - mcpServers.Remove("powertoys"); - - // Remove mcp.servers if empty - if (mcpServers.Count == 0) - { - rootObject.Remove("mcp.servers"); - } - } - - // Write updated settings with proper formatting - var updatedJson = JsonSerializer.Serialize(rootNode, JsonOptions); - File.WriteAllText(settingsPath, updatedJson); - - return true; - } - catch - { - // Try to restore from backup - var backupPath = settingsPath + ".bak"; - if (File.Exists(backupPath)) - { - try - { - File.Copy(backupPath, settingsPath, true); - } - catch - { - // Ignore backup restore errors - } - } - - return false; - } - } - } -} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 45dace9823..19cd75b022 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -415,7 +415,6 @@ namespace Microsoft.PowerToys.Settings.UI case "AdvancedPaste": return typeof(AdvancedPastePage); case "AlwaysOnTop": return typeof(AlwaysOnTopPage); case "Awake": return typeof(AwakePage); - case "MCP": return typeof(McpPage); case "CmdNotFound": return typeof(CmdNotFoundPage); case "ColorPicker": return typeof(ColorPickerPage); case "LightSwitch": return typeof(LightSwitchPage); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/McpPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/McpPage.xaml deleted file mode 100644 index ce2c16189e..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/McpPage.xaml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/McpPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/McpPage.xaml.cs deleted file mode 100644 index dd3b28cff1..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/McpPage.xaml.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Helpers; -using Microsoft.PowerToys.Settings.UI.Library; -using Microsoft.PowerToys.Settings.UI.Library.Interfaces; -using Microsoft.PowerToys.Settings.UI.Library.Utilities; -using Microsoft.PowerToys.Settings.UI.ViewModels; -using Microsoft.UI.Dispatching; -using PowerToys.GPOWrapper; - -namespace Microsoft.PowerToys.Settings.UI.Views -{ - public sealed partial class McpPage : NavigablePage, IRefreshablePage - { - private readonly SettingsUtils _settingsUtils; - - private readonly SettingsRepository _generalSettingsRepository; - private readonly SettingsRepository _moduleSettingsRepository; - - private readonly DispatcherQueue _dispatcherQueue; - - private readonly Func _sendConfigMsg; - - private McpViewModel ViewModel { get; set; } - - public McpPage() - { - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - _settingsUtils = new SettingsUtils(); - _sendConfigMsg = ShellPage.SendDefaultIPCMessage; - - ViewModel = new McpViewModel(); - ViewModel.PropertyChanged += ViewModel_PropertyChanged; - - _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); - _moduleSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); - - // We load the view model settings first. - LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); - - this.InitializeComponent(); - } - - public void RefreshEnabledState() - { - ViewModel.RefreshEnabledState(); - } - - private void LoadSettings( - SettingsRepository generalSettingsRepository, - SettingsRepository moduleSettingsRepository) - { - var generalSettings = generalSettingsRepository.SettingsConfig; - var moduleSettings = moduleSettingsRepository.SettingsConfig; - - ViewModel.IsEnabled = generalSettings.Enabled.MCP; - ViewModel.RegisterToVSCode = moduleSettings.Properties.RegisterToVSCode; - ViewModel.RegisterToWindowsCopilot = moduleSettings.Properties.RegisterToWindowsCopilot; - ViewModel.AwakeModuleEnabled = moduleSettings.Properties.EnabledModules.TryGetValue("Awake", out bool awakeEnabled) ? awakeEnabled : true; - - // TODO: Uncomment when GPO support is implemented - // ViewModel.IsEnabledGpoConfigured = GPOWrapper.GetConfiguredMcpEnabledValue() == GpoRuleConfigured.Enabled || GPOWrapper.GetConfiguredMcpEnabledValue() == GpoRuleConfigured.Disabled; - ViewModel.IsEnabledGpoConfigured = false; - } - - private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) - { - switch (e.PropertyName) - { - case nameof(ViewModel.IsEnabled): - { - var generalSettings = _generalSettingsRepository.SettingsConfig; - generalSettings.Enabled.MCP = ViewModel.IsEnabled; - OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettings); - _sendConfigMsg(outgoing.ToString()); - } - - break; - case nameof(ViewModel.RegisterToVSCode): - { - // Update VS Code registration - bool success = McpRegistrationHelper.UpdateVSCodeRegistration(ViewModel.RegisterToVSCode); - if (!success && ViewModel.RegisterToVSCode) - { - // If registration failed, revert the toggle - ViewModel.RegisterToVSCode = false; - } - - var moduleSettings = _moduleSettingsRepository.SettingsConfig; - moduleSettings.Properties.RegisterToVSCode = ViewModel.RegisterToVSCode; - _settingsUtils.SaveSettings(moduleSettings.ToJsonString(), McpSettings.ModuleName); - } - - break; - case nameof(ViewModel.RegisterToWindowsCopilot): - { - // Update Windows Copilot registration - bool success = McpRegistrationHelper.UpdateWindowsCopilotRegistration(ViewModel.RegisterToWindowsCopilot); - if (!success && ViewModel.RegisterToWindowsCopilot) - { - // If registration failed, revert the toggle - ViewModel.RegisterToWindowsCopilot = false; - } - - var moduleSettings = _moduleSettingsRepository.SettingsConfig; - moduleSettings.Properties.RegisterToWindowsCopilot = ViewModel.RegisterToWindowsCopilot; - _settingsUtils.SaveSettings(moduleSettings.ToJsonString(), McpSettings.ModuleName); - } - - break; - case nameof(ViewModel.AwakeModuleEnabled): - { - var moduleSettings = _moduleSettingsRepository.SettingsConfig; - moduleSettings.Properties.EnabledModules["Awake"] = ViewModel.AwakeModuleEnabled; - _settingsUtils.SaveSettings(moduleSettings.ToJsonString(), McpSettings.ModuleName); - } - - break; - } - } - } -} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml index d222a1751d..c2de045ed2 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml @@ -394,12 +394,6 @@ helpers:NavHelper.NavigateTo="views:HostsPage" AutomationProperties.AutomationId="HostsNavItem" Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Hosts.png}" /> - Register to VS Code - Automatically register MCP server to VS Code settings.json + Add the PowerToys MCP server to VS Code. Remove it manually from VS Code when you no longer need it. + + + PowerToys couldn't update VS Code + + + PowerToys couldn't register the MCP server with VS Code automatically. Run {0} in a PowerShell window or add the server manually from VS Code's Model Context Protocol settings. + {0} is a command that can be pasted into a terminal. + + + Got it + + + Registration added + + + PowerToys added the MCP server to VS Code. To remove it later, open VS Code, run "Copilot: Manage MCP Servers", and delete "powertoys-mcp". + + + VS Code's command-line interface isn't available. Install or update VS Code, enable the "code" command, and try again. + + + Add to VS Code Register to Windows Copilot diff --git a/src/settings-ui/Settings.UI/ViewModels/McpViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/McpViewModel.cs deleted file mode 100644 index 5ee07320ce..0000000000 --- a/src/settings-ui/Settings.UI/ViewModels/McpViewModel.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.ComponentModel; -using System.Runtime.CompilerServices; -using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Library; -using Microsoft.PowerToys.Settings.UI.Library.Helpers; - -namespace Microsoft.PowerToys.Settings.UI.ViewModels -{ - public class McpViewModel : Observable - { - private bool _isEnabled; - private bool _registerToVSCode; - private bool _registerToWindowsCopilot; - private bool _awakeModuleEnabled; - private bool _enabledStateIsGPOConfigured; - private bool _enabledGPOConfiguration; - - public McpViewModel() - { - _isEnabled = false; - _registerToVSCode = false; - _registerToWindowsCopilot = false; - _awakeModuleEnabled = true; - } - - public bool IsEnabled - { - get - { - if (_enabledStateIsGPOConfigured) - { - return _enabledGPOConfiguration; - } - else - { - return _isEnabled; - } - } - - set - { - if (_isEnabled != value) - { - if (_enabledStateIsGPOConfigured) - { - // If it's GPO configured, shouldn't be able to change this state. - return; - } - - _isEnabled = value; - RefreshEnabledState(); - NotifyPropertyChanged(); - } - } - } - - public bool IsEnabledGpoConfigured - { - get => _enabledStateIsGPOConfigured; - set - { - if (_enabledStateIsGPOConfigured != value) - { - _enabledStateIsGPOConfigured = value; - NotifyPropertyChanged(); - } - } - } - - public bool EnabledGPOConfiguration - { - get => _enabledGPOConfiguration; - set - { - if (_enabledGPOConfiguration != value) - { - _enabledGPOConfiguration = value; - NotifyPropertyChanged(); - } - } - } - - public bool RegisterToVSCode - { - get => _registerToVSCode; - set - { - if (_registerToVSCode != value) - { - _registerToVSCode = value; - NotifyPropertyChanged(); - } - } - } - - public bool RegisterToWindowsCopilot - { - get => _registerToWindowsCopilot; - set - { - if (_registerToWindowsCopilot != value) - { - _registerToWindowsCopilot = value; - NotifyPropertyChanged(); - } - } - } - - public bool AwakeModuleEnabled - { - get => _awakeModuleEnabled; - set - { - if (_awakeModuleEnabled != value) - { - _awakeModuleEnabled = value; - NotifyPropertyChanged(); - } - } - } - - public void RefreshEnabledState() - { - OnPropertyChanged(nameof(IsEnabled)); - } - - protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") - { - OnPropertyChanged(propertyName); - } - } -}