mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
update
This commit is contained in:
@@ -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
11
.vscode/mcp.json
vendored
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"servers": {
|
||||
"powertoys-mcp": {
|
||||
"command": "C:/PowerToys/x64/Release/PowerToys.McpServer.exe",
|
||||
"args": [],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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>
|
||||
649
src/McpServer/Tools/AwakeTools.cs
Normal file
649
src/McpServer/Tools/AwakeTools.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace McpConstants
|
||||
{
|
||||
inline const wchar_t* ModuleKey = L"MCP";
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
#include "pch.h"
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
@@ -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
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace Trace
|
||||
{
|
||||
void RegisterProvider() noexcept;
|
||||
void UnregisterProvider() noexcept;
|
||||
void EnableMCP(bool enabled) noexcept;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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=}">
|
||||
<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=}">
|
||||
<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=}">
|
||||
<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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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't update VS Code</value>
|
||||
</data>
|
||||
<data name="Mcp_VSCodeRegistrationFailedDialog_Content" xml:space="preserve">
|
||||
<value>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.</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 "Copilot: Manage MCP Servers", and delete "powertoys-mcp".</value>
|
||||
</data>
|
||||
<data name="Mcp_VSCodeRegistrationUnavailableDialog_Content" xml:space="preserve">
|
||||
<value>VS Code's command-line interface isn't available. Install or update VS Code, enable the "code" 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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user