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>
This commit is contained in:
drjones
2026-07-07 04:38:27 +00:00
commit 8d3ffef920
24 changed files with 3000 additions and 0 deletions

169
docs/modules/04-gateway.md Normal file
View File

@@ -0,0 +1,169 @@
# Gateway
> Module 04 · Plane: Data · Roadmap phase: 1
> Part of [MCP Nexus architecture](../ARCHITECTURE.md).
## Purpose
The Gateway is **the single MCP endpoint every AI agent connects to** —
`https://mcp.company.lan`. It embodies tenet #1 ("one endpoint, many
agents"): to an agent the Gateway *is* an MCP server; upstream, together with
the **Router** (the "MCP Router + Aggregator" of Architecture §4), it acts as
an MCP client to every managed
[`MCPInstance`](../ARCHITECTURE.md). It dynamically **aggregates** all healthy
upstream MCP servers behind one address and **namespaces** their tools so
`homeassistant.turn_on`, `postgres.query`, and `github.create_issue` never
collide.
The Gateway owns the *edge and transport*. It does not own tool ownership or
dispatch (that is the Router) and it does not own the tool
catalog (that is the [Dynamic Tool Registry](05-dynamic-tool-registry.md)).
Keeping these three concerns separate is deliberate: the Gateway can be
restarted, load-balanced, and hardened without touching routing logic or the
catalog.
## Responsibilities
- Terminate TLS and accept agent connections over **Streamable HTTP** (the
current MCP HTTP transport) speaking **JSON-RPC 2.0**.
- Run the MCP server side of the `initialize` handshake: negotiate protocol
version, advertise server capabilities (`tools`, and later `resources` /
`prompts`), and open a session.
- Manage the per-session request lifecycle: parse JSON-RPC frames, correlate
ids, enforce timeouts, and stream partial/chunked results back to the agent.
- Delegate every request to [Auth](06-authentication.md) (who is calling?) and
[RBAC](08-rbac.md) (what may they see/call?) *before* it reaches the Router.
- Hand `tools/list` and `tools/call` to the Router, which resolves the owning
instance from the Dynamic Tool Registry and dispatches over the upstream
client pool.
- Maintain the **connection pool** to upstream MCP servers (stdio and
Streamable HTTP), reusing warm connections across agent requests.
- Ship the `nexus connect` **stdio↔HTTP bridge** so local agents (Claude
Desktop, Cursor) that only speak stdio can attach to the HTTP endpoint.
- Emit per-request audit and metrics events.
## Non-goals
- **Tool ownership / dispatch decisions** — owned by the Router.
- **The tool catalog and its freshness** — owned by the Dynamic Tool Registry.
- **AuthN/AuthZ policy** — the Gateway *calls* Auth/RBAC; it stores no policy.
- **Installing, healing, or updating instances** — control-plane concern; the
Gateway only consumes the healthy upstream set.
- **Secret storage or injection** — secrets reach MCP containers via the
[Secrets Manager](07-secrets-manager.md), never through the Gateway.
## Interfaces
```go
// Server is the agent-facing MCP server. One per Nexus process.
type Server interface {
// Serve binds the Streamable HTTP listener (TLS terminated here).
Serve(ctx context.Context, addr string, tls *tls.Config) error
// Handle processes one JSON-RPC request within an authenticated session
// and writes the response (possibly streamed) to w.
Handle(ctx context.Context, s *Session, req *jsonrpc.Request, w StreamWriter) error
}
// Session is a live agent connection after a successful initialize.
type Session struct {
ID string
Principal auth.Principal // resolved by Auth
Roles []rbac.Role // resolved by RBAC → visible tool set
Protocol string // negotiated MCP version
Caps ClientCaps
}
// StreamWriter delivers incremental JSON-RPC results / progress notifications.
type StreamWriter interface {
WriteChunk(any) error
Close(err error) error
}
// UpstreamPool hands the Router warm MCP client connections per instance.
type UpstreamPool interface {
Client(ctx context.Context, instanceID string) (mcpclient.Conn, release func(), error)
Drain(instanceID string) // called when Health marks an instance gone
}
```
HTTP / MCP endpoints exposed at the edge:
| Method / path | Purpose |
|---|---|
| `POST /mcp` | Streamable HTTP MCP endpoint; carries JSON-RPC `initialize`, `tools/list`, `tools/call`, notifications. |
| `GET /mcp` | Server→client stream channel (SSE-style) for long-lived sessions. |
| `GET /healthz` | Liveness of the edge (does not gate on upstream health). |
`nexus connect --url https://mcp.company.lan --token …` runs a local process
that presents stdio to the agent and proxies frames to `POST/GET /mcp`.
## Data
The Gateway is **mostly stateless** — it holds only in-memory session and pool
state, so replicas behind a load balancer are interchangeable (HA topology,
Architecture §10).
- **Session table (in-memory):** `sessionID → Session`, TTL-expired.
- **Connection pool (in-memory):** per-`MCPInstance` warm `mcpclient.Conn`s,
keyed by `instanceID`, with idle eviction and per-instance max.
- **Persisted:** nothing of its own. It *reads* the healthy instance set and
the RBAC-filtered tool view via the Registry/Router, and *writes* audit +
metrics events to the shared Audit/Metrics stores.
## Dependencies
- [Authentication](06-authentication.md) — resolves the calling principal.
- [RBAC](08-rbac.md) — resolves the visible/callable tool set per session.
- [Dynamic Tool Registry](05-dynamic-tool-registry.md) — source of the live
`{namespace}.{tool}` catalog and tool→instance mapping.
- [AI Agent Profiles](14-agent-profiles.md) — maps agent identity to roles.
- [Health Monitoring](09-health-monitoring.md) — signals which instances are
poolable; drives `UpstreamPool.Drain`.
- [Secrets Manager](07-secrets-manager.md) — ensures no secret transits the
edge in an agent-visible payload.
- [Metrics](18-metrics.md) / [Security](19-security.md) — audit, TLS, limits.
## Failure modes & handling
- **Upstream instance down mid-call:** the pool returns a transport error; the
Router surfaces a JSON-RPC error to the agent. The Gateway never hangs on a
dead upstream — every dispatch is deadline-bounded.
- **Upstream slow:** per-request deadline + circuit breaker per instance;
streamed progress keeps the agent connection alive until the deadline.
- **Auth/RBAC unavailable:** fail **closed** — reject with a JSON-RPC error;
never fall back to unauthenticated access (tenet #5).
- **Pool exhaustion:** bounded per-instance connections + a wait queue with a
timeout; over-limit calls get a retryable error, not unbounded fan-out.
- **Malformed JSON-RPC:** respond with the spec error code; drop the frame,
keep the session.
- **Bridge disconnect (`nexus connect`):** local process retries with backoff;
the HTTP session is TTL-reaped if the bridge does not resume.
## Security notes
- **TLS terminates here** (Architecture §9); internal calls to Router/Auth are
over localhost/socket.
- The Gateway enforces that a session may only ever see and invoke its
RBAC-visible tool set — requests are scoped *before* the Router dispatches,
so an agent cannot call a tool outside its role even by guessing its name.
- Per-session and per-principal **rate limiting** at the edge.
- No secret material is ever echoed back to an agent; error messages from
upstreams are sanitized.
- Every `tools/call` is audited with principal, resolved instance, and outcome.
## Open questions
- Session affinity vs. stateless replicas: do we need sticky sessions for
streamed calls behind the HA load balancer, or is a shared session store
sufficient?
- Should the `nexus connect` bridge be a subcommand of the main binary or a
separately distributed micro-binary (relates to Architecture §12)?
- Streamable HTTP resumability: how far do we implement MCP resumable streams
vs. requiring the agent to re-issue on reconnect?
## Milestone
Phase 1 (Walking skeleton). Ships with the MCP server edge, upstream client
pool, and `nexus connect`. **Exit:** register a filesystem MCP + one HTTP MCP
in YAML, connect Claude, see `filesystem.*` and `other.*` tools, and call one
successfully — the core value proposition proven end-to-end.