diff --git a/Directory.Packages.props b/Directory.Packages.props index 649c927a64..e44d6e9c9e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,6 +61,7 @@ + diff --git a/PowerToys.sln b/PowerToys.sln index c2f96095ac..fa372d0495 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -805,6 +805,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MCPServer", "MCPServer", "{B637E6DD-FB81-4595-BB9C-01168556EA9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPServer", "src\modules\MCPServer\MCPServer\MCPServer.csproj", "{20CBF173-9E8D-3236-6664-5B9C303794A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2923,6 +2927,14 @@ Global {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.Build.0 = Debug|ARM64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.ActiveCfg = Debug|x64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.Build.0 = Debug|x64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.ActiveCfg = Release|ARM64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.Build.0 = Release|ARM64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.ActiveCfg = Release|x64 + {20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3243,6 +3255,8 @@ Global {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {B637E6DD-FB81-4595-BB9C-01168556EA9E} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {20CBF173-9E8D-3236-6664-5B9C303794A3} = {B637E6DD-FB81-4595-BB9C-01168556EA9E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/MCPServer/MCPServer/MCPServer.csproj b/src/modules/MCPServer/MCPServer/MCPServer.csproj new file mode 100644 index 0000000000..7f9a808ec4 --- /dev/null +++ b/src/modules/MCPServer/MCPServer/MCPServer.csproj @@ -0,0 +1,46 @@ + + + + + + + WinExe + false + false + false + PowerToys.MCPServer + PowerToys MCP Server for Model Context Protocol + true + ..\..\..\..\$(Platform)\$(Configuration) + false + true + PowerToys.MCPServer + enable + + + + + PowerToys.GPOWrapper + $(OutDir) + false + + + + + + + + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServer/Program.cs b/src/modules/MCPServer/MCPServer/Program.cs new file mode 100644 index 0000000000..6886c73fb2 --- /dev/null +++ b/src/modules/MCPServer/MCPServer/Program.cs @@ -0,0 +1,34 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Server; +using PowerToys.MCPServer.Tools; + +namespace MCPServer +{ + internal sealed class Program + { + private static async Task Main(string[] args) + { + var builder = Host.CreateApplicationBuilder(args); + builder.Logging.AddConsole(consoleLogOptions => + { + // Configure all logs to go to stderr + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; + }); + builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(); + await builder.Build().RunAsync(); + + return 0; + } + } +} diff --git a/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs b/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs new file mode 100644 index 0000000000..b6bff69e4b --- /dev/null +++ b/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs @@ -0,0 +1,17 @@ +// 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 ModelContextProtocol.Server; + +namespace PowerToys.MCPServer.Tools +{ + [McpServerToolType] + public static class AwakeTools + { + [McpServerTool] + [Description("Echoes the message back to the client.")] + public static string SetTimeTest(string message) => $"Hello {message}"; + } +} diff --git a/src/modules/MCPServer/MCPServer/Tools/EchoTool.cs b/src/modules/MCPServer/MCPServer/Tools/EchoTool.cs new file mode 100644 index 0000000000..f68a18e4ae --- /dev/null +++ b/src/modules/MCPServer/MCPServer/Tools/EchoTool.cs @@ -0,0 +1,17 @@ +// 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 ModelContextProtocol.Server; + +namespace PowerToys.MCPServer.Tools +{ + [McpServerToolType] + public static class EchoTool + { + [McpServerTool] + [Description("Echoes the message back to the client.")] + public static string Echo(string message) => $"Hello {message}"; + } +} diff --git a/src/modules/MCPServer/MCPServer/appsettings.json b/src/modules/MCPServer/MCPServer/appsettings.json new file mode 100644 index 0000000000..9c71e05cb6 --- /dev/null +++ b/src/modules/MCPServer/MCPServer/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "PowerToys.MCPServer": "Debug" + } + }, + "MCPServer": { + "Port": 8080, + "MaxConcurrentConnections": 100, + "RequestTimeoutSeconds": 30, + "EnableTools": true, + "EnableResources": true, + "Transport": "http" + } +} \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServerModuleInterface/MCPServerModuleInterface.def b/src/modules/MCPServer/MCPServerModuleInterface/MCPServerModuleInterface.def new file mode 100644 index 0000000000..6421b71234 --- /dev/null +++ b/src/modules/MCPServer/MCPServerModuleInterface/MCPServerModuleInterface.def @@ -0,0 +1,2 @@ +EXPORTS +powertoy_create \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServerModuleInterface/MCPServerModuleInterface.vcxproj b/src/modules/MCPServer/MCPServerModuleInterface/MCPServerModuleInterface.vcxproj new file mode 100644 index 0000000000..6318dc7fec --- /dev/null +++ b/src/modules/MCPServer/MCPServerModuleInterface/MCPServerModuleInterface.vcxproj @@ -0,0 +1,42 @@ + + + + + 16.0 + {A8B8D654-8F2A-4E6C-9B4F-1234567890AB} + Win32Proj + MCPServerModuleInterface + 10.0 + + + DynamicLibrary + + + + Use + pch.h + + + Windows + MCPServerModuleInterface.def + + + + + + Create + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServerModuleInterface/dllmain.cpp b/src/modules/MCPServer/MCPServerModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..fb9c1678e3 --- /dev/null +++ b/src/modules/MCPServer/MCPServerModuleInterface/dllmain.cpp @@ -0,0 +1,298 @@ +#include "pch.h" +#include +#include +#include +#include +#include +#include + +namespace NonLocalizable +{ + const wchar_t ModulePath[] = L"PowerToys.MCPServer.exe"; + const wchar_t ModuleKey[] = L"MCPServer"; +} + +BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +class MCPServerModuleInterface : public PowertoyModuleIface +{ +public: + virtual PCWSTR get_name() override + { + return app_name.c_str(); + } + + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_not_configured; + } + + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + PowerToysSettings::Settings settings(hinstance, get_name()); + + settings.set_description(L"MCP Server provides Model Context Protocol access to PowerToys functionality for AI assistants and tools"); + settings.set_icon_key(L"pt-mcp-server"); + + // Port configuration + settings.add_int_spinner( + L"port", + L"Server Port", + m_port, + 1024, + 65535, + 1); + + // Auto start option + settings.add_bool_toggle( + L"auto_start", + L"Auto Start Server", + m_auto_start); + + // Enable tools API + settings.add_bool_toggle( + L"enable_tools", + L"Enable Tools API", + m_enable_tools); + + // Enable resources API + settings.add_bool_toggle( + L"enable_resources", + L"Enable Resources API", + m_enable_resources); + + // Transport protocol + settings.add_dropdown( + L"transport", + L"Transport Protocol", + m_transport, + std::vector>{ + { L"http", L"HTTP" }, + { L"stdio", L"Standard I/O" }, + { L"tcp", L"TCP Socket" } + }); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + virtual void set_config(const wchar_t* config) override + { + try + { + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + if (auto port = values.get_int_value(L"port")) + { + m_port = port.value(); + } + + if (auto auto_start = values.get_bool_value(L"auto_start")) + { + m_auto_start = auto_start.value(); + } + + if (auto enable_tools = values.get_bool_value(L"enable_tools")) + { + m_enable_tools = enable_tools.value(); + } + + if (auto enable_resources = values.get_bool_value(L"enable_resources")) + { + m_enable_resources = enable_resources.value(); + } + + if (auto transport = values.get_string_value(L"transport")) + { + m_transport = transport.value(); + } + + values.save_to_settings_file(); + + // If service is running, restart to apply new configuration + if (m_enabled && is_process_running()) + { + StopMCPServer(); + StartMCPServer(); + } + } + catch (std::exception& e) + { + Logger::error("MCPServer configuration parsing failed: {}", std::string{ e.what() }); + } + } + + virtual void enable() override + { + Logger::info("MCPServer enabling"); + m_enabled = true; + if (m_auto_start) + { + StartMCPServer(); + } + } + + virtual void disable() override + { + Logger::info("MCPServer disabling"); + m_enabled = false; + StopMCPServer(); + } + + virtual bool is_enabled() override + { + return m_enabled; + } + + virtual void destroy() override + { + StopMCPServer(); + delete this; + } + + MCPServerModuleInterface() + { + app_name = L"MCP Server"; + app_key = NonLocalizable::ModuleKey; + m_port = 8080; + m_auto_start = true; + m_enable_tools = true; + m_enable_resources = true; + m_transport = L"http"; + init_settings(); + } + +private: + void StartMCPServer() + { + if (m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT) + { + return; // Already running + } + + std::wstring executable_args = L"--port=" + std::to_wstring(m_port); + + if (!m_enable_tools) + { + executable_args += L" --disable-tools"; + } + + if (!m_enable_resources) + { + executable_args += L" --disable-resources"; + } + + if (!m_transport.empty()) + { + executable_args += L" --transport=" + m_transport; + } + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = NonLocalizable::ModulePath; + sei.nShow = SW_HIDE; + sei.lpParameters = executable_args.data(); + + if (ShellExecuteExW(&sei)) + { + m_hProcess = sei.hProcess; + Logger::info("MCPServer started successfully on port {} with transport {}", m_port, std::string(m_transport.begin(), m_transport.end())); + } + else + { + Logger::error("Failed to start MCPServer"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + } + } + + void StopMCPServer() + { + if (m_hProcess) + { + TerminateProcess(m_hProcess, 0); + CloseHandle(m_hProcess); + m_hProcess = nullptr; + Logger::info("MCPServer stopped"); + } + } + + bool is_process_running() + { + return m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void init_settings() + { + try + { + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(get_key()); + + if (auto port = settings.get_int_value(L"port")) + { + m_port = port.value(); + } + + if (auto auto_start = settings.get_bool_value(L"auto_start")) + { + m_auto_start = auto_start.value(); + } + + if (auto enable_tools = settings.get_bool_value(L"enable_tools")) + { + m_enable_tools = enable_tools.value(); + } + + if (auto enable_resources = settings.get_bool_value(L"enable_resources")) + { + m_enable_resources = enable_resources.value(); + } + + if (auto transport = settings.get_string_value(L"transport")) + { + m_transport = transport.value(); + } + } + catch (std::exception&) + { + Logger::warn(L"MCPServer settings file not found, using defaults"); + } + } + + std::wstring app_name; + std::wstring app_key; + bool m_enabled = false; + HANDLE m_hProcess = nullptr; + int m_port = 8080; + bool m_auto_start = true; + bool m_enable_tools = true; + bool m_enable_resources = true; + std::wstring m_transport = L"http"; +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new MCPServerModuleInterface(); +} \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServerModuleInterface/pch.cpp b/src/modules/MCPServer/MCPServerModuleInterface/pch.cpp new file mode 100644 index 0000000000..17305716aa --- /dev/null +++ b/src/modules/MCPServer/MCPServerModuleInterface/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServerModuleInterface/pch.h b/src/modules/MCPServer/MCPServerModuleInterface/pch.h new file mode 100644 index 0000000000..09a424638d --- /dev/null +++ b/src/modules/MCPServer/MCPServerModuleInterface/pch.h @@ -0,0 +1,14 @@ +#pragma once + +#include "targetver.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/src/modules/MCPServer/MCPServerModuleInterface/targetver.h b/src/modules/MCPServer/MCPServerModuleInterface/targetver.h new file mode 100644 index 0000000000..5b1f29cad0 --- /dev/null +++ b/src/modules/MCPServer/MCPServerModuleInterface/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include \ No newline at end of file