commit 27da31b9f70b9121c41f3b693e6fa8862f0cc5c0 parent 02e720c43cdedd07890799703327efd00d4ff5a5 Author: KEINOS <github+fork-qiita-news@keinos.com> Date: Sat, 26 Jul 2025 17:28:20 +0000 Merge remote-tracking branch 'upstream/master' Diffstat:
63 files changed, 1257 insertions(+), 80 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -167,6 +167,7 @@ MAKEINTRESOURCE makemsix MANIFESTSCHEMA MANIFESTVERSION +mcp Memberwise meme metadatas diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt @@ -104,4 +104,5 @@ ^src/SfsClient/ ^src/Xlang/ ^src/VcpkgPortOverlay/ +^src/WinGetMCPServer/WinGetMCPServer.csproj$ ignore$ diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md @@ -1,5 +1,5 @@ ## New in v1.12 -*No applicable update found* +MCP server available; run `winget mcp` for assistance on configuring your client. ## Experimental Features * Experimental support for Fonts diff --git a/doc/admx/DesktopAppInstaller.admx b/doc/admx/DesktopAppInstaller.admx @@ -176,6 +176,16 @@ <decimal value="0" /> </disabledValue> </policy> + <policy name="EnableWindowsPackageManagerMcpServer" class="Machine" displayName="$(string.EnableWindowsPackageManagerMcpServer)" explainText="$(string.EnableWindowsPackageManagerMcpServerExplanation)" key="Software\Policies\Microsoft\Windows\AppInstaller" valueName="EnableWindowsPackageManagerMcpServer"> + <parentCategory ref="AppInstaller" /> + <supportedOn ref="windows:SUPPORTED_Windows_10_0_RS5" /> + <enabledValue> + <decimal value="1" /> + </enabledValue> + <disabledValue> + <decimal value="0" /> + </disabledValue> + </policy> <policy name="WindowsPackageManagerDefaultProxy" class="Machine" displayName="$(string.WindowsPackageManagerDefaultProxy)" explainText="$(string.WindowsPackageManagerDefaultProxyExplanation)" presentation="$(presentation.WindowsPackageManagerDefaultProxy)" key="Software\Policies\Microsoft\Windows\AppInstaller"> <parentCategory ref="AppInstaller" /> <supportedOn ref="windows:SUPPORTED_Windows_10_0_RS5" /> diff --git a/doc/admx/en-US/DesktopAppInstaller.adml b/doc/admx/en-US/DesktopAppInstaller.adml @@ -116,6 +116,13 @@ If you disable this setting, users will not be able to use the Windows Package M If you enable this setting, users will be able to configure the Windows Package Manager's use of proxy through the command line. If you disable or do not configure this setting, users will not be able to to configure the Windows Package Manager's use of proxy through the command line.</string> + <string id="EnableWindowsPackageManagerMcpServer">Enable Windows Package Manager MCP Server</string> + <string id="EnableWindowsPackageManagerMcpServerExplanation"> + This policy controls whether the Windows Package Manager Model Context Protocol (MCP) server can be used. + + If you enable or do not configure this setting, users will be able to use the Windows Package Manager's MCP server. + + If you disable this setting, users will not be able to to use the Windows Package Manager's MCP server.</string> <string id="WindowsPackageManagerDefaultProxy">Set Windows Package Manager Default Proxy</string> <string id="WindowsPackageManagerDefaultProxyExplanation">This policy controls the default proxy used by the Windows Package Manager. diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -209,6 +209,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "targets", "targets", "{A0B4 EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PureLib", "PureLib\PureLib.vcxitems", "{A33223D2-550B-4D99-A53D-488B1F68683E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetMCPServer", "WinGetMCPServer\WinGetMCPServer.csproj", "{33745E4A-39E2-676F-7E23-50FB43848D25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -966,6 +968,30 @@ Global {9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x64.Build.0 = ReleaseStatic|Any CPU {9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x86.ActiveCfg = ReleaseStatic|Any CPU {9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x86.Build.0 = ReleaseStatic|Any CPU + {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|ARM64.ActiveCfg = Debug|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|ARM64.Build.0 = Debug|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|x64.ActiveCfg = Debug|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|x64.Build.0 = Debug|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|x86.ActiveCfg = Debug|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|x86.Build.0 = Debug|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Fuzzing|ARM64.ActiveCfg = Release|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Fuzzing|ARM64.Build.0 = Release|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Fuzzing|x64.ActiveCfg = Release|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Fuzzing|x64.Build.0 = Release|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Fuzzing|x86.ActiveCfg = Release|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Fuzzing|x86.Build.0 = Release|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Release|ARM64.ActiveCfg = Release|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Release|ARM64.Build.0 = Release|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Release|x64.ActiveCfg = Release|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Release|x64.Build.0 = Release|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Release|x86.ActiveCfg = Release|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.Release|x86.Build.0 = Release|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|ARM64.ActiveCfg = Release|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|ARM64.Build.0 = Release|arm64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x64.ActiveCfg = Release|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x64.Build.0 = Release|x64 + {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x86.ActiveCfg = Release|x86 + {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -325,6 +325,7 @@ <ClInclude Include="Commands\FeaturesCommand.h" /> <ClInclude Include="Commands\HashCommand.h" /> <ClInclude Include="Commands\ListCommand.h" /> + <ClInclude Include="Commands\McpCommand.h" /> <ClInclude Include="Commands\PinCommand.h" /> <ClInclude Include="Commands\RepairCommand.h" /> <ClInclude Include="Commands\SearchCommand.h" /> @@ -409,6 +410,7 @@ <ClCompile Include="Commands\ErrorCommand.cpp" /> <ClCompile Include="Commands\FontCommand.cpp" /> <ClCompile Include="Commands\ImportCommand.cpp" /> + <ClCompile Include="Commands\McpCommand.cpp" /> <ClCompile Include="Commands\PinCommand.cpp" /> <ClCompile Include="Commands\RepairCommand.cpp" /> <ClCompile Include="Commands\TestCommand.cpp" /> @@ -504,4 +506,4 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorTextNuget)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorTextNuget)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets'))" /> </Target> -</Project> +</Project>+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -293,6 +293,9 @@ <ClInclude Include="Commands\DscAdminSettingsResource.h"> <Filter>Commands\Configuration</Filter> </ClInclude> + <ClInclude Include="Commands\McpCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -553,6 +556,9 @@ <ClCompile Include="Commands\DscAdminSettingsResource.cpp"> <Filter>Commands\Configuration</Filter> </ClCompile> + <ClCompile Include="Commands\McpCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -211,9 +211,9 @@ namespace AppInstaller::CLI return { type, "accept-configuration-agreements"_liv }; case Execution::Args::Type::ConfigurationSuppressPrologue: return { type, "suppress-initial-details"_liv }; - case Execution::Args::Type::ConfigurationEnable: + case Execution::Args::Type::ExtendedFeaturesEnable: return { type, "enable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; - case Execution::Args::Type::ConfigurationDisable: + case Execution::Args::Type::ExtendedFeaturesDisable: return { type, "disable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; case Execution::Args::Type::ConfigurationModulePath: return { type, "module-path"_liv }; diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -41,8 +41,8 @@ namespace AppInstaller::CLI Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, Argument{ Execution::Args::Type::ConfigurationSuppressPrologue, Resource::String::ConfigurationSuppressPrologueArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }, - Argument{ Execution::Args::Type::ConfigurationEnable, Resource::String::ConfigurationEnableMessage, ArgumentType::Flag, Argument::Visibility::Help }, - Argument{ Execution::Args::Type::ConfigurationDisable, Resource::String::ConfigurationDisableMessage, ArgumentType::Flag, Argument::Visibility::Help }, + Argument{ Execution::Args::Type::ExtendedFeaturesEnable, Resource::String::ExtendedFeaturesEnableMessage, ArgumentType::Flag, Argument::Visibility::Help }, + Argument{ Execution::Args::Type::ExtendedFeaturesDisable, Resource::String::ExtendedFeaturesDisableMessage, ArgumentType::Flag, Argument::Visibility::Help }, }; } @@ -63,15 +63,15 @@ namespace AppInstaller::CLI void ConfigureCommand::ExecuteInternal(Execution::Context& context) const { - if (context.Args.Contains(Execution::Args::Type::ConfigurationEnable)) + if (context.Args.Contains(Execution::Args::Type::ExtendedFeaturesEnable)) { context << - EnableConfiguration; + EnableExtendedFeatures; } - else if (context.Args.Contains(Execution::Args::Type::ConfigurationDisable)) + else if (context.Args.Contains(Execution::Args::Type::ExtendedFeaturesDisable)) { context << - DisableConfiguration; + DisableExtendedFeatures; } else { @@ -90,12 +90,12 @@ namespace AppInstaller::CLI void ConfigureCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { - if (execArgs.Contains(Execution::Args::Type::ConfigurationEnable) || - execArgs.Contains(Execution::Args::Type::ConfigurationDisable)) + if (execArgs.Contains(Execution::Args::Type::ExtendedFeaturesEnable) || + execArgs.Contains(Execution::Args::Type::ExtendedFeaturesDisable)) { if (execArgs.GetArgsCount() > 1) { - throw CommandException(Resource::String::ConfigurationEnableArgumentError); + throw CommandException(Resource::String::ExtendedFeaturesEnableArgumentError); } } else diff --git a/src/AppInstallerCLICore/Commands/ConfigureListCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureListCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureListCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureShowCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureTestCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureValidateCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; diff --git a/src/AppInstallerCLICore/Commands/McpCommand.cpp b/src/AppInstallerCLICore/Commands/McpCommand.cpp @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "McpCommand.h" +#include "Workflows/MSStoreInstallerHandler.h" +#include <AppInstallerRuntime.h> + +using namespace AppInstaller::CLI::Workflow; + +namespace AppInstaller::CLI +{ + McpCommand::McpCommand(std::string_view parent) : + Command("mcp", {}, parent, Settings::TogglePolicy::Policy::McpServer) + { + } + + std::vector<Argument> McpCommand::GetArguments() const + { + return { + Argument{ Execution::Args::Type::ExtendedFeaturesEnable, Resource::String::ExtendedFeaturesEnableMessage, ArgumentType::Flag, Argument::Visibility::Help }, + Argument{ Execution::Args::Type::ExtendedFeaturesDisable, Resource::String::ExtendedFeaturesDisableMessage, ArgumentType::Flag, Argument::Visibility::Help }, + }; + } + + Resource::LocString McpCommand::ShortDescription() const + { + return { Resource::String::McpCommandShortDescription }; + } + + Resource::LocString McpCommand::LongDescription() const + { + return { Resource::String::McpCommandLongDescription }; + } + + Utility::LocIndView McpCommand::HelpLink() const + { + return "https://aka.ms/winget-command-mcp"_liv; + } + + void McpCommand::ExecuteInternal(Execution::Context& context) const + { + if (context.Args.Contains(Execution::Args::Type::ExtendedFeaturesEnable)) + { + context << + EnableExtendedFeatures; + } + else if (context.Args.Contains(Execution::Args::Type::ExtendedFeaturesDisable)) + { + context << + DisableExtendedFeatures; + } + else + { + context << + VerifyIsFullPackage << + [](Execution::Context& context) + { + std::filesystem::path exePath = Runtime::GetPathTo(Runtime::PathName::MCPExecutable); + std::string jsonCompatiblePath = exePath.u8string(); + Utility::FindAndReplace(jsonCompatiblePath, "\\", "\\\\"); + + context.Reporter.Info() << Resource::String::McpConfigurationPreamble << + R"( + "winget-mcp": { + "type": "stdio", + "command": ")" << jsonCompatiblePath << R"(" + })" << std::endl; + }; + } + } + + void McpCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::ExtendedFeaturesEnable) || + execArgs.Contains(Execution::Args::Type::ExtendedFeaturesDisable)) + { + if (execArgs.GetArgsCount() > 1) + { + throw CommandException(Resource::String::ExtendedFeaturesEnableArgumentError); + } + } + } +} diff --git a/src/AppInstallerCLICore/Commands/McpCommand.h b/src/AppInstallerCLICore/Commands/McpCommand.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct McpCommand final : public Command + { + McpCommand(std::string_view parent); + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -29,6 +29,7 @@ #include "ResumeCommand.h" #include "RepairCommand.h" #include "DscCommand.h" +#include "McpCommand.h" #include "Resources.h" #include "TableOutput.h" @@ -199,6 +200,7 @@ namespace AppInstaller::CLI std::make_unique<RepairCommand>(FullName()), std::make_unique<FontCommand>(FullName()), std::make_unique<DscCommand>(FullName()), + std::make_unique<McpCommand>(FullName()), #if _DEBUG std::make_unique<DebugCommand>(FullName()), #endif diff --git a/src/AppInstallerCLICore/Commands/TestCommand.cpp b/src/AppInstallerCLICore/Commands/TestCommand.cpp @@ -9,6 +9,7 @@ #include "TableOutput.h" #include "Public/ConfigurationSetProcessorFactoryRemoting.h" #include "Workflows/ConfigurationFlow.h" +#include "Workflows/MSStoreInstallerHandler.h" #include <winrt/Microsoft.Management.Configuration.h> using namespace AppInstaller::CLI::Workflow; diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureExportCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -125,12 +125,14 @@ namespace AppInstaller::CLI::Execution // Font Command Family, + // Stub package (extended features) + ExtendedFeaturesEnable, + ExtendedFeaturesDisable, + // Configuration ConfigurationFile, ConfigurationAcceptWarning, ConfigurationSuppressPrologue, - ConfigurationEnable, - ConfigurationDisable, ConfigurationProcessorPath, ConfigurationModulePath, ConfigurationExportPackageId, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -61,13 +61,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAssert); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDescriptionWasTruncated); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisabledMessage); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisableMessage); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisablingMessage); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableArgumentError); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnabledMessage); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableMessage); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnablingMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportAddingToFile); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailed); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailedToGetUnitProcessors); @@ -106,7 +99,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModulePathArgError); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModules); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModuleWithDetails); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNotEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNoTestRun); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNotInDesiredState); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationProcessorPath); @@ -276,6 +268,14 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExportedPackageRequiresLicenseAgreement); WINGET_DEFINE_RESOURCE_STRINGID(ExportIncludeVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesDisabledMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesDisableMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesDisablingMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesEnableArgumentError); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesEnabledMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesEnableMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesEnablingMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ExtendedFeaturesNotEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ExternalDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveFailed); WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveSucceeded); @@ -428,6 +428,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ManifestValidationFail); WINGET_DEFINE_RESOURCE_STRINGID(ManifestValidationSuccess); WINGET_DEFINE_RESOURCE_STRINGID(ManifestValidationWarning); + WINGET_DEFINE_RESOURCE_STRINGID(McpCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(McpCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(McpConfigurationPreamble); WINGET_DEFINE_RESOURCE_STRINGID(MissingArgumentError); WINGET_DEFINE_RESOURCE_STRINGID(ModifiedPathRequiresShellRestart); WINGET_DEFINE_RESOURCE_STRINGID(MonikerArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -2152,15 +2152,6 @@ namespace AppInstaller::CLI::Workflow } } - void VerifyIsFullPackage(Execution::Context& context) - { - if (IsStubPackage()) - { - context.Reporter.Error() << Resource::String::ConfigurationNotEnabledMessage << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_IS_STUB); - } - } - void ValidateConfigurationSetSemantics(Execution::Context& context) { ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -77,12 +77,6 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void TestConfigurationSet(Execution::Context& context); - // Verifies the full package is installed. - // Required Args: None - // Inputs: None - // Outputs: None - void VerifyIsFullPackage(Execution::Context& context); - // Validates the configuration set semantically. // Required Args: None // Inputs: ConfigurationProcessor, ConfigurationSet diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -418,38 +418,47 @@ namespace AppInstaller::CLI::Workflow } } - void EnableConfiguration(Execution::Context& context) + void VerifyIsFullPackage(Execution::Context& context) + { + if (IsStubPackage()) + { + context.Reporter.Error() << Resource::String::ExtendedFeaturesNotEnabledMessage << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_IS_STUB); + } + } + + void EnableExtendedFeatures(Execution::Context& context) { #ifndef AICLI_DISABLE_TEST_HOOKS AppInstallerUpdate(false, true, context); #else if (IsStubPackage()) { - context.Reporter.Info() << Resource::String::ConfigurationEnablingMessage << std::endl; + context.Reporter.Info() << Resource::String::ExtendedFeaturesEnablingMessage << std::endl; bool bypassStorePolicy = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::BypassIsStoreClientBlockedPolicyCheck); AppInstallerUpdate(false, bypassStorePolicy, context); } else { - context.Reporter.Info() << Resource::String::ConfigurationEnabledMessage << std::endl; + context.Reporter.Info() << Resource::String::ExtendedFeaturesEnabledMessage << std::endl; } #endif } - void DisableConfiguration(Execution::Context& context) + void DisableExtendedFeatures(Execution::Context& context) { #ifndef AICLI_DISABLE_TEST_HOOKS AppInstallerUpdate(true, true, context); #else if (!IsStubPackage()) { - context.Reporter.Info() << Resource::String::ConfigurationDisablingMessage << std::endl; + context.Reporter.Info() << Resource::String::ExtendedFeaturesDisablingMessage << std::endl; bool bypassStorePolicy = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::BypassIsStoreClientBlockedPolicyCheck); AppInstallerUpdate(true, bypassStorePolicy, context); } else { - context.Reporter.Info() << Resource::String::ConfigurationDisabledMessage << std::endl; + context.Reporter.Info() << Resource::String::ExtendedFeaturesDisabledMessage << std::endl; } #endif } diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h @@ -36,19 +36,21 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void EnsureStorePolicySatisfied(Execution::Context& context); + // Verifies the full package is installed. + // Required Args: None + // Inputs: None + // Outputs: None + void VerifyIsFullPackage(Execution::Context& context); + // Change stub preference to full and installs full package if needed. - // This should go into configuration flow once installing from the store is - // moved out of this work flow. // Required Args: None // Inputs: None // Outputs: None - void EnableConfiguration(Execution::Context& context); + void EnableExtendedFeatures(Execution::Context& context); // Change stub preference to stub and installs stub package if needed. - // This should go into configuration flow once installing from the store is - // moved out of this work flow. // Required Args: None // Inputs: None // Outputs: None - void DisableConfiguration(Execution::Context& context); + void DisableExtendedFeatures(Execution::Context& context); } diff --git a/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj b/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj @@ -246,6 +246,13 @@ <PackagePath>ConfigurationRemotingServer</PackagePath> <Recurse>true</Recurse> </WinGetAdditionalPackageFile> + <WinGetAdditionalPackageFile Include="$(WinGetAdditionalPackageFileRoot)\$(PlatformTarget)\$(Configuration)\WinGetMCPServer\net8.0-windows10.0.26100.0\$(ConfigServerRid)\**\*"> + <PackagePath>WinGetMCPServer</PackagePath> + <Recurse>true</Recurse> + </WinGetAdditionalPackageFile> + <WinGetAdditionalPackageFile Include="$(WinGetAdditionalPackageFileRoot)\$(PlatformTarget)\$(Configuration)\Microsoft.Management.Deployment.OutOfProc\Microsoft.Management.Deployment.OutOfProc.dll"> + <PackagePath>WinGetMCPServer\Microsoft.Management.Deployment.dll</PackagePath> + </WinGetAdditionalPackageFile> <WinGetAdditionalPackageFile Include="$(SolutionDir)\PowerShell\ExternalModules\**\*"> <PackagePath>ExternalModules</PackagePath> <Recurse>true</Recurse> diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -97,6 +97,18 @@ </com:Extension> </Extensions> </Application> + <Application Id="WinGetMCPServer" Executable="WinGetMCPServer\WinGetMCPServer.exe" EntryPoint="Windows.FullTrustApplication"> + <uap:VisualElements DisplayName="WinGet Dev MCP Server" Square150x150Logo="Images\MedTile.png" Square44x44Logo="Images\AppList.png" Description="The WinGet MCP server." BackgroundColor="#0078d7" AppListEntry="none" > + <uap:DefaultTile/> + </uap:VisualElements > + <Extensions> + <uap5:Extension Category="windows.appExecutionAlias"> + <uap5:AppExecutionAlias> + <uap5:ExecutionAlias Alias="WindowsPackageManagerMCPServerDev.exe" /> + </uap5:AppExecutionAlias> + </uap5:Extension> + </Extensions> + </Application> </Applications> <Extensions> <Extension Category="windows.activatableClass.proxyStub"> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1975,30 +1975,31 @@ Please specify one of them using the --source option to proceed.</value> <data name="InstallFullPackageDescription" xml:space="preserve"> <value>Install the full package for AppInstaller</value> </data> - <data name="ConfigurationEnableMessage" xml:space="preserve"> - <value>Enable configuration components. Requires store access.</value> + <data name="ExtendedFeaturesEnableMessage" xml:space="preserve"> + <value>Enable extended features. Requires store access.</value> </data> - <data name="ConfigurationEnableArgumentError" xml:space="preserve"> + <data name="ExtendedFeaturesEnableArgumentError" xml:space="preserve"> <value>Option '--enable' and '--disable' cannot be used with other arguments.</value> <comment>{Locked="--enable", "--disable"}</comment> </data> - <data name="ConfigurationEnablingMessage" xml:space="preserve"> - <value>Enabling configuration components. Requires store access.</value> + <data name="ExtendedFeaturesEnablingMessage" xml:space="preserve"> + <value>Enabling extended features. Requires store access.</value> </data> - <data name="ConfigurationNotEnabledMessage" xml:space="preserve"> - <value>Configuration is not enabled. Run `winget configure --enable` to enable it.</value> + <data name="ExtendedFeaturesNotEnabledMessage" xml:space="preserve"> + <value>Extended features are not enabled. Run `winget configure --enable` to enable them.</value> + <comment>{Locked="winget configure --enable"}</comment> </data> - <data name="ConfigurationEnabledMessage" xml:space="preserve"> - <value>Configuration is enabled.</value> + <data name="ExtendedFeaturesEnabledMessage" xml:space="preserve"> + <value>Extended features are enabled.</value> </data> - <data name="ConfigurationDisableMessage" xml:space="preserve"> - <value>Disable configuration components. Requires store access.</value> + <data name="ExtendedFeaturesDisableMessage" xml:space="preserve"> + <value>Disable extended features. Requires store access.</value> </data> - <data name="ConfigurationDisablingMessage" xml:space="preserve"> - <value>Disabling configuration components. Requires store access.</value> + <data name="ExtendedFeaturesDisablingMessage" xml:space="preserve"> + <value>Disabling extended features. Requires store access.</value> </data> - <data name="ConfigurationDisabledMessage" xml:space="preserve"> - <value>Configuration is disabled.</value> + <data name="ExtendedFeaturesDisabledMessage" xml:space="preserve"> + <value>Extended features are disabled.</value> </data> <data name="SkipDependenciesArgumentDescription" xml:space="preserve"> <value>Skips processing package dependencies and Windows features</value> @@ -3370,4 +3371,20 @@ Please specify one of them using the --source option to proceed.</value> <data name="ResetAllAdminSettingsSucceeded" xml:space="preserve"> <value>All admin settings reset.</value> </data> + <data name="McpCommandShortDescription" xml:space="preserve"> + <value>MCP information</value> + <comment>MCP stands for Model Context Protocol and should probably remain as-is</comment> + </data> + <data name="McpCommandLongDescription" xml:space="preserve"> + <value>MCP (Model Context Protocol) information for the Windows Package Manager.</value> + </data> + <data name="McpConfigurationPreamble" xml:space="preserve"> + <value>To manually configure the Windows Package Manager MCP server with your MCP client, use the following JSON fragment in the `servers` object:</value> + <comment>{Locked="`servers`"} +MCP stands for Model Context Protocol and should probably remain as-is. +An unlocalized JSON fragment will follow on another line.</comment> + </data> + <data name="PolicyEnableMcpServer" xml:space="preserve"> + <value>Enable Windows Package Manager MCP Server</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/Filesystem.cpp b/src/AppInstallerCLITests/Filesystem.cpp @@ -103,3 +103,13 @@ TEST_CASE("ReplaceCommonPathPrefix", "[filesystem]") REQUIRE(!ReplaceCommonPathPrefix(shouldNotReplace, prefix, replacement)); REQUIRE(shouldNotReplace.u8string() == "C:\\test1\\test3\\subdir1\\subdir2"); } + +TEST_CASE("GetExecutablePathForProcess", "[filesystem]") +{ + std::filesystem::path thisExecutable = GetExecutablePathForProcess(GetCurrentProcess()); + REQUIRE(!thisExecutable.empty()); + REQUIRE(thisExecutable.is_absolute()); + REQUIRE(thisExecutable.has_filename()); + REQUIRE(thisExecutable.has_extension()); + REQUIRE(thisExecutable.filename() == L"AppInstallerCLITests.exe"); +} diff --git a/src/AppInstallerCLITests/GroupPolicy.cpp b/src/AppInstallerCLITests/GroupPolicy.cpp @@ -402,6 +402,7 @@ TEST_CASE("GroupPolicy_AllEnabled", "[groupPolicy]") SetRegistryValue(policiesKey.get(), EnableWindowsPackageManagerCommandLineInterfaces, 1); SetRegistryValue(policiesKey.get(), ConfigurationPolicyValueName, 1); SetRegistryValue(policiesKey.get(), ProxyCommandLineOptionsPolicyValueName, 1); + SetRegistryValue(policiesKey.get(), McpServerValueName, 1); GroupPolicy groupPolicy{ policiesKey.get() }; for (const auto& policy : TogglePolicy::GetAllPolicies()) diff --git a/src/AppInstallerCLITests/TestSettings.h b/src/AppInstallerCLITests/TestSettings.h @@ -23,6 +23,7 @@ namespace TestCommon const std::wstring EnableWindowsPackageManagerCommandLineInterfaces = L"EnableWindowsPackageManagerCommandLineInterfaces"; const std::wstring ConfigurationPolicyValueName = L"EnableWindowsPackageManagerConfiguration"; const std::wstring ProxyCommandLineOptionsPolicyValueName = L"EnableWindowsPackageManagerProxyCommandLineOptions"; + const std::wstring McpServerValueName = L"EnableWindowsPackageManagerMcpServer"; const std::wstring SourceUpdateIntervalPolicyValueName = L"SourceAutoUpdateInterval"; const std::wstring SourceUpdateIntervalPolicyOldValueName = L"SourceAutoUpdateIntervalInMinutes"; diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -16,7 +16,7 @@ g_hTraceProvider,\ _eventName_,\ s_useGlobalTelemetryActivityId ? &s_globalTelemetryLoggerActivityId : GetActivityId(),\ -nullptr,\ +s_useGlobalTelemetryActivityId ? nullptr : GetParentActivityId(),\ TraceLoggingCountedUtf8String(m_caller.c_str(), static_cast<ULONG>(m_caller.size()), "Caller"),\ TraceLoggingPackedFieldEx(m_telemetryCorrelationJsonW.c_str(), static_cast<ULONG>((m_telemetryCorrelationJsonW.size() + 1) * sizeof(wchar_t)), TlgInUNICODESTRING, TlgOutJSON, "CvJson"),\ __VA_ARGS__) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -56,6 +56,8 @@ namespace AppInstaller::Runtime CheckpointsLocation, // The location of the CLI executable file. CLIExecutable, + // The directory containing the CLI executable file. + MCPExecutable, // The location of the image assets, if it exists. ImageAssets, // The location where fonts are installed with user scope. diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -50,6 +50,8 @@ namespace AppInstaller::Runtime constexpr std::string_view s_WindowsApps_Base = "Microsoft\\WindowsApps"sv; constexpr std::string_view s_WinGetDev_Exe = "wingetdev.exe"; constexpr std::string_view s_WinGet_Exe = "winget.exe"; + constexpr std::string_view s_WinGetMCPDev_Exe = "WindowsPackageManagerMCPServerDev.exe"; + constexpr std::string_view s_WinGetMCP_Exe = "WindowsPackageManagerMCPServer.exe"; static std::optional<std::string> s_runtimePathStateName; static wil::srwlock s_runtimePathStateNameLock; @@ -344,14 +346,28 @@ namespace AppInstaller::Runtime result.Path /= s_CheckpointsDirectory; break; case PathName::CLIExecutable: + case PathName::MCPExecutable: result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); result.Path /= s_WindowsApps_Base; result.Path /= GetPackageFamilyName(); + + if (path == PathName::CLIExecutable) + { #if USE_PROD_CLSIDS - result.Path /= s_WinGet_Exe; + result.Path /= s_WinGet_Exe; #else - result.Path /= s_WinGetDev_Exe; + result.Path /= s_WinGetDev_Exe; #endif + } + else if (path == PathName::MCPExecutable) + { +#if USE_PROD_CLSIDS + result.Path /= s_WinGetMCP_Exe; +#else + result.Path /= s_WinGetMCPDev_Exe; +#endif + } + result.Create = false; mayBeInProfilePath = true; break; @@ -439,6 +455,7 @@ namespace AppInstaller::Runtime break; case PathName::SelfPackageRoot: case PathName::CLIExecutable: + case PathName::MCPExecutable: case PathName::ImageAssets: result.Path = GetBinaryDirectoryPath(); result.Create = false; @@ -446,6 +463,10 @@ namespace AppInstaller::Runtime { result.Path /= s_WinGet_Exe; } + else if (path == PathName::MCPExecutable) + { + result.Path /= s_WinGetMCP_Exe; + } else if (path == PathName::ImageAssets) { result.Path /= s_ImageAssetsDirectoryRelative; diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp @@ -497,4 +497,16 @@ namespace AppInstaller::Filesystem return result; } + + std::filesystem::path GetExecutablePathForProcess(HANDLE process) + { + wil::unique_cotaskmem_string imageName = nullptr; + if (SUCCEEDED(wil::QueryFullProcessImageNameW(process, 0, imageName)) && + (imageName.get() != nullptr)) + { + return imageName.get(); + } + + return {}; + } } diff --git a/src/AppInstallerSharedLib/GroupPolicy.cpp b/src/AppInstallerSharedLib/GroupPolicy.cpp @@ -326,6 +326,8 @@ namespace AppInstaller::Settings return TogglePolicy(policy, "EnableWindowsPackageManagerConfiguration"sv, String::PolicyEnableWinGetConfiguration); case TogglePolicy::Policy::ProxyCommandLineOptions: return TogglePolicy(policy, "EnableWindowsPackageManagerProxyCommandLineOptions"sv, String::PolicyEnableProxyCommandLineOptions); + case TogglePolicy::Policy::McpServer: + return TogglePolicy(policy, "EnableWindowsPackageManagerMcpServer"sv, String::PolicyEnableMcpServer); default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -119,4 +119,7 @@ namespace AppInstaller::Filesystem // Gets the PathDetails used for the given path. // This is exposed primarily to allow for testing, GetPathTo should be preferred. PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); + + // Gets the path to the executable for the given process. + std::filesystem::path GetExecutablePathForProcess(HANDLE process); } diff --git a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h @@ -47,6 +47,7 @@ namespace AppInstaller::Settings WinGetCommandLineInterfaces, Configuration, ProxyCommandLineOptions, + McpServer, Max, }; diff --git a/src/AppInstallerSharedLib/Public/winget/Resources.h b/src/AppInstallerSharedLib/Public/winget/Resources.h @@ -60,6 +60,7 @@ namespace AppInstaller WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWindowsPackageManagerCommandLineInterfaces); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWinGetConfiguration); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableProxyCommandLineOptions); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableMcpServer); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldFormat); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldValue); diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props @@ -1,4 +1,4 @@ -<Project> +<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <!-- @@ -10,6 +10,7 @@ <PackageVersion Include="coverlet.collector" Version="3.1.2" /> <PackageVersion Include="Microsoft.ApplicationInsights" Version="2.21.0" /> <PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" /> + <PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.6" /> <PackageVersion Include="Microsoft.CSharp" Version="4.7.0" /> <PackageVersion Include="Microsoft.Msix.Utils" Version="2.1.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> @@ -17,6 +18,7 @@ <PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.1.6" /> <PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.26100.1742" /> + <PackageVersion Include="ModelContextProtocol" Version="0.2.0-preview.3" /> <PackageVersion Include="Moq" Version="4.18.2" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="nunit" Version="3.12.0" /> diff --git a/src/Microsoft.Management.Deployment/Helpers.cpp b/src/Microsoft.Management.Deployment/Helpers.cpp @@ -6,7 +6,10 @@ #include <winrt/Windows.Security.Authorization.AppCapabilityAccess.h> #include <appmodel.h> #include <Helpers.h> +#include <winget/Filesystem.h> #include <winget/Security.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerLogging.h> using namespace std::string_literals; using namespace std::string_view_literals; @@ -135,15 +138,22 @@ namespace winrt::Microsoft::Management::Deployment::implementation UINT32 length = ARRAYSIZE(packageFamilyName); if (::GetPackageFamilyName(processHandle.get(), &length, packageFamilyName) == ERROR_SUCCESS) { - return { packageFamilyName }; + // If the package is calling into itself, fall through to the executable name + if (AppInstaller::Runtime::GetPackageFamilyName() != packageFamilyName) + { + return { packageFamilyName }; + } } // if the caller doesn't have an AppUserModelID then fall back to the executable name - wil::unique_cotaskmem_string imageName = nullptr; - if (SUCCEEDED(wil::QueryFullProcessImageNameW(processHandle.get(), 0, imageName)) && - (imageName.get() != nullptr)) + std::filesystem::path executablePath = AppInstaller::Filesystem::GetExecutablePathForProcess(processHandle.get()); + if (executablePath.has_filename()) + { + return executablePath.filename(); + } + else if (!executablePath.empty()) { - return imageName.get(); + AICLI_LOG(Fail, Error, << "Unable to get valid executable for process ID [" << callerProcessId << "]: " << executablePath); } } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1408,5 +1408,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation co_return GetRemovePackageCatalogResult(terminationHR); } + winrt::hstring PackageManager::Version() const + { + return winrt::hstring{ AppInstaller::Utility::ConvertToUTF16(AppInstaller::Runtime::GetClientVersion()) }; + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManager); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h @@ -49,6 +49,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation AddPackageCatalogAsync(winrt::Microsoft::Management::Deployment::AddPackageCatalogOptions options); winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::RemovePackageCatalogResult, double> RemovePackageCatalogAsync(winrt::Microsoft::Management::Deployment::RemovePackageCatalogOptions options); + // Contract 13.0 + winrt::hstring Version() const; }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(12)] // For version 1.10 + [contractversion(13)] // For version 1.12 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -1569,6 +1569,12 @@ namespace Microsoft.Management.Deployment // Repair the specified package Windows.Foundation.IAsyncOperationWithProgress<RepairResult, RepairProgress> RepairPackageAsync(CatalogPackage package, RepairOptions options); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 13)] + { + // The version of the Windows Package Manager that is running. + String Version{ get; }; + } } /// Global settings for PackageManager operations. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/VersionCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/VersionCommand.cs @@ -14,7 +14,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <summary> /// Version commands. /// </summary> - public sealed class VersionCommand : BaseCommand + public sealed class VersionCommand : ManagementDeploymentCommand { /// <summary> /// Initializes a new instance of the <see cref="VersionCommand"/> class. @@ -30,7 +30,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// </summary> public void Get() { - this.Write(StreamType.Object, WinGetVersion.InstalledWinGetVersion(this).TagVersion); + this.Write(StreamType.Object, this.Execute(() => WinGetVersion.InstalledWinGetVersion(this).TagVersion)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -136,6 +136,22 @@ namespace Microsoft.WinGet.Client.Engine.Helpers false); } + /// <summary> + /// Gets the version of the package manager that is running. + /// </summary> + /// <returns>The version string.</returns> + public string? GetVersion() + { + try + { + return this.Execute(() => this.packageManager.Version, true); + } + catch + { + return null; + } + } + private TReturn Execute<TReturn>(Func<TReturn> func, bool canRetry) { if (Utilities.UsesInProcWinget && Utilities.ThreadIsSTA) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs @@ -71,6 +71,13 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <returns>The WinGetVersion.</returns> public static WinGetVersion InstalledWinGetVersion(PowerShellCmdlet pwshCmdlet) { + // Try getting the version through COM if it is available (user might have an older build installed) + string? comVersion = PackageManagerWrapper.Instance.GetVersion(); + if (comVersion != null) + { + return new WinGetVersion(comVersion); + } + var wingetCliWrapper = new WingetCLIWrapper(); var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); return new WinGetVersion(result.StdOut.Replace(Environment.NewLine, string.Empty)); diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/Enums.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/Enums.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="Enums.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -96,6 +96,11 @@ namespace Microsoft.WinGet.SharedLib.PolicySettings /// Enabled configuration. /// </summary> Configuration, + + /// <summary> + /// Enabled MCP server. + /// </summary> + McpServer, } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/TogglePolicy.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/TogglePolicy.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="TogglePolicy.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -125,6 +125,8 @@ namespace Microsoft.WinGet.SharedLib.PolicySettings return new TogglePolicy(policy, "EnableWindowsPackageManagerCommandLineInterfaces", GroupPolicyResource.PolicyEnableWindowsPackageManagerCommandLineInterfaces); case Policy.Configuration: return new TogglePolicy(policy, "EnableWindowsPackageManagerConfiguration", GroupPolicyResource.PolicyEnableWinGetConfiguration); + case Policy.McpServer: + return new TogglePolicy(policy, "EnableWindowsPackageManagerMcpServer", GroupPolicyResource.PolicyEnableMcpServer); default: throw new ArgumentException(null, nameof(policy)); } diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.Designer.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.Designer.cs @@ -142,6 +142,15 @@ namespace Microsoft.WinGet.SharedLib.Resources { } /// <summary> + /// Looks up a localized string similar to Enable Windows Package Manager MCP Server. + /// </summary> + internal static string PolicyEnableMcpServer { + get { + return ResourceManager.GetString("PolicyEnableMcpServer", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Enable Windows App Installer Microsoft Store Source. /// </summary> internal static string PolicyEnableMSStoreSource { diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.resx b/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.resx @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <root> <!-- Microsoft ResX Schema @@ -144,6 +144,9 @@ <data name="PolicyEnableLocalManifests" xml:space="preserve"> <value>Enable Windows App Installer Local Manifest Files</value> </data> + <data name="PolicyEnableMcpServer" xml:space="preserve"> + <value>Enable Windows Package Manager MCP Server</value> + </data> <data name="PolicyEnableMSStoreSource" xml:space="preserve"> <value>Enable Windows App Installer Microsoft Store Source</value> </data> diff --git a/src/WinGetMCPServer/Exceptions/ToolResponseException.cs b/src/WinGetMCPServer/Exceptions/ToolResponseException.cs @@ -0,0 +1,23 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ToolResponseException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Exceptions +{ + using ModelContextProtocol.Protocol; + + /// <summary> + /// An exception that contains a tool response. + /// </summary> + internal class ToolResponseException : Exception + { + public ToolResponseException(CallToolResponse toolResponse) + { + this.Response = toolResponse; + } + + public CallToolResponse Response { get; private set; } + } +} diff --git a/src/WinGetMCPServer/Extensions/PackageListExtensions.cs b/src/WinGetMCPServer/Extensions/PackageListExtensions.cs @@ -0,0 +1,78 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PackageListExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Extensions +{ + using Microsoft.Management.Deployment; + using WinGetMCPServer.Response; + + /// <summary> + /// Extensions for List<FindPackageResult>. + /// </summary> + internal static class PackageListExtensions + { + /// <summary> + /// Adds the packages from a find to the list. + /// </summary> + /// <param name="list">The list to add to.</param> + /// <param name="findResult">A find result.</param> + /// <returns>The list.</returns> + static public List<FindPackageResult> AddPackages(this List<FindPackageResult> list, FindPackagesResult findResult) + { + for (int i = 0; i < findResult.Matches!.Count; ++i) + { + list.Add(FindPackageResultFromCatalogPackage(findResult.Matches[i].CatalogPackage)); + } + + return list; + } + + /// <summary> + /// Creates a <see cref="FindPackageResult"/> from the specified <see cref="CatalogPackage"/>. + /// </summary> + /// <remarks>The method populates the <see cref="FindPackageResult"/> with details such as the + /// package identifier, name, installation status, and update availability. If the package is installed, + /// additional information such as the catalog name and installed location is included.</remarks> + /// <param name="package">The catalog package from which to create the result. Cannot be null.</param> + /// <returns>A <see cref="FindPackageResult"/> representing the state of the specified catalog package, including + /// installation status and available updates.</returns> + static public FindPackageResult FindPackageResultFromCatalogPackage(CatalogPackage package) + { + FindPackageResult findPackageResult = new FindPackageResult() + { + Identifier = package.Id, + Name = package.Name, + }; + + var installedVersion = package.InstalledVersion; + findPackageResult.IsInstalled = installedVersion != null; + + if (installedVersion != null) + { + findPackageResult.Source = installedVersion.PackageCatalog?.Info?.Name; + if (string.IsNullOrEmpty(findPackageResult.Source)) + { + findPackageResult.Source = package.DefaultInstallVersion?.PackageCatalog?.Info?.Name; + } + + findPackageResult.InstalledVersion = installedVersion.Version; + findPackageResult.IsUpdateAvailable = package.IsUpdateAvailable; + + string installLocation = installedVersion.GetMetadata(PackageVersionMetadataField.InstalledLocation); + if (!string.IsNullOrEmpty(installLocation)) + { + findPackageResult.InstalledLocation = installLocation; + } + } + else + { + findPackageResult.Source = package.DefaultInstallVersion.PackageCatalog.Info.Name; + } + + return findPackageResult; + } + } +} diff --git a/src/WinGetMCPServer/Program.cs b/src/WinGetMCPServer/Program.cs @@ -0,0 +1,35 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Program.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer +{ + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using ModelContextProtocol.Protocol; + + internal class Program + { + private const string ServerName = "winget-mcp"; + + static void Main(string[] args) + { + var builder = Host.CreateApplicationBuilder(); + builder.Logging.AddConsole(consoleOptions => { consoleOptions.LogToStandardErrorThreshold = LogLevel.Trace; }); + + builder.Services + .AddMcpServer(configureOptions => + { + // TODO: More options setup? + configureOptions.ServerInfo = new Implementation() { Name = ServerName, Version = ServerConnection.Instance.Version }; + }) + .WithStdioServerTransport() + .WithTools<WingetPackageTools>(); + + builder.Build().Run(); + } + } +} diff --git a/src/WinGetMCPServer/Response/FindPackageResult.cs b/src/WinGetMCPServer/Response/FindPackageResult.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------------- +// <copyright file="FindPackageResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Response +{ + /// <summary> + /// Contains information about found packages. + /// </summary> + internal class FindPackageResult + { + public required string Identifier { get; init; } + + public string? Name { get; set; } + + public string? Source { get; set; } + + public bool? IsInstalled { get; set; } + + public string? InstalledVersion { get; set; } + + public string? InstalledLocation { get; set; } + + public bool? IsUpdateAvailable { get; set; } + } +} diff --git a/src/WinGetMCPServer/Response/InstallOperationResult.cs b/src/WinGetMCPServer/Response/InstallOperationResult.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InstallOperationResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Response +{ + /// <summary> + /// Contains information about an install operation. + /// </summary> + internal class InstallOperationResult + { + public string? Message { get; set; } + + public bool? RebootRequired { get; set; } + + public int? ErrorCode { get; set; } + + public uint? InstallerErrorCode { get; set; } + + public FindPackageResult? InstalledPackageInformation { get; set; } + } +} diff --git a/src/WinGetMCPServer/Response/PackageIdentityErrorResult.cs b/src/WinGetMCPServer/Response/PackageIdentityErrorResult.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PackageResponse.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Response +{ + using Microsoft.Management.Deployment; + + /// <summary> + /// Contains information relevant to a failure to identify a package. + /// </summary> + internal class PackageIdentityErrorResult + { + public required string Message { get; init; } + + public required string Identifier { get; init; } + + public string? Source { get; set; } + + public List<FindPackageResult> Packages { get; set; } = new List<FindPackageResult>(); + } +} diff --git a/src/WinGetMCPServer/Response/PackageResponse.cs b/src/WinGetMCPServer/Response/PackageResponse.cs @@ -0,0 +1,168 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PackageResponse.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Response +{ + using Microsoft.Management.Deployment; + using ModelContextProtocol.Protocol; + using WinGetMCPServer.Extensions; + + /// <summary> + /// Contains reusable responses for package tools. + /// </summary> + internal static class PackageResponse + { + /// <summary> + /// Creates a response for a ConnectResult error. + /// </summary> + /// <param name="connectResult">The connect result.</param> + /// <returns>The response.</returns> + public static CallToolResponse ForConnectError(ConnectResult connectResult) + { + return new CallToolResponse() + { + IsError = true, + Content = [new Content() { Text = $"Failed when connecting to the package source with error: {connectResult.ExtendedErrorCode.Message} [0x{connectResult.ExtendedErrorCode.HResult:X8}]" }], + }; + } + + /// <summary> + /// Creates a response for a FindPackagesResult error. + /// </summary> + /// <param name="findResult">The find packages result.</param> + /// <returns>The response.</returns> + public static CallToolResponse ForFindError(FindPackagesResult findResult) + { + return new CallToolResponse() + { + IsError = true, + Content = [new Content() { Text = $"Failed when finding packages with reason {findResult.Status} and error: {findResult.ExtendedErrorCode.Message} [0x{findResult.ExtendedErrorCode.HResult:X8}]" }], + }; + } + + /// <summary> + /// Creates a response that indicates the operation was cancelled before any changes were made. + /// </summary> + /// <returns>The response.</returns> + public static CallToolResponse ForCancelBeforeSystemChange() + { + return new CallToolResponse() + { + IsError = true, + Content = [new Content() { Text = $"The operation was cancelled before any system change was started" }], + }; + } + + /// <summary> + /// Creates a response for not finding any packages. + /// </summary> + /// <param name="identifer">The identifier used when searching.</param> + /// <param name="source">The source that was searched.</param> + /// <returns>The response.</returns> + public static CallToolResponse ForEmptyFind(string identifer, string? source) + { + PackageIdentityErrorResult result = new() + { + Message = "Did not find a package with the requested identifier", + Identifier = identifer, + Source = source, + }; + + return ToolResponse.FromObject(result); + } + + /// <summary> + /// Creates a response for finding multiple packages when only 1 is required. + /// </summary> + /// <param name="identifer">The identifier used when searching.</param> + /// <param name="source">The source that was searched.</param> + /// <param name="findResult">The result that contains multiple packages.</param> + /// <returns>The response.</returns> + public static CallToolResponse ForMultiFind(string identifer, string? source, FindPackagesResult findResult) + { + PackageIdentityErrorResult result = new() + { + Message = "Found multiple packages matching the requested identifier; provide a more specific identifier and/or source", + Identifier = identifer, + Source = source, + }; + + result.Packages.AddPackages(findResult); + + return ToolResponse.FromObject(result); + } + + /// <summary> + /// Creates a response for an install operation. + /// </summary> + /// <param name="installResult">The install operation result.</param> + /// <param name="findResult">The post-install package data.</param> + /// <returns>The response.</returns> + public static CallToolResponse ForInstallOperation(InstallResult installResult, FindPackagesResult? findResult) + { + InstallOperationResult result = new InstallOperationResult(); + + switch (installResult.Status) + { + case InstallResultStatus.Ok: + result.Message = "Install completed successfully"; + break; + case InstallResultStatus.BlockedByPolicy: + result.Message = "Installation was blocked by policy"; + break; + case InstallResultStatus.CatalogError: + result.Message = "An error occurred with the source"; + break; + case InstallResultStatus.InternalError: + result.Message = "An internal WinGet error occurred"; + break; + case InstallResultStatus.InvalidOptions: + result.Message = "The install options were invalid"; + break; + case InstallResultStatus.DownloadError: + result.Message = "An error occurred while downloading the package installer"; + break; + case InstallResultStatus.InstallError: + result.Message = "The package installer failed during installation"; + break; + case InstallResultStatus.ManifestError: + result.Message = "The package manifest was invalid"; + break; + case InstallResultStatus.NoApplicableInstallers: + result.Message = "No applicable package installers were available for this system"; + break; + case InstallResultStatus.NoApplicableUpgrade: + result.Message = "No applicable upgrade was available for this system"; + break; + case InstallResultStatus.PackageAgreementsNotAccepted: + result.Message = "The package requires accepting agreements; please install manually"; + break; + default: + result.Message = "Unknown install status"; + break; + } + + if (installResult.RebootRequired) + { + result.RebootRequired = true; + } + + result.ErrorCode = installResult.ExtendedErrorCode?.HResult; + + if (installResult.Status == InstallResultStatus.InstallError) + { + result.InstallerErrorCode = installResult.InstallerErrorCode; + } + + if (findResult != null && findResult.Status == FindPackagesResultStatus.Ok && findResult.Matches?.Count == 1) + { + result.InstalledPackageInformation = PackageListExtensions.FindPackageResultFromCatalogPackage(findResult.Matches[0].CatalogPackage); + } + + return ToolResponse.FromObject(result, installResult.Status != InstallResultStatus.Ok); + } + } +} diff --git a/src/WinGetMCPServer/Response/ToolResponse.cs b/src/WinGetMCPServer/Response/ToolResponse.cs @@ -0,0 +1,80 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ToolResponse.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer.Response +{ + using Microsoft.WinGet.SharedLib.PolicySettings; + using ModelContextProtocol.Protocol; + using System.Text.Json; + using System.Text.Json.Serialization; + using WinGetMCPServer.Exceptions; + using static System.Runtime.InteropServices.JavaScript.JSType; + + /// <summary> + /// Contains reusable responses for tools. + /// </summary> + internal static class ToolResponse + { + /// <summary> + /// Checks whether the server is disabled by group policy. + /// </summary> + public static void CheckGroupPolicy() + { + if (!GroupPolicy.GetInstance().IsEnabled(Policy.McpServer)) + { + throw new ToolResponseException(new CallToolResponse() + { + IsError = true, + Content = [new Content() { Text = "The Windows Package Manager MCP server is disabled by group policy." }] + }); + } + } + + /// <summary> + /// Constructs a response from an object. + /// </summary> + /// <param name="value">The object to return in the response.</param> + /// <param name="isError">Whether or not the response is an error.</param> + /// <returns>The response.</returns> + public static CallToolResponse FromObject(object value, bool isError = false) + { + return FromObject(value, isError, GetDefaultJsonOptions()); + } + + /// <summary> + /// Constructs a response from an object. + /// </summary> + /// <param name="value">The object to return in the response.</param> + /// <param name="isError">Whether or not the response is an error.</param> + /// <param name="jsonSerializerOptions">The JSON serializer options for serializing the object.</param> + /// <returns>The response.</returns> + public static CallToolResponse FromObject(object value, bool isError, JsonSerializerOptions jsonSerializerOptions) + { + return new CallToolResponse() + { + IsError = isError, + Content = [new Content() { Text = JsonSerializer.Serialize(value, GetDefaultJsonOptions()) }] + }; + } + + /// <summary> + /// Gets the default serialization options. + /// </summary> + /// <returns>The default serialization options.</returns> + public static JsonSerializerOptions GetDefaultJsonOptions() + { + return new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + } + } +} diff --git a/src/WinGetMCPServer/ServerConnection.cs b/src/WinGetMCPServer/ServerConnection.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ServerConnection.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer +{ + using Microsoft.Management.Deployment; + + /// <summary> + /// Maintains the connection to the COM server. + /// </summary> + internal static class ServerConnection + { + private static PackageManager? packageManager = null; + + public static PackageManager Instance + { + get + { + if (packageManager == null) + { + packageManager = new PackageManager(); + } + + try + { + // Perform the simplest available call to check if the COM server is still active. + _ = packageManager.Version; + } + catch + { + packageManager = new PackageManager(); + } + + return packageManager; + } + } + } +} diff --git a/src/WinGetMCPServer/WinGetMCPServer.csproj b/src/WinGetMCPServer/WinGetMCPServer.csproj @@ -0,0 +1,72 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetWindowsVersion>10.0.26100.0</TargetWindowsVersion> + <TargetFramework>net8.0-windows$(TargetWindowsVersion)</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion> + <Platforms>x64;x86;arm64</Platforms> + <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutputPath> + <SelfContained>true</SelfContained> + <RuntimeIdentifiers>win-x64;win-x86;win-arm64</RuntimeIdentifiers> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Platform)' == 'x64' "> + <RuntimeIdentifier>win-x64</RuntimeIdentifier> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Platform)' == 'x86' "> + <RuntimeIdentifier>win-x86</RuntimeIdentifier> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Platform)' == 'arm64' "> + <RuntimeIdentifier>win-arm64</RuntimeIdentifier> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" /> + <PackageReference Include="ModelContextProtocol" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Windows.CsWinRT" /> + <ProjectReference Include="..\Microsoft.Management.Deployment\Microsoft.Management.Deployment.vcxproj"> + <Private>False</Private> + </ProjectReference> + <ProjectReference Include="..\PowerShell\Microsoft.WinGet.SharedLib\Microsoft.WinGet.SharedLib.csproj" /> + </ItemGroup> + + <Import Project="..\targets\EmbeddedCsWinRT.targets" /> + + <PropertyGroup> + <CsWinRTIncludes> + Microsoft.Management.Deployment; + Windows.Data.Text.TextSegmen; + Windows.Devices.Geolocation; + Windows.Foundation; + Windows.Globalization.DayOfWee; + Windows.Networking.Connectivity; + Windows.Networking.DomainNameTyp; + Windows.Networking.EndpointPai; + Windows.Networking.IEndpointPai; + Windows.Networking.HostNam; + Windows.Networking.IHostNam; + Windows.Security.Cryptography.Certificates; + Windows.Storage; + Windows.Storage.Provider.FileUpdateStatu; + Windows.System.ProcessorArchitectur; + Windows.System.Use; + Windows.System.IUse; + </CsWinRTIncludes> + <CsWinRTExcludes> + Windows.Foundation.PropertyType; + Windows.Storage.Provider; + </CsWinRTExcludes> + <CsWinRTWindowsMetadata>$(TargetWindowsVersion)</CsWinRTWindowsMetadata> + <!-- Ensure Support for Windows 10, Version 1809 --> + <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> + </PropertyGroup> + +</Project> diff --git a/src/WinGetMCPServer/WingetPackageTools.cs b/src/WinGetMCPServer/WingetPackageTools.cs @@ -0,0 +1,271 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WingetPackageTools.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetMCPServer +{ + using System.ComponentModel; + using Microsoft.Management.Deployment; + using ModelContextProtocol.Protocol; + using ModelContextProtocol.Server; + using ModelContextProtocol; + using Windows.Foundation; + using WinGetMCPServer.Extensions; + using WinGetMCPServer.Response; + using WinGetMCPServer.Exceptions; + + /// <summary> + /// WinGet package tools. + /// </summary> + [McpServerToolType] + internal class WingetPackageTools + { + private PackageManager packageManager; + + public WingetPackageTools() + { + packageManager = ServerConnection.Instance; + } + + [McpServerTool( + Name = "find-winget-packages", + Title = "Find WinGet Packages", + ReadOnly = true, + OpenWorld = false)] + [Description("Find installed and available packages using WinGet")] + public CallToolResponse FindPackages( + [Description("Find packages identified by this value")] string query) + { + try + { + ToolResponse.CheckGroupPolicy(); + + var catalog = ConnectCatalog(); + + // First attempt a more exact match + var findResult = FindForQuery(catalog, query, fullStringMatch: true); + + // If nothing is found, expand to a looser search + if ((findResult.Matches?.Count ?? 0) == 0) + { + findResult = FindForQuery(catalog, query, fullStringMatch: false); + } + + if (findResult.Status != FindPackagesResultStatus.Ok) + { + return PackageResponse.ForFindError(findResult); + } + + List<FindPackageResult> contents = new List<FindPackageResult>(); + contents.AddPackages(findResult); + + return ToolResponse.FromObject(contents); + } + catch (ToolResponseException e) + { + return e.Response; + } + } + + [McpServerTool( + Name = "install-winget-package", + Title = "Install WinGet Package", + ReadOnly = false, + Destructive = true, + Idempotent = false, + OpenWorld = false)] + [Description("Install or update a package using WinGet")] + public async Task<CallToolResponse> InstallPackage( + [Description("The identifier of the WinGet package")] string identifier, + IProgress<ProgressNotificationValue> progress, + CancellationToken cancellationToken, + [Description("The source containing the package")] string? source = null) + { + try + { + ToolResponse.CheckGroupPolicy(); + + var packageCatalog = ConnectCatalog(source); + + if (cancellationToken.IsCancellationRequested) + { + return PackageResponse.ForCancelBeforeSystemChange(); + } + + // First attempt a more exact match + var findResult = FindForIdentifier(packageCatalog, identifier, expandedFields: false); + + if (cancellationToken.IsCancellationRequested) + { + return PackageResponse.ForCancelBeforeSystemChange(); + } + + // If nothing is found, expand to a looser search + if ((findResult.Matches?.Count ?? 0) == 0) + { + findResult = FindForIdentifier(packageCatalog, identifier, expandedFields: true); + } + + if (findResult.Status != FindPackagesResultStatus.Ok) + { + return PackageResponse.ForFindError(findResult); + } + + if (findResult.Matches?.Count == 0) + { + return PackageResponse.ForEmptyFind(identifier, source); + } + else if (findResult.Matches?.Count > 1) + { + return PackageResponse.ForMultiFind(identifier, source, findResult); + } + + CatalogPackage catalogPackage = findResult.Matches![0].CatalogPackage; + InstallOptions options = new InstallOptions(); + IAsyncOperationWithProgress<InstallResult, InstallProgress>? operation = null; + + if (cancellationToken.IsCancellationRequested) + { + return PackageResponse.ForCancelBeforeSystemChange(); + } + + if (catalogPackage.InstalledVersion == null) + { + operation = packageManager.InstallPackageAsync(catalogPackage, options); + } + else + { + operation = packageManager.UpgradePackageAsync(catalogPackage, options); + } + + operation.Progress = (asyncInfo, progressInfo) => progress.Report(CreateInstallProgressNotification(ref progressInfo)); + using CancellationTokenRegistration registration = cancellationToken.Register(() => operation.Cancel()); + + var installResult = await operation; + findResult = null; + + if (installResult.Status == InstallResultStatus.Ok) + { + findResult = ReFindForPackage(catalogPackage.DefaultInstallVersion); + } + + return PackageResponse.ForInstallOperation(installResult, findResult); + } + catch (ToolResponseException e) + { + return e.Response; + } + } + + private ConnectResult ConnectCatalogWithResult(string? catalog = null) + { + CreateCompositePackageCatalogOptions createCompositePackageCatalogOptions = new CreateCompositePackageCatalogOptions(); + + var catalogs = packageManager.GetPackageCatalogs(); + for (int i = 0; i < catalogs.Count; ++i) + { + var catalogRef = catalogs[i]; + if (string.IsNullOrEmpty(catalog) || catalogRef?.Info.Id == catalog) + { + createCompositePackageCatalogOptions.Catalogs.Add(catalogs[i]); + } + } + createCompositePackageCatalogOptions.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs; + + var compositeRef = packageManager.CreateCompositePackageCatalog(createCompositePackageCatalogOptions); + return compositeRef.Connect(); + } + + private PackageCatalog ConnectCatalog(string? catalog = null) + { + var result = ConnectCatalogWithResult(catalog); + if (result.Status != ConnectResultStatus.Ok) + { + throw new ToolResponseException(PackageResponse.ForConnectError(result)); + } + return result.PackageCatalog; + } + + private FindPackagesResult FindForQuery(PackageCatalog catalog, string query, bool fullStringMatch) + { + PackageFieldMatchOption fullStringMatchOption = fullStringMatch ? PackageFieldMatchOption.EqualsCaseInsensitive : PackageFieldMatchOption.ContainsCaseInsensitive; + + FindPackagesOptions findPackageOptions = new(); + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Id, Option = fullStringMatchOption, Value = query }); + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Name, Option = fullStringMatchOption, Value = query }); + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Moniker, Option = PackageFieldMatchOption.EqualsCaseInsensitive, Value = query }); + + return catalog!.FindPackages(findPackageOptions); + } + + private FindPackagesResult FindForIdentifier(PackageCatalog catalog, string query, bool expandedFields) + { + FindPackagesOptions findPackageOptions = new(); + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Id, Option = PackageFieldMatchOption.EqualsCaseInsensitive, Value = query }); + + if (expandedFields) + { + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Name, Option = PackageFieldMatchOption.EqualsCaseInsensitive, Value = query }); + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Moniker, Option = PackageFieldMatchOption.EqualsCaseInsensitive, Value = query }); + } + + return catalog!.FindPackages(findPackageOptions); + } + + private FindPackagesResult? ReFindForPackage(PackageVersionInfo packageVersionInfo) + { + var connectResult = ConnectCatalogWithResult(packageVersionInfo.PackageCatalog.Info.Id); + + if (connectResult.Status != ConnectResultStatus.Ok) + { + return null; + } + + var catalog = connectResult.PackageCatalog; + + FindPackagesOptions findPackageOptions = new(); + findPackageOptions.Selectors.Add(new PackageMatchFilter() { Field = PackageMatchField.Id, Option = PackageFieldMatchOption.Equals, Value = packageVersionInfo.Id }); + + return catalog!.FindPackages(findPackageOptions); + } + + private static ProgressNotificationValue CreateInstallProgressNotification(ref InstallProgress installProgress) + { + string? message = null; + + switch (installProgress.State) + { + case PackageInstallProgressState.Queued: + message = "The install operation is queued"; + break; + case PackageInstallProgressState.Downloading: + message = "The package installer is being downloaded"; + break; + case PackageInstallProgressState.Installing: + message = "The package is being installed"; + break; + case PackageInstallProgressState.PostInstall: + message = "The installation operation is wrapping up"; + break; + case PackageInstallProgressState.Finished: + message = "The install is complete"; + break; + default: + message = "Unknown install state"; + break; + } + + const float downloadPercentage = 0.8f; + + ProgressNotificationValue result = new ProgressNotificationValue() + { + Progress = (float)((installProgress.DownloadProgress * downloadPercentage) + (installProgress.InstallationProgress * (1.0f - downloadPercentage))), + Message = message, + }; + + return result; + } + } +} diff --git a/src/targets/EmbeddedCsWinRT.targets b/src/targets/EmbeddedCsWinRT.targets @@ -23,4 +23,11 @@ <Compile Remove="$(CsWinRTGeneratedFilesDir)Module.cs" /> </ItemGroup> </Target> + + <!-- Remove any items from ReferenceCopyLocalPaths that have CopyLocal set to false. --> + <Target Name="WinGetRemoveCopyLocalReferencesAsNeeded" AfterTargets="CsWinRTRemoveWinMDReferences"> + <ItemGroup> + <ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="%(ReferenceCopyLocalPaths.CopyLocal) == 'false'" /> + </ItemGroup> + </Target> </Project>