Context
The ToolHive CLI bump to v0.29.0 (PR #2305) ships new REST endpoints dedicated to workload upgrades:
GET /api/v1beta/workloads/upgrade-check — bulk drift check (filter by group, include stopped via all=true)
GET /api/v1beta/workloads/{name}/upgrade-check — per-workload drift check
POST /api/v1beta/workloads/{name}/upgrade — apply upgrade (optional { env, secrets } merge body)
These cover end-to-end what the renderer is doing today by hand.
Current state (what we do "a mano")
Drift detection (client-side):
useIsServerFromRegistry (renderer/src/features/mcp-servers/hooks/use-is-server-from-registry.ts) parses the running image tag, matches the workload image against the registry catalog, and flags drift when registryTag !== localTag.
- The card surfaces an amber
ArrowUpCircle button when isFromRegistry && drift (renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx:64).
Apply (via the generic edit endpoint):
useUpdateVersion (renderer/src/features/mcp-servers/hooks/use-update-version.tsx) orchestrates two paths:
- Direct update when there is no env-var drift: confirm dialog →
useMutationUpdateWorkload → POST /workloads/{name}/edit with the new image.
- Edit & review when env-var drift is detected: opens the full edit dialog pre-populated with
imageOverride, envVarsOverride, secretsOverride, then submits via the same edit mutation.
- The env-var diff itself is computed client-side from the registry entry.
New API surface (after #2305 lands)
GET .../upgrade-check returns a full UpgradeCheckResult:
status: up-to-date | upgrade-available | not-registry-sourced | server-not-found | unknown
current_image, candidate_image, registry_server
env_var_drift.added / .removed (with name, description, default, required, secret) — same data we recompute today for the review dialog
config_drift.transport / config_drift.permission_profile — drift we currently ignore
POST .../upgrade accepts PkgApiV1UpgradeRequest:
env?: Record<string, string> — overrides/merge
secrets?: string[] — secret references in <name>,target=<env> form
- Empty body = upgrade preserving existing configuration
Proposed migration (two independent steps)
Step 1 — replace client-side drift detection
- Replace
useIsServerFromRegistry consumers with getApiV1BetaWorkloadsUpgradeCheckOptions (bulk on list views) and the per-name variant where granularity is needed.
- Delete the tag parsing / registry comparison logic in the renderer.
- The card's "update available" indicator becomes `status === 'upgrade-available'`.
Step 2 — replace the apply mutation
- In
use-update-version.tsx, swap useMutationUpdateWorkload for postApiV1BetaWorkloadsByNameUpgradeMutation.
- Direct upgrade path →
POST .../upgrade with empty body.
- Edit-&-review path → map the form values for newly added vars/secrets into
{ env, secrets } and call upgrade instead of edit.
- Continue using the
edit endpoint only for unrelated config changes (it stays the right tool for arbitrary edits).
Wins
- Backend becomes the source of truth for drift; renderer stops parsing image tags.
- We start surfacing
config_drift (transport, permission profile) which is currently silently ignored.
- Clean semantic separation:
edit for arbitrary edits, upgrade for the registry-driven version bump.
Caveats / open questions
PkgApiV1UpgradeRequest does not accept cmd_arguments or arbitrary workload fields. If we want to allow tweaking those at upgrade time, we either keep using edit for that case or split it into two steps (upgrade then edit).
- The bulk endpoint takes
all and group query params — confirm the list view's current filter semantics still work with one call.
- Decide UX for
status === 'not-registry-sourced' | 'server-not-found' | 'unknown' (today these workloads are silently treated as "no drift").
Acceptance criteria
References
Context
The ToolHive CLI bump to v0.29.0 (PR #2305) ships new REST endpoints dedicated to workload upgrades:
GET /api/v1beta/workloads/upgrade-check— bulk drift check (filter bygroup, include stopped viaall=true)GET /api/v1beta/workloads/{name}/upgrade-check— per-workload drift checkPOST /api/v1beta/workloads/{name}/upgrade— apply upgrade (optional{ env, secrets }merge body)These cover end-to-end what the renderer is doing today by hand.
Current state (what we do "a mano")
Drift detection (client-side):
useIsServerFromRegistry(renderer/src/features/mcp-servers/hooks/use-is-server-from-registry.ts) parses the running image tag, matches the workload image against the registry catalog, and flags drift whenregistryTag !== localTag.ArrowUpCirclebutton whenisFromRegistry && drift(renderer/src/features/mcp-servers/components/card-mcp-server/index.tsx:64).Apply (via the generic edit endpoint):
useUpdateVersion(renderer/src/features/mcp-servers/hooks/use-update-version.tsx) orchestrates two paths:useMutationUpdateWorkload→POST /workloads/{name}/editwith the new image.imageOverride,envVarsOverride,secretsOverride, then submits via the sameeditmutation.New API surface (after #2305 lands)
GET .../upgrade-checkreturns a fullUpgradeCheckResult:status:up-to-date | upgrade-available | not-registry-sourced | server-not-found | unknowncurrent_image,candidate_image,registry_serverenv_var_drift.added/.removed(with name, description, default, required, secret) — same data we recompute today for the review dialogconfig_drift.transport/config_drift.permission_profile— drift we currently ignorePOST .../upgradeacceptsPkgApiV1UpgradeRequest:env?: Record<string, string>— overrides/mergesecrets?: string[]— secret references in<name>,target=<env>formProposed migration (two independent steps)
Step 1 — replace client-side drift detection
useIsServerFromRegistryconsumers withgetApiV1BetaWorkloadsUpgradeCheckOptions(bulk on list views) and the per-name variant where granularity is needed.Step 2 — replace the apply mutation
use-update-version.tsx, swapuseMutationUpdateWorkloadforpostApiV1BetaWorkloadsByNameUpgradeMutation.POST .../upgradewith empty body.{ env, secrets }and callupgradeinstead ofedit.editendpoint only for unrelated config changes (it stays the right tool for arbitrary edits).Wins
config_drift(transport, permission profile) which is currently silently ignored.editfor arbitrary edits,upgradefor the registry-driven version bump.Caveats / open questions
PkgApiV1UpgradeRequestdoes not acceptcmd_argumentsor arbitrary workload fields. If we want to allow tweaking those at upgrade time, we either keep usingeditfor that case or split it into two steps (upgrade then edit).allandgroupquery params — confirm the list view's current filter semantics still work with one call.status === 'not-registry-sourced' | 'server-not-found' | 'unknown'(today these workloads are silently treated as "no drift").Acceptance criteria
getApiV1BetaWorkloadsUpgradeCheck*instead ofuseIsServerFromRegistry; the hook is removed (or reduced to non-upgrade callers, if any).useUpdateVersionusespostApiV1BetaWorkloadsByNameUpgradefor both direct and edit-&-review paths.config_drift(transport / permission profile) is surfaced in the review dialog.upgrade-available,up-to-date,not-registry-sourced, etc.).References