MCP Nexus is a self-hosted control plane for MCP servers — "Kubernetes for
MCP." Agents connect to one endpoint; Nexus discovers services, installs the
right MCP servers, aggregates them behind a namespaced router, secures access
with auth/RBAC, and heals/updates them via a reconcile loop.
This first commit is design-phase only (no runnable code yet):
- README.md project front door + module map
- docs/ARCHITECTURE.md target design: tenets, two-plane split, reconcile
loop, domain types, storage, security, deployment
- docs/ROADMAP.md phased delivery (foundations -> walking skeleton ->
discover+install -> secure -> operate -> extend/scale)
- docs/modules/01-20 one design doc per module, all cross-linked
Backend stack decision: Go (single static binary, embedded SQLite + embedded
React dashboard). Repo initialized in /root with a whitelist .gitignore so
only project files are tracked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
8.2 KiB
Markdown
168 lines
8.2 KiB
Markdown
# Update Manager
|
||
|
||
> Module 10 · Plane: Control · Roadmap phase: 4
|
||
> Part of [MCP Nexus architecture](../ARCHITECTURE.md).
|
||
|
||
## Purpose
|
||
|
||
The Update Manager keeps managed `MCPInstance`s current and safe. It watches
|
||
upstream sources for newer versions of the packages an instance was installed
|
||
from — Docker image tags/digests, GitHub releases, and
|
||
[Registry](02-package-registry.md) index updates — decides whether to act based
|
||
on **policy** (automatic, manual-approval-gated, or pinned), and applies updates
|
||
as **re-installs** through the [Auto Installer](03-auto-installer.md). Crucially,
|
||
it keeps the previous known-good spec so any update can be **rolled back** safely.
|
||
|
||
It is a day-2 control-plane reconciler (Architecture §5): a failure here degrades
|
||
*management currency*, never the data plane. Per tenet #2 it proposes and applies
|
||
state transitions through the loop, it does not run ad-hoc upgrade scripts.
|
||
|
||
## Responsibilities
|
||
|
||
- **Watch** upstream sources per instance and detect available updates:
|
||
- Docker image **tags/digests** (a new digest behind a tracked tag).
|
||
- **GitHub releases** for `source=github` packages.
|
||
- **Registry index updates** (a package's recommended version advanced).
|
||
- Evaluate each candidate against the instance's **update policy**:
|
||
`automatic`, `manual` (approval-gated), or `pinned` (never auto-update).
|
||
- Enforce **version pinning**: a pinned instance is skipped entirely (still
|
||
reported as "update available").
|
||
- Apply an approved update by driving a **re-install** at the new digest via the
|
||
[Installer](03-auto-installer.md), preserving the same `bound_resource_uuid`.
|
||
- Retain the **previous known-good `MCPInstance` spec** and perform **rollback**
|
||
when the new version fails to become/stay healthy.
|
||
- Coordinate with [Health Monitoring](09-health-monitoring.md) to gate and
|
||
validate updates, and with [Notifications](13-notifications.md) to alert on
|
||
available / applied / failed / rolled-back updates.
|
||
- Record all decisions and transitions to Audit (Architecture §9).
|
||
|
||
## Non-goals
|
||
|
||
- Performing the actual pull/create/start — delegated to the
|
||
[Installer](03-auto-installer.md) and Runtime.
|
||
- Choosing/verifying package signatures or digests — owned by the
|
||
[Registry](02-package-registry.md).
|
||
- Restarting crashed-but-same-version instances — that is
|
||
[Health Monitoring](09-health-monitoring.md) (self-healing), not an update.
|
||
- Deciding which package a service uses ([Recipes](15-smart-recipes.md)).
|
||
|
||
## Interfaces
|
||
|
||
```go
|
||
type UpdatePolicy string
|
||
|
||
const (
|
||
PolicyAuto UpdatePolicy = "automatic" // apply without human action
|
||
PolicyManual UpdatePolicy = "manual" // detect, then wait for approval
|
||
PolicyPinned UpdatePolicy = "pinned" // never auto-update
|
||
)
|
||
|
||
// Everything-is-a-plugin (tenet #3): each upstream is a watcher.
|
||
type UpdateWatcher interface {
|
||
Name() string // "docker-digest", "github-release", "registry-index"
|
||
// Return a candidate if a newer version exists for this instance.
|
||
Check(ctx context.Context, inst MCPInstance) (Candidate, bool, error)
|
||
}
|
||
|
||
type Candidate struct {
|
||
InstanceID string
|
||
From PackageVersion // current (digest-pinned)
|
||
To PackageVersion // proposed (digest-pinned)
|
||
Source string // watcher name
|
||
Notes string // e.g. GitHub release notes
|
||
}
|
||
|
||
type UpdateManager interface {
|
||
Scan(ctx context.Context) ([]Candidate, error) // periodic + event-driven
|
||
Approve(ctx context.Context, candidateID string) error
|
||
Apply(ctx context.Context, c Candidate) (MCPInstance, error) // re-install
|
||
Rollback(ctx context.Context, instanceID string) error // to known-good
|
||
SetPolicy(ctx context.Context, instanceID string, p UpdatePolicy) error
|
||
Pin(ctx context.Context, instanceID string, version string) error
|
||
}
|
||
```
|
||
|
||
HTTP/API surface (behind Auth + [RBAC](08-rbac.md)):
|
||
|
||
- `GET /api/v1/updates` — available/pending candidates across instances.
|
||
- `POST /api/v1/updates/{id}/approve` — approve a manual-gated candidate.
|
||
- `POST /api/v1/updates/{id}/apply` — force-apply now (audited).
|
||
- `POST /api/v1/instances/{id}/rollback` — roll back to previous known-good.
|
||
- `PUT /api/v1/instances/{id}/update-policy` — set policy / pin a version.
|
||
|
||
## Data
|
||
|
||
- **Reads:** `MCPInstance` (current `package`, `version`, digest) from
|
||
**Inventory**; `Package`/`PackageVersion` from the **Registry** store
|
||
(Architecture §8); instance `health` from Health Monitoring.
|
||
- **Writes:** on apply, the Installer updates the `MCPInstance` (new `version`,
|
||
`container_ref`); the Update Manager updates `state` around the transition
|
||
(`Updating`, `RollingBack`).
|
||
- **Module-local persistence:**
|
||
- `update_policy` per instance (`automatic|manual|pinned`, optional pinned
|
||
version/digest).
|
||
- `previous_good_spec` — the last healthy `MCPInstance` spec (package, version,
|
||
digest, rendered config-with-secret-refs) for rollback.
|
||
- `update_candidate` rows (from→to, source, status: available/approved/applied/
|
||
failed/rolled-back) and per-watcher cursors/ETags to avoid re-alerting.
|
||
|
||
## Dependencies
|
||
|
||
- [Auto Installer](03-auto-installer.md) — updates are re-installs of a new version.
|
||
- [MCP Package Registry](02-package-registry.md) — resolves/validates target digests.
|
||
- [Health Monitoring](09-health-monitoring.md) — gates the new version; signals rollback.
|
||
- [Notifications](13-notifications.md) — alerts on available/applied/failed/rolled-back.
|
||
- [Secrets Manager](07-secrets-manager.md) — re-injected at the re-install (unchanged).
|
||
- [Plugin System](11-plugin-system.md) — `UpdateWatcher` is a plugin point.
|
||
- [RBAC](08-rbac.md) — who may approve/apply/pin/rollback.
|
||
|
||
## Failure modes & handling
|
||
|
||
- **New version unhealthy:** the Installer's health gate fails, or Health
|
||
Monitoring reports the freshly updated instance unhealthy within a validation
|
||
window → automatic **rollback** to `previous_good_spec`; fire a failed-update
|
||
notification.
|
||
- **Watcher/upstream unreachable:** skip silently (best-effort), keep the last
|
||
cursor; repeated failures alert but never block other updates or the data plane.
|
||
- **Registry can't validate target digest:** candidate is rejected (not applied);
|
||
reported as `failed` with reason.
|
||
- **Pinned instance with newer upstream:** never applied; surfaced as
|
||
"update available (pinned)" only.
|
||
- **Rollback itself fails:** instance marked degraded, high-severity notification,
|
||
left for Health Monitoring quarantine + human intervention.
|
||
- **Concurrent apply + heal:** updates are serialized per `instance-id`
|
||
(shared lock with the Installer) so an update and a restart don't race.
|
||
- **Approval race / stale candidate:** applying a candidate whose `From` no longer
|
||
matches the live instance is rejected and re-scanned.
|
||
|
||
## Security notes
|
||
|
||
- Only **digest-pinned, signature-verified** target versions are applied; the
|
||
Registry re-verifies before the Installer pulls (Architecture §9).
|
||
- Secrets are re-injected at runtime by the Installer on the re-install — never
|
||
copied into `previous_good_spec` as plaintext; the retained spec holds secret
|
||
*references* only.
|
||
- Approve/apply/pin/rollback are audited mutating actions with principal, target,
|
||
from→to versions, and outcome.
|
||
- Auto-update policy is itself an RBAC-guarded setting; a compromised auto policy
|
||
is a supply-chain risk, so downgrades/policy changes are audited.
|
||
|
||
## Open questions
|
||
|
||
- Health **validation window** length after an update before declaring success —
|
||
fixed, or per-recipe?
|
||
- Should GitHub-release watching drive version *selection* directly, or only
|
||
notify the Registry to advance its index (single source of truth)?
|
||
- Batch/canary updates across many instances of the same package vs. one-at-a-time.
|
||
- Retention depth for `previous_good_spec` — just N−1, or a short history for
|
||
multi-step rollback?
|
||
- Maintenance windows / update scheduling to avoid disrupting active agent calls.
|
||
|
||
## Milestone
|
||
|
||
Phase 4 — *Operate (day-2)*. Update Manager ships with Docker-digest, GitHub-release,
|
||
and Registry-index watchers; automatic/manual/pinned policies; and health-gated
|
||
rollback. It contributes to the Phase 4 exit story alongside
|
||
[Health](09-health-monitoring.md), [Metrics](18-metrics.md),
|
||
[Notifications](13-notifications.md), and the [Dashboard](12-web-dashboard.md).
|