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>
8.1 KiB
MCP Package Registry
Module 02 · Plane: Control · Roadmap phase: 2 Part of MCP Nexus architecture.
Purpose
The Package Registry is the catalog that answers one question for the rest of the
control plane: "Given a service type, which MCP server should I install, from
where, at what version, and configured how?" It maintains a searchable, locally
cached index of Package objects (Architecture §7) synced from one or more
remote indexes plus operator-authored local packages. It is the source of truth
for artifact provenance (source, digest, signature) and for the config_schema
that Recipes bind to at install time.
The Registry does not run anything. It resolves and describes. The
Auto Installer consumes its resolutions; a Package
never becomes an MCPInstance inside this module.
Responsibilities
- Maintain the local
Packagecatalog in the Registry store (Architecture §8), co-resident with Recipes. - Sync from multiple remote indexes (a Nexus index is a signed JSON/HTTP manifest listing packages) and merge them with local packages, with a deterministic precedence order on collision.
- Provide resolution:
service type → recommended Package(e.g. Home Assistant →homeassistant-mcp). - Provide version selection: given a
Packageand a constraint, return a concrete version pinned by digest. - Verify signatures on packages and index manifests before a package is eligible for install (tenet #5, Architecture §9).
- Expose the
config_schemaand config template reference each package carries, so Recipes can render config against it. - Full-text and faceted search over name, service type, source, and tags for the Dashboard and API.
Non-goals
- Pulling images or creating containers — that is the Installer via the Runtime.
- Deciding whether to install (matching a
DiscoveredResourceto a package) — that is Smart Recipes and the reconciler. - Storing secrets or rendered config — templates are declarative; values come from Secrets at install time.
- Being an OCI registry itself. Nexus reads from GitHub / OCI / Docker Hub / self-hosted repos; it does not host artifacts.
Interfaces
// Source of a package artifact (mirrors Package.source in Architecture §7).
type Source string
const (
SourceGitHub Source = "github"
SourceOCI Source = "oci"
SourceDockerHub Source = "dockerhub"
SourceLocal Source = "local"
)
// A concrete, install-ready version pinned by digest.
type PackageVersion struct {
Version string // semver-ish tag, e.g. "1.4.2"
Ref string // "ghcr.io/project/homeassistant-mcp:1.4.2"
Digest string // "sha256:..." — the install pin
Signature Signature
}
type Package struct {
Name string // "homeassistant-mcp"
ServiceType string // "home-assistant" — resolution key
Source Source
Versions []PackageVersion
ConfigSchema json.RawMessage // JSON Schema for config
ConfigTemplateRef string // e.g. "homeassistant.yaml"
Signature Signature
}
type Registry interface {
// Resolution: service type -> recommended package.
Resolve(ctx context.Context, serviceType string) (Package, error)
// Version selection against a constraint ("", "latest", "~1.4", "1.4.2").
SelectVersion(ctx context.Context, pkg string, constraint string) (PackageVersion, error)
Get(ctx context.Context, name string) (Package, error)
Search(ctx context.Context, q Query) ([]Package, error)
}
// Everything-is-a-plugin (tenet #3): remote index providers are swappable.
type IndexProvider interface {
Name() string
Fetch(ctx context.Context) ([]Package, IndexMeta, error) // signed manifest
}
type SyncManager interface {
Sync(ctx context.Context) (SyncReport, error) // merge all providers + local
AddIndex(url string, opts IndexOpts) error
}
HTTP/API surface (control-plane API, behind Auth):
GET /api/v1/packages?service_type=&source=&q=— search/list.GET /api/v1/packages/{name}— full package incl. versions + schema.POST /api/v1/packages— register/upsert a local package.POST /api/v1/registry/sync— trigger a sync of all configured indexes.GET /api/v1/registry/indexes— configured remote indexes + last sync state.
Data
- Reads/writes:
Packageobjects in the Registry store (Architecture §8), alongsideRecipes (which reference packages by name +ConfigTemplateRef). - Module-local persistence:
registry_indexrows (index URL, public key, last-synced digest, ETag/cursor),package_versionrows (digest, ref, signature status), and a cached copy of each fetched index manifest for offline/deterministic resolution. - Config templates referenced by
ConfigTemplateRefare stored as opaque blobs in the Registry store and rendered later by the Installer/Recipe engine.
Resolution flow: a DiscoveredResource.type (Architecture §7) is mapped by a
Recipe to a serviceType; Resolve returns the recommended Package;
SelectVersion pins it by digest. Example entry:
service_type: home-assistant
name: homeassistant-mcp
source: oci
ref: ghcr.io/project/homeassistant-mcp
versions: [1.4.2 @ sha256:…, 1.4.1 @ sha256:…]
config_template_ref: homeassistant.yaml
Dependencies
- Smart Recipes — consumes packages + config templates.
- Auto Installer — consumes resolved, digest-pinned versions.
- Secrets Manager — config templates declare secret refs filled at install time, not here.
- Security — signature/trust model for indexes and packages.
- Plugin System —
IndexProvideris a plugin point. - Update Manager — watches index updates as one update source.
Failure modes & handling
- Remote index unreachable: sync is best-effort; last good cached manifest
continues to serve
Resolve/SelectVersion. Sync failures are surfaced via Notifications, never block the control loop. - Signature verification fails: the offending package/version is marked
untrustedand excluded from resolution; it is never returned as installable. - Index collision (same package from two indexes): deterministic precedence (local > pinned-trusted index > community index); the shadowed entry is logged.
- No package for a service type:
Resolvereturns a typedErrNoPackage; the reconciler leaves the resource un-installed and low-confidence handling applies (tenet #6). - Missing/invalid
config_schema: package is listed but flaggedunschematized; Recipes referencing it fail validation early, not at runtime.
Security notes
- Every remote index manifest and every package is signature-verified before eligibility (tenet #5, Architecture §9); trust anchors (public keys) are configured per index.
- Versions are always pinned by digest; tags are advisory only. The digest is what the Installer pulls.
- The Registry stores no secrets and renders no secret values; templates carry only references.
- Mutating API calls (register local package, add index, sync) are audited (Architecture §9) and gated by RBAC.
Open questions
- Community index governance and the trust/signing model for shared packages (mirrors Architecture §12 "Recipe distribution").
- Do we support multiple recommended packages per service type with a ranking, or strictly one canonical recommendation?
- Should
SelectVersionhonor per-package update policy hints, or is that owned entirely by the Update Manager? - Config-template format: single YAML template vs. a small templating dialect shared with Recipes.
Milestone
Phase 2 — Discover → install. Registry v1 ships with local + GitHub/OCI package
indexes, digest-pinned versions, config schemas, and a starter package set so the
Installer can resolve (e.g.) Postgres → postgres-mcp
during the Phase 2 exit-criteria loop.