Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ Unregisters
unvirtualized
UParse
upgradable
upgradeable
upgradecode
URLZONE
USEDEFAULT
Expand Down
7 changes: 7 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ match criteria that factor into the result ordering. This will prevent them from

Added a new `--no-progress` command-line flag that disables all progress reporting (progress bars and spinners). This flag is universally available on all commands and takes precedence over the `visual.progressBar` setting. Useful for automation scenarios or when running WinGet in environments where progress output is undesirable.

### MCP `upgrade` support

The WinGet MCP server's existing tools have been extended with new parameters to support upgrade scenarios:

- **`find-winget-packages`** now accepts an `upgradeable` parameter (default: `false`). When set to `true`, it lists only installed packages that have available upgrades — equivalent to `winget upgrade`. The `query` parameter becomes optional in this mode, allowing it to filter results or be omitted to list all upgradeable packages. AI agents can use this to answer requests like "What apps can I update with WinGet?"
- **`install-winget-package`** now accepts an `upgradeOnly` parameter (default: `false`). When set to `true`, it only upgrades an already-installed package and returns a clear error if the package is not installed (pointing to `install-winget-package` without `upgradeOnly` instead). AI agents can use this to answer requests like "Update WinGetCreate" or, in combination with `find-winget-packages` with `upgradeable=true`, "Update all my apps."

### Authenticated GitHub API requests in PowerShell module

The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` environment variables to authenticate GitHub API requests. This significantly increases the GitHub API rate limit, preventing failures in CI/CD pipelines. Use `-Verbose` to see which token is being used.
Expand Down
88 changes: 88 additions & 0 deletions src/WinGetMCPServer/Response/PackageResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,94 @@ public static CallToolResult ForMultiFind(string identifer, string? source, Find
return ToolResponse.FromObject(result);
}

/// <summary>
/// Creates a response for a package that is not installed.
/// </summary>
/// <param name="identifier">The identifier used when searching.</param>
/// <param name="source">The source that was searched.</param>
/// <returns>The response.</returns>
public static CallToolResult ForNotInstalled(string identifier, string? source)
{
PackageIdentityErrorResult result = new()
{
Message = "The package is not installed; use install-winget-package to install it",
Identifier = identifier,
Source = source,
};

return ToolResponse.FromObject(result, isError: true);
}

/// <summary>
/// Creates a response for an upgrade operation.
/// </summary>
/// <param name="installResult">The upgrade operation result.</param>
/// <param name="findResult">The post-upgrade package data.</param>
/// <returns>The response.</returns>
public static CallToolResult ForUpgradeOperation(InstallResult installResult, FindPackagesResult? findResult)
{
InstallOperationResult result = new InstallOperationResult();

switch (installResult.Status)
{
case InstallResultStatus.Ok:
result.Message = "Upgrade completed successfully";
break;
case InstallResultStatus.BlockedByPolicy:
result.Message = "Upgrade 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 upgrade 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 the upgrade";
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 upgrade manually";
break;
default:
result.Message = "Unknown upgrade 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);
}

/// <summary>
/// Creates a response for an install operation.
/// </summary>
Expand Down
86 changes: 70 additions & 16 deletions src/WinGetMCPServer/WingetPackageTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,48 @@ public WingetPackageTools()
Title = "Find WinGet Packages",
ReadOnly = true,
OpenWorld = false)]
[Description("Find installed and available packages using WinGet")]
[Description("Find installed and available packages using WinGet. To list all installed packages that have available upgrades (equivalent to 'winget upgrade'), call with upgradeable=true and no query. To filter upgradeable packages by name, call with upgradeable=true and a query. To search for packages to install, call with upgradeable=false and a required query.")]
public CallToolResult FindPackages(
[Description("Find packages identified by this value")] string query)
[Description("Find packages identified by this value. Required when upgradeable is false; optionally filters results when upgradeable is true.")] string? query = null,
[Description("When true, only return installed packages that have available upgrades")] bool upgradeable = false)
{
try
{
ToolResponse.CheckGroupPolicy();

var catalog = ConnectCatalog();
if (!upgradeable && string.IsNullOrEmpty(query))
{
return new CallToolResult()
{
IsError = true,
Content = [new TextContentBlock() { Text = "A query is required when upgradeable is false" }],
};
}

// First attempt a more exact match
var findResult = FindForQuery(catalog, query, fullStringMatch: true);
// Use LocalCatalogs when listing upgrades to enumerate only installed packages,
// consistent with `winget upgrade`. Remote catalogs are still included in the
// composite so IsUpdateAvailable remains accurate.
var catalog = ConnectCatalog(searchBehavior: upgradeable
? CompositeSearchBehavior.LocalCatalogs
: CompositeSearchBehavior.AllCatalogs);

// If nothing is found, expand to a looser search
if ((findResult.Matches?.Count ?? 0) == 0)
FindPackagesResult findResult;
if (string.IsNullOrEmpty(query))
{
findResult = FindForQuery(catalog, query, fullStringMatch: false);
// This can only happen in the case that upgradeable is true, in which case this
// won't accidentally list all packages from all catalogs
findResult = FindAllPackages(catalog);
}
else
{
// First attempt a more exact match
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)
Expand All @@ -59,7 +84,21 @@ public CallToolResult FindPackages(
}

List<FindPackageResult> contents = new List<FindPackageResult>();
contents.AddPackages(findResult);
if (upgradeable)
{
for (int i = 0; i < findResult.Matches?.Count; ++i)
{
var package = findResult.Matches[i].CatalogPackage;
if (package.IsUpdateAvailable)
{
contents.Add(PackageListExtensions.FindPackageResultFromCatalogPackage(package));
}
}
}
else
{
contents.AddPackages(findResult);
}

return ToolResponse.FromObject(contents);
}
Expand All @@ -76,12 +115,13 @@ public CallToolResult FindPackages(
Destructive = true,
Idempotent = false,
OpenWorld = false)]
[Description("Install or update a package using WinGet")]
[Description("Install or upgrade a package using WinGet. When upgradeOnly is true, only upgrades an already-installed package and returns an error if it is not installed. When upgradeOnly is false (default), installs the package if not present or upgrades it if already installed.")]
public async Task<CallToolResult> InstallPackage(
[Description("The identifier of the WinGet package")] string identifier,
IProgress<ProgressNotificationValue> progress,
CancellationToken cancellationToken,
[Description("The source containing the package")] string? source = null)
[Description("The source containing the package")] string? source = null,
[Description("When true, only upgrade an already-installed package; returns an error if the package is not installed")] bool upgradeOnly = false)
{
try
{
Expand Down Expand Up @@ -123,6 +163,12 @@ public async Task<CallToolResult> InstallPackage(
}

CatalogPackage catalogPackage = findResult.Matches![0].CatalogPackage;

if (upgradeOnly && catalogPackage.InstalledVersion == null)
{
return PackageResponse.ForNotInstalled(identifier, source);
}

InstallOptions options = new InstallOptions();
IAsyncOperationWithProgress<InstallResult, InstallProgress>? operation = null;

Expand Down Expand Up @@ -153,15 +199,17 @@ public async Task<CallToolResult> InstallPackage(
findResult = ReFindForPackage(catalogPackage.DefaultInstallVersion);
}

return PackageResponse.ForInstallOperation(installResult, findResult);
return upgradeOnly
? PackageResponse.ForUpgradeOperation(installResult, findResult)
: PackageResponse.ForInstallOperation(installResult, findResult);
}
catch (ToolResponseException e)
{
return e.Response;
}
}

private ConnectResult ConnectCatalogWithResult(string? catalog = null)
private ConnectResult ConnectCatalogWithResult(string? catalog = null, CompositeSearchBehavior searchBehavior = CompositeSearchBehavior.AllCatalogs)
{
CreateCompositePackageCatalogOptions createCompositePackageCatalogOptions = new CreateCompositePackageCatalogOptions();

Expand All @@ -175,15 +223,15 @@ private ConnectResult ConnectCatalogWithResult(string? catalog = null)
createCompositePackageCatalogOptions.Catalogs.Add(catalogRef);
}
}
createCompositePackageCatalogOptions.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs;
createCompositePackageCatalogOptions.CompositeSearchBehavior = searchBehavior;

var compositeRef = packageManager.CreateCompositePackageCatalog(createCompositePackageCatalogOptions);
return compositeRef.Connect();
}

private PackageCatalog ConnectCatalog(string? catalog = null)
private PackageCatalog ConnectCatalog(string? catalog = null, CompositeSearchBehavior searchBehavior = CompositeSearchBehavior.AllCatalogs)
{
var result = ConnectCatalogWithResult(catalog);
var result = ConnectCatalogWithResult(catalog, searchBehavior);
if (result.Status != ConnectResultStatus.Ok)
{
throw new ToolResponseException(PackageResponse.ForConnectError(result));
Expand Down Expand Up @@ -217,6 +265,12 @@ private FindPackagesResult FindForIdentifier(PackageCatalog catalog, string quer
return catalog!.FindPackages(findPackageOptions);
}

private FindPackagesResult FindAllPackages(PackageCatalog catalog)
{
FindPackagesOptions findPackageOptions = new();
return catalog!.FindPackages(findPackageOptions);
}

private FindPackagesResult? ReFindForPackage(PackageVersionInfo packageVersionInfo)
{
var connectResult = ConnectCatalogWithResult(packageVersionInfo.PackageCatalog.Info.Id);
Expand Down
Loading