Files
mcp-gateway-nexus/docs/modules/11-plugin-system.md
drjones 8d3ffef920 docs: initial architecture and design for MCP Nexus
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>
2026-07-07 04:38:27 +00:00

9.3 KiB

Plugin System

Module 11 · Plane: Cross-cutting · Roadmap phase: 5 Part of MCP Nexus architecture.

Purpose

The extensibility backbone that makes tenet #3 real — "everything is a plugin." The Nexus core knows only interfaces; concrete discovery methods, fingerprinters, installers, package providers, notifiers, auth backends, dashboards, and tool transformers are swappable implementations. This module does not invent those interfaces — each owning module already defines its own (discovery.Method, notify.Notifier, health.Probe, …). Module 11 generalizes them into one plugin contract, lifecycle, discovery/registration mechanism, and a recommended out-of-process transport so third parties can extend Nexus, in any language, without forking or recompiling the core.

Responsibilities

  • Define the common plugin contract: a lifecycle (register → configure → start → health → stop) and metadata (name, type, version, capabilities) every plugin implements regardless of its type.
  • Enumerate and load the supported plugin types, each mapping to an interface already owned by a core module: discovery methods, fingerprinters, installers, package providers, notifiers, auth backends, dashboards, and tool transformers.
  • Provide the out-of-process transport (gRPC over a handshake-negotiated local socket, in the style of hashicorp/go-plugin) so plugins run as isolated subprocesses — a crashing or malicious plugin cannot take down the core.
  • Support in-process (compiled-in) plugins too, for first-party built-ins and performance-sensitive paths — the transport is an implementation detail behind the same contract.
  • Own versioning & compatibility: a semver'd plugin API, capability negotiation on the handshake, and refusal to load incompatible plugins.
  • Manage plugin discovery, install, config, and health: a plugin registry, per-plugin config (secrets by ref), and health surfacing to the Dashboard.

Non-goals

  • Does not define the per-type interfaces' semantics. What a Method or Notifier means is owned by Discovery, Notifications, etc. Module 11 provides the wrapper, transport, and lifecycle.
  • Not the MCP tool plugins agents call. Those are upstream MCPInstances managed by the control loop. This is about extending Nexus itself.
  • Not a general sandbox runtime — container sandboxing of MCP servers is Security + Runtime; plugin isolation here is process-level (separate proc, dropped privileges, scoped host interface).
  • Not a package registry. Distributing/signing plugin artifacts leans on Package Registry + Security supply-chain rules.

Interfaces

// Plugin is the universal contract every plugin implements, regardless of type.
type Plugin interface {
    // Info returns identity + declared capabilities used for compatibility checks.
    Info() PluginInfo
    // Configure applies validated config; secrets arrive as refs, resolved by host.
    Configure(ctx context.Context, cfg map[string]any) error
    // Health reports readiness so the dashboard/plugin manager can surface it.
    Health(ctx context.Context) error
    // Stop releases resources; the host may kill the subprocess after a deadline.
    Stop(ctx context.Context) error
}

type PluginInfo struct {
    Name    string
    Type    PluginType // Discovery, Fingerprinter, Installer, PackageProvider,
                       // Notifier, AuthBackend, Dashboard, ToolTransformer
    Version string     // plugin's own version
    APIVer  string     // Nexus plugin-API semver it was built against
    Caps    []string
}

// Host is the surface the core exposes back to a plugin (least-privilege).
type Host interface {
    Recorder() metrics.Recorder          // scoped metrics
    Secret(ref string) (string, error)   // resolve a Secret by ref only
    Logger() Logger                       // structured, plugin-namespaced
    EventBus() EventEmitter               // emit typed events (RBAC-scoped)
}

// Manager loads, negotiates, and supervises plugins (in- or out-of-process).
type Manager interface {
    Load(ctx context.Context, spec PluginSpec) (handle PluginHandle, err error)
    // TypedDispatch adapts a loaded plugin to the owning module's interface,
    // e.g. as a discovery.Method or notify.Notifier, over the transport.
    Bind(handle PluginHandle) (any, error)
    List() []PluginStatus
    Unload(ctx context.Context, name string) error
}

Transport: a go-plugin-style handshake starts the subprocess, negotiates the API version + a shared secret, and multiplexes typed gRPC services (one service definition per plugin type) over a local Unix socket. Each core module ships the gRPC ⇆ Go-interface adapter for its type.

Lifecycle: Load (spawn + handshake + version check) → Configure (validated config, secrets by ref) → Bind (adapt to the owning module's typed interface) → serving (health-polled) → Unload/Stop (graceful, then kill after a deadline). A plugin that fails Health is restarted with backoff; the owning module keeps functioning on its built-in implementations while a plugin is down, so an extension is never a single point of failure for a core capability.

Internal HTTP (control API, RBAC-guarded):

  • GET /api/v1/plugins — installed plugins, type, version, health, config status.
  • POST /api/v1/plugins — install/register a plugin (artifact ref + config).
  • POST /api/v1/plugins/{name}/reload · DELETE /api/v1/plugins/{name}.

Data

  • Writes the plugin registry (installed plugins, type, version, artifact digest, enabled state) to the Config/Registry store (§8).
  • Reads plugin config from Config and plugin credentials by ref from Secrets — never handed the raw vault.
  • Plugin subprocess state (PID, socket, handshake) is module-local runtime state, rebuilt on restart from the registry.

Dependencies

Failure modes & handling

Failure Behavior
Plugin subprocess crashes Isolated by the transport; the Manager marks it unhealthy, restarts with backoff, and the owning module falls back to built-ins — the core stays up.
Version incompatibility Handshake rejects a plugin built against an incompatible API semver; it is not loaded; surfaced in dashboard.
Plugin hangs / slow RPC Every host↔plugin call is deadline-bounded; a hung plugin is killed after a grace period, not awaited.
Malicious/greedy plugin Runs out-of-process with dropped privileges and only the least-privilege Host surface (no direct DB, secrets only by ref); resource limits applied.
Bad plugin config Configure validates and rejects atomically; the plugin stays in its prior good state or disabled.
Unsigned/tampered artifact Security supply-chain check refuses to install (signature + digest pin).

Security notes

Honors Architecture §9 and tenet #5. Out-of-process isolation is the core safety property: plugins get no ambient authority — only the narrow Host interface (scoped metrics, secrets by ref, namespaced logging, RBAC-scoped event emit), never the database, the raw vault, or the host network unless policy grants it. Plugin artifacts are signature-verified and digest-pinned before load (supply chain). Installing/reloading/removing a plugin is a mutating, RBAC-gated, audited action. Plugin logs and emitted events are sanitized so a plugin cannot exfiltrate secrets or forge another principal's identity.

Open questions

  • Transport: standardize on hashicorp/go-plugin directly, or a thin in-house gRPC handshake to avoid the dependency surface?
  • Do we support WASM plugins (sandboxed, portable) as a third mode alongside in-process and subprocess?
  • How rich should the Host surface be before it becomes an attack surface — where is the line between capability and least privilege?
  • Compatibility policy: strict semver gate, or capability-negotiation that allows partial feature sets?
  • Plugin distribution/governance — reuse the community registry + recipe trust model (Architecture §12)?

Milestone

Delivered in Phase 5 (Extend & scale). The per-type interfaces exist earlier (each module defines its own from its phase); Phase 5 promotes them to a stable, semver'd plugin API with the out-of-process gRPC transport, plugin manager, and registry. Exit proof (shared with the phase goal): a community-authored discovery plugin + recipe installs a new MCP with no core changes, running as an isolated subprocess.