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>
170 lines
7.8 KiB
Markdown
170 lines
7.8 KiB
Markdown
# 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.
|