This commit is contained in:
Leilei Zhang
2025-10-22 14:36:21 +08:00
parent 0243f57ea3
commit f32b2534ce
33 changed files with 1165 additions and 1134 deletions

View File

@@ -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": [

11
.vscode/mcp.json vendored
View File

@@ -1,11 +0,0 @@
{
"servers": {
"powertoys-mcp": {
"command": "C:/PowerToys/x64/Release/PowerToys.McpServer.exe",
"args": [],
"env": {
"NODE_ENV": "production"
}
}
}
}

View File

@@ -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}

View File

@@ -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 |

View File

@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>PowerToys.McpServer</RootNamespace>
<AssemblyName>PowerToys.McpServer</AssemblyName>
<Nullable>enable</Nullable>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<OutputPath>..\..\$(Platform)\$(Configuration)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
@@ -26,7 +26,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\common\ManagedCommon\ManagedCommon.csproj" />
</ItemGroup>
</Project>

View File

@@ -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
{
/// <summary>
/// MCP tools for PowerToys Awake module.
/// </summary>
[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),
];
/// <summary>
/// Gets the current Awake mode and configuration.
/// </summary>
/// <returns>JSON object with current Awake status.</returns>
[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>(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();
}
}
/// <summary>
/// Sets the Awake mode to passive (allow system sleep).
/// </summary>
/// <returns>JSON object with updated Awake status.</returns>
[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>(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();
}
}
/// <summary>
/// Sets the Awake mode to indefinite (keep system awake forever).
/// </summary>
/// <param name="keepDisplayOn">Whether to keep the display on. Default is true.</param>
/// <returns>JSON object with updated Awake status.</returns>
[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>(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();
}
}
/// <summary>
/// Sets the Awake mode to timed (keep system awake for a specific duration).
/// </summary>
/// <param name="durationSeconds">Duration in seconds (minimum 60).</param>
/// <param name="keepDisplayOn">Whether to keep the display on. Default is true.</param>
/// <returns>JSON object with updated Awake status.</returns>
[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>(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;
}
}
/// <summary>
/// Gets whether the Awake module is enabled in PowerToys settings.
/// </summary>
/// <returns>True if Awake module is enabled, false otherwise</returns>
private static bool IsAwakeModuleEnabled()
{
try
{
var generalSettings = SettingsUtils.GetSettings<GeneralSettings>();
return generalSettings?.Enabled?.Awake == true;
}
catch
{
// If we can't read settings, assume disabled
return false;
}
}
/// <summary>
/// Checks PowerToys and Awake module status.
/// </summary>
/// <returns>Tuple containing (powerToysRunning, awakeModuleEnabled)</returns>
private static (bool PowerToysRunning, bool AwakeModuleEnabled) CheckPowerToysAndAwakeStatus()
{
bool powerToysRunning = IsPowerToysRunning();
bool awakeModuleEnabled = powerToysRunning && IsAwakeModuleEnabled();
return (powerToysRunning, awakeModuleEnabled);
}
/// <summary>
/// Handles CLI scenario when PowerToys is not running or Awake module is disabled.
/// </summary>
/// <param name="mode">The Awake mode to set</param>
/// <param name="keepDisplayOn">Whether to keep display on</param>
/// <param name="durationSeconds">Duration in seconds (0 for indefinite)</param>
/// <param name="force">Whether to force override existing process</param>
/// <returns>JSON response for CLI scenario</returns>
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;
}
}
}
}

View File

@@ -1,6 +0,0 @@
#pragma once
namespace McpConstants
{
inline const wchar_t* ModuleKey = L"MCP";
}

View File

@@ -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

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{8E87A3A7-9B8C-4F7D-A3E1-5C4B8D9F6E2A}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>McpServer</RootNamespace>
<ProjectName>McpModuleInterface</ProjectName>
<TargetName>PowerToys.McpModuleInterface</TargetName>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="McpConstants.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="McpModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="McpConstants.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="McpModuleInterface.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -1,167 +0,0 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/interop/shared_constants.h>
#include "trace.h"
#include "resource.h"
#include "McpConstants.h"
#include <common/logger/logger.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include <common/utils/os-detect.h>
#include <common/utils/winapi_error.h>
#include <filesystem>
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<HINSTANCE>(&__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();
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -1 +0,0 @@
#include "pch.h"

View File

@@ -1,5 +0,0 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <string>

View File

@@ -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

View File

@@ -1,30 +0,0 @@
#include "pch.h"
#include "trace.h"
#include <TraceLoggingProvider.h>
#include <common/Telemetry/TraceBase.h>
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"));
}

View File

@@ -1,8 +0,0 @@
#pragma once
namespace Trace
{
void RegisterProvider() noexcept;
void UnregisterProvider() noexcept;
void EnableMCP(bool enabled) noexcept;
}

View File

@@ -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();
/// <summary>
/// Check if Awake module is enabled in MCP settings.
/// </summary>
private static void CheckModuleEnabled()
{
try
{
McpSettings mcpSettings = SettingsUtils.GetSettingsOrDefault<McpSettings>(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),
];
/// <summary>
/// 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>(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>(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>(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>(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;
}
}
/// <summary>
/// Gets whether the Awake module is enabled in PowerToys settings.
/// </summary>
/// <returns>True if Awake module is enabled, false otherwise</returns>
private static bool IsAwakeModuleEnabled()
{
try
{
var generalSettings = SettingsUtils.GetSettings<GeneralSettings>();
return generalSettings?.Enabled?.Awake == true;
}
catch
{
// If we can't read settings, assume disabled
return false;
}
}
/// <summary>
/// Checks PowerToys and Awake module status.
/// </summary>
/// <returns>Tuple containing (powerToysRunning, awakeModuleEnabled)</returns>
private static (bool PowerToysRunning, bool AwakeModuleEnabled) CheckPowerToysAndAwakeStatus()
{
bool powerToysRunning = IsPowerToysRunning();
bool awakeModuleEnabled = powerToysRunning && IsAwakeModuleEnabled();
return (powerToysRunning, awakeModuleEnabled);
}
/// <summary>
/// Handles CLI scenario when PowerToys is not running or Awake module is disabled.
/// </summary>
/// <param name="mode">The Awake mode to set</param>
/// <param name="keepDisplayOn">Whether to keep display on</param>
/// <param name="durationSeconds">Duration in seconds (0 for indefinite)</param>
/// <param name="force">Whether to force override existing process</param>
/// <returns>JSON response for CLI scenario</returns>
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;
}
}
}
}

View File

@@ -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",

View File

@@ -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();

View File

@@ -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<string, bool>
{
{ "Awake", true },
};
}
[JsonPropertyName("registerToVSCode")]
public bool RegisterToVSCode { get; set; }
[JsonPropertyName("registerToWindowsCopilot")]
public bool RegisterToWindowsCopilot { get; set; }
[JsonPropertyName("enabledModules")]
public Dictionary<string, bool> EnabledModules { get; set; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Helper class for registering/unregistering MCP server to VS Code and Windows Copilot.
/// </summary>
public static class McpRegistrationHelper
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>
/// Register or unregister MCP server to VS Code settings.json.
/// </summary>
/// <param name="register">True to register, false to unregister.</param>
/// <returns>True if successful, false otherwise.</returns>
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;
}
}
/// <summary>
/// Register or unregister MCP server to Windows Copilot.
/// </summary>
/// <param name="register">True to register, false to unregister.</param>
/// <returns>True if successful, false otherwise.</returns>
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;
}
}
}
}

View File

@@ -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);

View File

@@ -1,69 +0,0 @@
<local:NavigablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.McpPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
d:DataContext="{d:DesignInstance Type=viewModels:McpViewModel}"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<controls:SettingsPageControl
x:Uid="Mcp"
IsTabStop="False"
ModuleImageSource="ms-appx:///Assets/Settings/Modules/Mcp.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="McpEnableSettingsCard"
x:Uid="Mcp_EnableSettingsCard"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Mcp.png}"
IsEnabled="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="Mcp_RegistrationSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="McpVSCodeRegistrationSettingsCard"
x:Uid="Mcp_VSCodeRegistrationSettingsCard"
Description="Automatically register MCP server to VS Code settings.json"
Header="Register to VS Code"
HeaderIcon="{ui:FontIcon Glyph=&#xE943;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.RegisterToVSCode, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="McpWindowsCopilotRegistrationSettingsCard"
x:Uid="Mcp_WindowsCopilotRegistrationSettingsCard"
Description="Automatically register MCP server to Windows Copilot"
Header="Register to Windows Copilot"
HeaderIcon="{ui:FontIcon Glyph=&#xE943;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.RegisterToWindowsCopilot, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="Mcp_ModuleTogglesSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="McpAwakeModuleToggleSettingsCard"
x:Uid="Mcp_AwakeModuleToggleSettingsCard"
Description="Enable or disable Awake module tools in MCP"
Header="Awake Module"
HeaderIcon="{ui:FontIcon Glyph=&#xE706;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AwakeModuleEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink x:Uid="LearnMore_MCP" Link="https://github.com/microsoft/PowerToys/wiki/Model-Context-Protocol" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>

View File

@@ -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<GeneralSettings> _generalSettingsRepository;
private readonly SettingsRepository<McpSettings> _moduleSettingsRepository;
private readonly DispatcherQueue _dispatcherQueue;
private readonly Func<string, int> _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<GeneralSettings>.GetInstance(_settingsUtils);
_moduleSettingsRepository = SettingsRepository<McpSettings>.GetInstance(_settingsUtils);
// We load the view model settings first.
LoadSettings(_generalSettingsRepository, _moduleSettingsRepository);
this.InitializeComponent();
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();
}
private void LoadSettings(
SettingsRepository<GeneralSettings> generalSettingsRepository,
SettingsRepository<McpSettings> 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;
}
}
}
}

View File

@@ -394,12 +394,6 @@
helpers:NavHelper.NavigateTo="views:HostsPage"
AutomationProperties.AutomationId="HostsNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Hosts.png}" />
<NavigationViewItem
x:Name="McpNavigationItem"
x:Uid="Shell_Mcp"
helpers:NavHelper.NavigateTo="views:McpPage"
AutomationProperties.AutomationId="McpNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Mcp.png}" />
<NavigationViewItem
x:Name="RegistryPreviewNavigationItem"
x:Uid="Shell_RegistryPreview"

View File

@@ -4118,7 +4118,29 @@ Activate by holding the key for the character you want to add an accent to, then
<value>Register to VS Code</value>
</data>
<data name="Mcp_VSCodeRegistrationSettingsCard.Description" xml:space="preserve">
<value>Automatically register MCP server to VS Code settings.json</value>
<value>Add the PowerToys MCP server to VS Code. Remove it manually from VS Code when you no longer need it.</value>
</data>
<data name="Mcp_VSCodeRegistrationFailedDialog_Title" xml:space="preserve">
<value>PowerToys couldn&apos;t update VS Code</value>
</data>
<data name="Mcp_VSCodeRegistrationFailedDialog_Content" xml:space="preserve">
<value>PowerToys couldn&apos;t register the MCP server with VS Code automatically. Run {0} in a PowerShell window or add the server manually from VS Code&apos;s Model Context Protocol settings.</value>
<comment>{0} is a command that can be pasted into a terminal.</comment>
</data>
<data name="Mcp_VSCodeRegistrationDialog_PrimaryButtonText" xml:space="preserve">
<value>Got it</value>
</data>
<data name="Mcp_VSCodeRegistrationSuccessDialog_Title" xml:space="preserve">
<value>Registration added</value>
</data>
<data name="Mcp_VSCodeRegistrationSuccessDialog_Content" xml:space="preserve">
<value>PowerToys added the MCP server to VS Code. To remove it later, open VS Code, run &quot;Copilot: Manage MCP Servers&quot;, and delete &quot;powertoys-mcp&quot;.</value>
</data>
<data name="Mcp_VSCodeRegistrationUnavailableDialog_Content" xml:space="preserve">
<value>VS Code&apos;s command-line interface isn&apos;t available. Install or update VS Code, enable the &quot;code&quot; command, and try again.</value>
</data>
<data name="Mcp_AddToVSCodeButton.Content" xml:space="preserve">
<value>Add to VS Code</value>
</data>
<data name="Mcp_WindowsCopilotRegistrationSettingsCard.Header" xml:space="preserve">
<value>Register to Windows Copilot</value>

View File

@@ -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);
}
}
}