Skip to content

Commit 89a2830

Browse files
authored
Add Update commands for MCP (#6117)
1 parent 584e601 commit 89a2830

4 files changed

Lines changed: 166 additions & 16 deletions

File tree

.github/actions/spelling/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ Unregisters
607607
unvirtualized
608608
UParse
609609
upgradable
610+
upgradeable
610611
upgradecode
611612
URLZONE
612613
USEDEFAULT

doc/ReleaseNotes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ match criteria that factor into the result ordering. This will prevent them from
3232

3333
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.
3434

35+
### MCP `upgrade` support
36+
37+
The WinGet MCP server's existing tools have been extended with new parameters to support upgrade scenarios:
38+
39+
- **`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?"
40+
- **`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."
41+
3542
### Authenticated GitHub API requests in PowerShell module
3643

3744
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.

src/WinGetMCPServer/Response/PackageResponse.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,94 @@ public static CallToolResult ForMultiFind(string identifer, string? source, Find
9595
return ToolResponse.FromObject(result);
9696
}
9797

98+
/// <summary>
99+
/// Creates a response for a package that is not installed.
100+
/// </summary>
101+
/// <param name="identifier">The identifier used when searching.</param>
102+
/// <param name="source">The source that was searched.</param>
103+
/// <returns>The response.</returns>
104+
public static CallToolResult ForNotInstalled(string identifier, string? source)
105+
{
106+
PackageIdentityErrorResult result = new()
107+
{
108+
Message = "The package is not installed; use install-winget-package to install it",
109+
Identifier = identifier,
110+
Source = source,
111+
};
112+
113+
return ToolResponse.FromObject(result, isError: true);
114+
}
115+
116+
/// <summary>
117+
/// Creates a response for an upgrade operation.
118+
/// </summary>
119+
/// <param name="installResult">The upgrade operation result.</param>
120+
/// <param name="findResult">The post-upgrade package data.</param>
121+
/// <returns>The response.</returns>
122+
public static CallToolResult ForUpgradeOperation(InstallResult installResult, FindPackagesResult? findResult)
123+
{
124+
InstallOperationResult result = new InstallOperationResult();
125+
126+
switch (installResult.Status)
127+
{
128+
case InstallResultStatus.Ok:
129+
result.Message = "Upgrade completed successfully";
130+
break;
131+
case InstallResultStatus.BlockedByPolicy:
132+
result.Message = "Upgrade was blocked by policy";
133+
break;
134+
case InstallResultStatus.CatalogError:
135+
result.Message = "An error occurred with the source";
136+
break;
137+
case InstallResultStatus.InternalError:
138+
result.Message = "An internal WinGet error occurred";
139+
break;
140+
case InstallResultStatus.InvalidOptions:
141+
result.Message = "The upgrade options were invalid";
142+
break;
143+
case InstallResultStatus.DownloadError:
144+
result.Message = "An error occurred while downloading the package installer";
145+
break;
146+
case InstallResultStatus.InstallError:
147+
result.Message = "The package installer failed during the upgrade";
148+
break;
149+
case InstallResultStatus.ManifestError:
150+
result.Message = "The package manifest was invalid";
151+
break;
152+
case InstallResultStatus.NoApplicableInstallers:
153+
result.Message = "No applicable package installers were available for this system";
154+
break;
155+
case InstallResultStatus.NoApplicableUpgrade:
156+
result.Message = "No applicable upgrade was available for this system";
157+
break;
158+
case InstallResultStatus.PackageAgreementsNotAccepted:
159+
result.Message = "The package requires accepting agreements; please upgrade manually";
160+
break;
161+
default:
162+
result.Message = "Unknown upgrade status";
163+
break;
164+
}
165+
166+
if (installResult.RebootRequired)
167+
{
168+
result.RebootRequired = true;
169+
}
170+
171+
result.ErrorCode = installResult.ExtendedErrorCode?.HResult;
172+
173+
if (installResult.Status == InstallResultStatus.InstallError)
174+
{
175+
result.InstallerErrorCode = installResult.InstallerErrorCode;
176+
}
177+
178+
if (findResult != null && findResult.Status == FindPackagesResultStatus.Ok && findResult.Matches?.Count == 1)
179+
{
180+
result.InstalledPackageInformation = PackageListExtensions.FindPackageResultFromCatalogPackage(findResult.Matches[0].CatalogPackage);
181+
}
182+
183+
return ToolResponse.FromObject(result, installResult.Status != InstallResultStatus.Ok);
184+
}
185+
98186
/// <summary>
99187
/// Creates a response for an install operation.
100188
/// </summary>

src/WinGetMCPServer/WingetPackageTools.cs

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,48 @@ public WingetPackageTools()
3434
Title = "Find WinGet Packages",
3535
ReadOnly = true,
3636
OpenWorld = false)]
37-
[Description("Find installed and available packages using WinGet")]
37+
[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.")]
3838
public CallToolResult FindPackages(
39-
[Description("Find packages identified by this value")] string query)
39+
[Description("Find packages identified by this value. Required when upgradeable is false; optionally filters results when upgradeable is true.")] string? query = null,
40+
[Description("When true, only return installed packages that have available upgrades")] bool upgradeable = false)
4041
{
4142
try
4243
{
4344
ToolResponse.CheckGroupPolicy();
4445

45-
var catalog = ConnectCatalog();
46+
if (!upgradeable && string.IsNullOrEmpty(query))
47+
{
48+
return new CallToolResult()
49+
{
50+
IsError = true,
51+
Content = [new TextContentBlock() { Text = "A query is required when upgradeable is false" }],
52+
};
53+
}
4654

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

50-
// If nothing is found, expand to a looser search
51-
if ((findResult.Matches?.Count ?? 0) == 0)
62+
FindPackagesResult findResult;
63+
if (string.IsNullOrEmpty(query))
5264
{
53-
findResult = FindForQuery(catalog, query, fullStringMatch: false);
65+
// This can only happen in the case that upgradeable is true, in which case this
66+
// won't accidentally list all packages from all catalogs
67+
findResult = FindAllPackages(catalog);
68+
}
69+
else
70+
{
71+
// First attempt a more exact match
72+
findResult = FindForQuery(catalog, query, fullStringMatch: true);
73+
74+
// If nothing is found, expand to a looser search
75+
if ((findResult.Matches?.Count ?? 0) == 0)
76+
{
77+
findResult = FindForQuery(catalog, query, fullStringMatch: false);
78+
}
5479
}
5580

5681
if (findResult.Status != FindPackagesResultStatus.Ok)
@@ -59,7 +84,21 @@ public CallToolResult FindPackages(
5984
}
6085

6186
List<FindPackageResult> contents = new List<FindPackageResult>();
62-
contents.AddPackages(findResult);
87+
if (upgradeable)
88+
{
89+
for (int i = 0; i < findResult.Matches?.Count; ++i)
90+
{
91+
var package = findResult.Matches[i].CatalogPackage;
92+
if (package.IsUpdateAvailable)
93+
{
94+
contents.Add(PackageListExtensions.FindPackageResultFromCatalogPackage(package));
95+
}
96+
}
97+
}
98+
else
99+
{
100+
contents.AddPackages(findResult);
101+
}
63102

64103
return ToolResponse.FromObject(contents);
65104
}
@@ -76,12 +115,13 @@ public CallToolResult FindPackages(
76115
Destructive = true,
77116
Idempotent = false,
78117
OpenWorld = false)]
79-
[Description("Install or update a package using WinGet")]
118+
[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.")]
80119
public async Task<CallToolResult> InstallPackage(
81120
[Description("The identifier of the WinGet package")] string identifier,
82121
IProgress<ProgressNotificationValue> progress,
83122
CancellationToken cancellationToken,
84-
[Description("The source containing the package")] string? source = null)
123+
[Description("The source containing the package")] string? source = null,
124+
[Description("When true, only upgrade an already-installed package; returns an error if the package is not installed")] bool upgradeOnly = false)
85125
{
86126
try
87127
{
@@ -123,6 +163,12 @@ public async Task<CallToolResult> InstallPackage(
123163
}
124164

125165
CatalogPackage catalogPackage = findResult.Matches![0].CatalogPackage;
166+
167+
if (upgradeOnly && catalogPackage.InstalledVersion == null)
168+
{
169+
return PackageResponse.ForNotInstalled(identifier, source);
170+
}
171+
126172
InstallOptions options = new InstallOptions();
127173
IAsyncOperationWithProgress<InstallResult, InstallProgress>? operation = null;
128174

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

156-
return PackageResponse.ForInstallOperation(installResult, findResult);
202+
return upgradeOnly
203+
? PackageResponse.ForUpgradeOperation(installResult, findResult)
204+
: PackageResponse.ForInstallOperation(installResult, findResult);
157205
}
158206
catch (ToolResponseException e)
159207
{
160208
return e.Response;
161209
}
162210
}
163211

164-
private ConnectResult ConnectCatalogWithResult(string? catalog = null)
212+
private ConnectResult ConnectCatalogWithResult(string? catalog = null, CompositeSearchBehavior searchBehavior = CompositeSearchBehavior.AllCatalogs)
165213
{
166214
CreateCompositePackageCatalogOptions createCompositePackageCatalogOptions = new CreateCompositePackageCatalogOptions();
167215

@@ -175,15 +223,15 @@ private ConnectResult ConnectCatalogWithResult(string? catalog = null)
175223
createCompositePackageCatalogOptions.Catalogs.Add(catalogRef);
176224
}
177225
}
178-
createCompositePackageCatalogOptions.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs;
226+
createCompositePackageCatalogOptions.CompositeSearchBehavior = searchBehavior;
179227

180228
var compositeRef = packageManager.CreateCompositePackageCatalog(createCompositePackageCatalogOptions);
181229
return compositeRef.Connect();
182230
}
183231

184-
private PackageCatalog ConnectCatalog(string? catalog = null)
232+
private PackageCatalog ConnectCatalog(string? catalog = null, CompositeSearchBehavior searchBehavior = CompositeSearchBehavior.AllCatalogs)
185233
{
186-
var result = ConnectCatalogWithResult(catalog);
234+
var result = ConnectCatalogWithResult(catalog, searchBehavior);
187235
if (result.Status != ConnectResultStatus.Ok)
188236
{
189237
throw new ToolResponseException(PackageResponse.ForConnectError(result));
@@ -217,6 +265,12 @@ private FindPackagesResult FindForIdentifier(PackageCatalog catalog, string quer
217265
return catalog!.FindPackages(findPackageOptions);
218266
}
219267

268+
private FindPackagesResult FindAllPackages(PackageCatalog catalog)
269+
{
270+
FindPackagesOptions findPackageOptions = new();
271+
return catalog!.FindPackages(findPackageOptions);
272+
}
273+
220274
private FindPackagesResult? ReFindForPackage(PackageVersionInfo packageVersionInfo)
221275
{
222276
var connectResult = ConnectCatalogWithResult(packageVersionInfo.PackageCatalog.Info.Id);

0 commit comments

Comments
 (0)