Files
mcp-gateway-nexus/docs/modules/05-dynamic-tool-registry.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

165 lines
7.5 KiB
Markdown

# Dynamic Tool Registry
> Module 05 · Plane: Data · Roadmap phase: 1
> Part of [MCP Nexus architecture](../ARCHITECTURE.md).
## Purpose
The Dynamic Tool Registry is the **live, continuously-updated catalog of every
tool exposed by every healthy [`MCPInstance`](../ARCHITECTURE.md)**, each
namespaced as `{namespace}.{tool}` (Architecture §7). It is the authoritative
answer to two questions on the data-plane hot path:
1. *"What tools exist?"* — it serves `tools/list` to agents, filtered to the
caller's RBAC-visible set.
2. *"Who owns this tool?"* — it maintains the `tool → owning instance` mapping
the Router uses to dispatch `tools/call`.
It is **live** because upstream MCP servers appear, disappear, and change their
tool sets at runtime. The Registry reacts to instances coming and going (from
the [Installer](03-auto-installer.md) / [Health](09-health-monitoring.md)) and
to upstream `tools/list` results and `notifications/tools/list_changed`
notifications, keeping the aggregated catalog current without a restart.
## Responsibilities
- Aggregate the tool lists of all **healthy** instances into one namespaced
catalog; drop tools of unhealthy/absent instances immediately.
- Assign and enforce the `{namespace}.{tool}` naming and **collision rules**
(see [Data](#data)).
- Maintain the `{namespace}.{tool} → instanceID` dispatch map consumed by the
Router.
- Serve `tools/list` results and the `GET /tools` view, always filtered by the
caller's RBAC-resolved visibility.
- Subscribe to lifecycle events (instance added/removed, health transitions)
and to upstream `list_changed` notifications; refresh incrementally.
- Cache upstream tool schemas (input/output JSON Schema) so `tools/list` does
not fan out to every upstream on each agent call.
- Emit change events so the Gateway can forward `list_changed` to connected
agents whose visible set changed.
## Non-goals
- **Transport / sessions / the agent edge** — owned by the
[Gateway](04-gateway.md).
- **Dispatching `tools/call` and streaming results** — owned by the Router; the
Registry only supplies the ownership mapping.
- **Deciding what an agent may see** — the Registry *applies* an RBAC filter it
is given; policy lives in [RBAC](08-rbac.md).
- **Installing or health-checking instances** — control-plane modules feed the
Registry; it does not manage instance lifecycle.
## Interfaces
```go
// Registry is the live namespaced tool catalog. Concurrency-safe; reads are
// hot-path, writes are event-driven.
type Registry interface {
// List returns the tools visible to a principal, already RBAC-filtered.
List(ctx context.Context, view rbac.ToolView) ([]Tool, error)
// Resolve maps a namespaced tool name to its owning instance for dispatch.
Resolve(name string) (instanceID string, t Tool, ok bool)
// Subscribe streams catalog change events (tool added/removed/schema-changed).
Subscribe(ctx context.Context) (<-chan ChangeEvent, func())
}
// Ingest is the write side, driven by lifecycle + upstream events.
type Ingest interface {
// UpsertInstance (re)loads an instance's tools under its namespace.
UpsertInstance(ctx context.Context, inst MCPInstance, tools []UpstreamTool) error
// RemoveInstance drops all tools owned by an instance.
RemoveInstance(ctx context.Context, instanceID string) error
// OnListChanged handles an upstream notifications/tools/list_changed.
OnListChanged(ctx context.Context, instanceID string) error
}
type Tool struct {
Name string // "{namespace}.{tool}", e.g. "postgres.query"
Namespace string
Bare string // upstream tool name, e.g. "query"
InstanceID string
InputSchema json.RawMessage
Title, Description string
}
```
HTTP / MCP surface:
| Method / path | Purpose |
|---|---|
| MCP `tools/list` | Served via the Gateway; RBAC-filtered namespaced catalog. |
| MCP `notifications/tools/list_changed` | Forwarded to agents when their visible set changes. |
| `GET /tools` | Dashboard/API view of the full catalog with owning instance, namespace, health, and schema (control-plane, RBAC-guarded). |
## Data
- **Catalog (in-memory, source of truth at runtime):** `map[name]Tool`, plus a
`namespace → instanceID` index and reverse `instanceID → []name`.
- **Namespacing rule:** the namespace is the instance's stable, human-readable
name (from its Recipe/config, e.g. `homeassistant`, `postgres`, `github`).
The fully-qualified tool name is `{namespace}.{tool}`.
- **Collision rules:**
- Two instances may never share a namespace — enforced at instance
registration; a duplicate namespace is a config/reconcile error surfaced to
the operator, not silently merged.
- Within a namespace, upstream tool names are already unique per MCP server,
so `{namespace}.{tool}` is globally unique by construction.
- Bare tool names never reach the agent; only fully-qualified names are
exposed, so unrelated servers with the same tool name (`query`) never clash.
- **Persistence:** the catalog is *derived* and rebuilt from Inventory +
upstream `tools/list` on boot; it is a cache, not a store. Namespace
assignments live with the `MCPInstance` in Inventory.
## Dependencies
- [Gateway](04-gateway.md) — consumes `List` for `tools/list`, forwards
`list_changed`.
- [RBAC](08-rbac.md) — supplies the `ToolView` filter applied to every list.
- [Health Monitoring](09-health-monitoring.md) — health transitions add/remove
instances from the catalog.
- [Auto Installer](03-auto-installer.md) — instance create/destroy events.
- [AI Agent Profiles](14-agent-profiles.md) — identity → roles → tool view.
- [Service Graph](17-service-graph.md) — consumes the tool→instance mapping as
graph edges.
## Failure modes & handling
- **Upstream `tools/list` fails on ingest:** keep the last-known good tool set
for that instance, mark it stale, and retry with backoff; do not blank the
namespace on a transient error.
- **`list_changed` storm:** debounce/coalesce refreshes per instance so a
flapping upstream cannot thrash the hot path.
- **Instance vanishes:** `RemoveInstance` drops its tools atomically; in-flight
`Resolve` for a removed tool returns `ok=false`, and the Router returns a
clean JSON-RPC "unknown tool" error.
- **Namespace collision at registration:** reject the newer instance's tools,
emit an operator notification, and keep the existing namespace intact.
- **Cold start:** serve an empty catalog that fills as instances report; never
block the Gateway waiting for a full rebuild.
## Security notes
- Every `tools/list` and `GET /tools` response is RBAC-filtered — an agent
cannot even *see* a tool outside its role (Architecture §9, request scoping).
- Cached schemas contain no secret material; secrets live only in the running
MCP container (see [Secrets Manager](07-secrets-manager.md)).
- Namespaces are validated (charset/length) to prevent spoofing a well-known
namespace like `github`.
## Open questions
- Namespace assignment when two instances legitimately wrap the *same* service
type (two Postgres servers): auto-suffix (`postgres`, `postgres-2`) vs.
operator-chosen aliases?
- How aggressively to pre-fetch/refresh schemas vs. lazy-load on first
`tools/call`.
- Whether to surface tool *deprecation*/version metadata in the catalog for the
Update Manager and Dashboard.
## Milestone
Phase 1 (Walking skeleton), alongside the [Gateway](04-gateway.md). **Exit:**
with a filesystem MCP and one HTTP MCP registered in YAML, an agent's
`tools/list` shows `filesystem.*` and `other.*`, and `Resolve` maps a chosen
tool to its owning instance for a successful `tools/call`.