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>
109 lines
9.6 KiB
Markdown
109 lines
9.6 KiB
Markdown
# Web Dashboard
|
|
|
|
> Module 12 · Plane: Data · Roadmap phase: 4
|
|
> Part of [MCP Nexus architecture](../ARCHITECTURE.md).
|
|
|
|
## Purpose
|
|
The human control surface for Nexus: a modern **React** UI that shows what has been discovered, what is installed, whether it is healthy, what changed, and who may access what — with real-time updates and dark mode. Per tenet #4, the compiled UI is **embedded in the Go binary via `embed.FS`** and served by the same process on the same port, so there is no separate frontend to deploy, build, or version-skew. It is a *data-plane* surface for operators (distinct from the agent MCP edge) and sits behind the same [Auth](06-authentication.md) + [RBAC](08-rbac.md) as everything else — the dashboard is a privileged client, not a bypass.
|
|
|
|
## Responsibilities
|
|
- Serve the embedded SPA (`embed.FS`) plus a versioned, RBAC-guarded **backend API** the UI consumes; API and assets are one binary, one origin.
|
|
- Provide **real-time updates** over WebSocket (with SSE fallback): live instance state, discovery events, logs, health transitions, and update progress stream without polling.
|
|
- Render the full **page map** (below), each backed by the owning module's control API — the dashboard aggregates, it does not own the data.
|
|
- Authenticate operators (session/JWT from [Auth](06-authentication.md)) and render only what the operator's [role](08-rbac.md) permits — hide pages/actions the role can't perform.
|
|
- Stream container **logs** and expose action buttons (restart, quarantine, approve update, test notification) that call the owning module's API — every action audited.
|
|
- Support **dark mode** (default) and be responsive for tablet/desktop operator use.
|
|
- Provide the operator workflows that close the loop by hand when automation is below the confidence threshold (tenet #6): approve a low-confidence discovery, promote a gated update, release a quarantined instance, edit a recipe match.
|
|
- Surface **build/version info** and per-module health so an operator can tell at a glance whether the control plane itself is healthy, independent of any single MCP instance.
|
|
|
|
## Non-goals
|
|
- **Owns no domain data or business logic.** Every panel reads/writes through another module's API; the dashboard holds only view state.
|
|
- **Not the agent endpoint.** Agents speak MCP to the [Gateway](04-gateway.md); the dashboard is for humans and never routes tool calls.
|
|
- **Not an auth provider.** It consumes [Auth](06-authentication.md)/[RBAC](08-rbac.md); it defines no policy.
|
|
- **Not a separate service.** No standalone Node server in production — assets are embedded; a dev proxy is a build-time convenience only.
|
|
- **Not a plugin host runtime** — dashboard *plugins* (custom panels) are a [Plugin System](11-plugin-system.md) type; the shell just mounts them.
|
|
|
|
## Interfaces
|
|
```go
|
|
//go:embed all:dist
|
|
var uiFS embed.FS
|
|
|
|
// DashboardServer mounts the SPA + the aggregating backend API on the shared mux.
|
|
type DashboardServer interface {
|
|
// Mount registers static asset routes (from uiFS) and /api/v1/* handlers,
|
|
// all wrapped by the Auth + RBAC middleware chain.
|
|
Mount(r Router, mw ...Middleware) error
|
|
// Realtime upgrades a request to WS/SSE and streams events the caller
|
|
// is authorized to see (RBAC-filtered event bus fan-out).
|
|
Realtime(w http.ResponseWriter, r *http.Request) error
|
|
}
|
|
```
|
|
|
|
Real-time transport:
|
|
- `GET /api/v1/stream` — WebSocket (SSE fallback at `GET /api/v1/events`); server pushes RBAC-filtered `Event`s from the bus (instance state, discovery, logs, update progress). Client subscribes to topics; server enforces visibility.
|
|
|
|
Real-time model: the browser opens one multiplexed connection and subscribes to topics (`instances`, `discovery`, `logs:{id}`, `updates`). The server tees the internal event bus, applies the connection's RBAC filter, and fans out only authorized events — the same event stream Notifications and Metrics consume, so the dashboard shows exactly the transitions the rest of the system acted on, with no separate polling loop drifting out of sync.
|
|
|
|
Asset pipeline: the React app is built (`vite build`) into `dist/` at compile time and embedded with `//go:embed`. There is no runtime Node process; a Vite dev server proxying to a running `nexus` is a developer convenience only. The binary is the single deployable artifact (tenet #4).
|
|
|
|
Backend API surface (each page consumes the owning module's control API):
|
|
|
|
| Page | Backing API / module |
|
|
|---|---|
|
|
| Dashboard (overview) | aggregate of Health, Metrics, Inventory |
|
|
| Discovered Services | [Discovery](01-discovery-engine.md) `/discovery/resources` |
|
|
| Installed MCPs | [Installer](03-auto-installer.md) / Inventory `/instances` |
|
|
| Containers | Runtime `/containers` |
|
|
| Logs | Runtime log stream (via `/stream`) |
|
|
| Metrics | [Metrics](18-metrics.md) `/metrics` + Grafana links |
|
|
| Health | [Health](09-health-monitoring.md) `/health/instances` |
|
|
| Updates | [Update Manager](10-update-manager.md) `/updates` |
|
|
| Settings | Config store `/settings` |
|
|
| Secrets | [Secrets](07-secrets-manager.md) `/secrets` (refs only) |
|
|
| Users | [Auth](06-authentication.md) `/users` |
|
|
| Roles / Permissions | [RBAC](08-rbac.md) `/roles`, `/permissions` |
|
|
| Discovery (config) | [Discovery](01-discovery-engine.md) `/discovery/methods` |
|
|
| Plugins | [Plugin System](11-plugin-system.md) `/plugins` |
|
|
| Registry | [Package Registry](02-package-registry.md) `/packages`, `/recipes` |
|
|
| Agents | [AI Agent Profiles](14-agent-profiles.md) `/agents` |
|
|
| Notifications | [Notifications](13-notifications.md) `/notifications/channels`, `/rules` |
|
|
| Permissions | [RBAC](08-rbac.md) `/permissions` (effective grant inspector) |
|
|
|
|
Pages fall into three groups: **observe** (Dashboard, Discovered Services, Installed MCPs, Containers, Logs, Metrics, Health), **govern** (Users, Roles, Permissions, Agents, Secrets), and **operate** (Updates, Discovery config, Plugins, Registry, Settings, Notifications). The nav and the realtime subscription set are both derived from the operator's role, so an operator only loads and streams the groups they may act on.
|
|
|
|
## Data
|
|
- **Owns no persistent store.** All reads/writes proxy to module APIs; the browser holds ephemeral view/query-cache state only.
|
|
- Static assets (the React `dist/`) are compiled into the binary via `embed.FS` at build time — versioned with the binary, never fetched at runtime.
|
|
- Real-time state is derived from the shared event bus, RBAC-filtered per connection.
|
|
|
|
## Dependencies
|
|
- [Authentication](06-authentication.md) — operator login / sessions.
|
|
- [RBAC](08-rbac.md) — page/action/data visibility; drives the realtime fan-out filter.
|
|
- Every module with a control API (Discovery, Registry, Installer, Health, Updates, Secrets, Notifications, Agents, Plugins, Metrics) — the dashboard is their aggregating client.
|
|
- [Metrics](18-metrics.md) — the Metrics page and links to shipped Grafana dashboards.
|
|
- [Plugin System](11-plugin-system.md) — custom dashboard panels are a plugin type.
|
|
|
|
## Failure modes & handling
|
|
| Failure | Behavior |
|
|
|---|---|
|
|
| A backing module API is down | The affected panel shows a scoped error/empty state; the rest of the dashboard keeps working (no all-or-nothing render). |
|
|
| WebSocket drops | Client reconnects with backoff and falls back to SSE, then to periodic REST polling as last resort. |
|
|
| Operator lacks a role for a page | The page/action is hidden **and** the API rejects (defense in depth); RBAC is enforced server-side, not just in the UI. |
|
|
| Stale asset vs. API version | Assets are embedded with the binary, so UI and API versions can't skew; a build-info banner surfaces the version. |
|
|
| Large log stream | Streaming with backpressure + client-side windowing; server caps per-connection throughput. |
|
|
| Session expiry mid-session | 401 triggers a re-auth flow; unsaved form state is preserved where feasible. |
|
|
| Control plane degraded (reconciler down) | The dashboard clearly flags degraded management while the data-plane panels (Gateway/Router health) keep reporting — the split of Architecture §5 is visible, not hidden. |
|
|
| Browser clock skew / event replay | Events carry server timestamps + monotonic sequence; the client orders by sequence, not local time. |
|
|
|
|
## Security notes
|
|
Honors Architecture §9. The dashboard is served over the same **TLS**-terminated edge and is fully behind [Auth](06-authentication.md) + [RBAC](08-rbac.md) — there is no anonymous view. RBAC is enforced **server-side** for every API call and for the realtime fan-out; hiding a control in the UI is UX, never the security boundary. The Secrets page shows **references and metadata only** — secret values are never sent to the browser (tenet #5). Every mutating action from the UI is audited with the operator principal. Standard web hardening applies: CSRF protection on state-changing requests, strict CSP, same-origin API, and no third-party asset CDNs (everything embedded).
|
|
|
|
## Open questions
|
|
- WebSocket vs. SSE as the primary transport — WS is bidirectional but SSE is simpler behind proxies; ship both with WS default?
|
|
- How much log history to buffer server-side for the Logs page before requiring the operator to pull from container/host?
|
|
- Do dashboard plugins (custom panels) load as sandboxed iframes/web-components, or as build-time-composed federated modules?
|
|
- Mobile: a responsive read-mostly view for on-call, or defer entirely to notifications?
|
|
|
|
## Milestone
|
|
Delivered in **Phase 4** (Operate / day-2). Thin slice: the embedded React shell with Dashboard, Discovered Services, Installed MCPs, Health, Logs, and Updates pages, live WebSocket updates, dark mode, behind Auth/RBAC. Exit proof (shared with the phase goal): kill a managed MCP container → the Health/Installed pages reflect `offline→restarting→running` in real time without a manual refresh.
|