Documentation
¶
Overview ¶
Package source is the pluggable EXTENSIBILITY POINT for WHERE the harness's MCP server configs come from. A Source yields a set of mcp.ServerConfig values together with non-fatal per-server diagnostics (SkipError), and a fatal error only for a genuine infrastructure fault that prevented the source from being consulted at all.
LAYERING: this seam lives in this ADAPTER package, not the domain. Nothing in the domain or the agent loop consumes MCP server configs — they are connected and packaged into tools at composition time (cmd/mecated) — so a domain port would be the wrong home. The seam is scoped to where it is consumed (the composition root), mirroring skills.Source. The static --mcp-server list is one implementation (StaticSource); a live ToolHive workload inventory is another (ToolHiveSource); both satisfy this same interface and slot in without touching the consumer or the mcp.ServerConfig value object.
THIS PACKAGE IS SDK-FREE except for toolhive.go, which is the ONLY file allowed to import the ToolHive Go library. Everything else here — the seam, the static source, the resolver, the inspection types — is pure mecatl + stdlib so the layering audit stays simple.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Resolve ¶
func Resolve(ctx context.Context, sources []Source) (merged []mcp.ServerConfig, inventory []SourceInfo, skips []SkipError)
Resolve walks the resolved sources EXACTLY ONCE and returns three views built from that single pass:
- merged: the de-duplicated, cross-source-shadowed server configs, sorted by name — the SAME result MultiSource.Servers produces (earlier sources win a name collision; the dropped lower-precedence config becomes a shadow SkipError). This is the set the composition root connects.
- inventory: the per-source SourceInfo snapshot (identity, Kind, Group, the servers each source contributed pre-shadow, and its diagnostics) for the gRPC ListMcpSources / ListToolHiveGroups RPCs.
- skips: the aggregated cross-source diagnostics (each source's own skips, plus the shadow notices), in source order.
It exists to kill a double walk: previously the composition root called both InspectSources AND NewMultiSource(...).Servers, each of which consults every source — for the live ToolHive source that is two ListWorkloads container round-trips and, worse, two independent snapshots (so the reported inventory could differ from the connected servers). Resolve consults each source once, so both views are derived from the same snapshot.
Fail-soft contract is preserved: a source's fatal error is recorded as a diagnostic on that source's SourceInfo (and appended to skips) and that source contributes no servers to the merge, rather than aborting the whole resolution — matching the read-only, non-fatal philosophy our real sources already follow.
Types ¶
type MultiSource ¶
type MultiSource struct {
// contains filtered or unexported fields
}
MultiSource composes an ORDERED list of Sources into one, with a defined precedence on name collisions and aggregated diagnostics. It is what makes "keep adding sources" easy: future sources implement Source and slot into the ordered list — the consumer (registerMCP) is unchanged.
PRECEDENCE (collision rule): EARLIER sources win. When two sources both produce a server with the same ServerConfig.Name, the one from the earlier source is kept and the later one is SHADOWED — dropped, with a SkipError "shadowed by a higher-precedence source" notice so the operator can see it happened. Callers order the slice highest-precedence-first; the conventional resolver (ResolveSources) builds it as static (explicit --mcp-server) > ToolHive.
Diagnostics from every source are concatenated in source order, with each shadow notice appended at the point the collision is detected. A fatal error from ANY source is returned immediately (with the diagnostics gathered so far) — a source that genuinely could not be consulted is a fault worth surfacing, unlike a merely-absent one which its own implementation reports as "no servers".
func NewMultiSource ¶
func NewMultiSource(sources ...Source) MultiSource
NewMultiSource builds a MultiSource over the given ordered sources (highest-precedence first). nil entries are ignored so callers can assemble the slice conditionally without sprinkling nil checks.
func (MultiSource) Servers ¶
func (m MultiSource) Servers(ctx context.Context) ([]mcp.ServerConfig, []SkipError, error)
Servers aggregates every composed source, applies the earlier-wins precedence on name collisions, and returns the merged configs sorted by name for deterministic output. Shadowed lower-precedence configs are dropped and reported.
type ResolveOptions ¶
type ResolveOptions struct {
// StaticServers are the operator-configured --mcp-server entries, already
// parsed. Highest precedence.
StaticServers []mcp.ServerConfig
// ToolHiveEnabled adds the live ToolHive workload source (lower precedence than
// the static list). Default-on is decided by the composition root's flag, not
// here.
ToolHiveEnabled bool
// ToolHiveGroup is the ToolHive group to filter to. Empty -> the effective
// "default" group. Only consulted when ToolHiveEnabled.
ToolHiveGroup string
}
ResolveOptions configures the conventional MCP-source resolver. The zero value resolves NOTHING (no static servers, ToolHive disabled), so MCP stays opt-in unless the operator configured a server or left ToolHive on.
type ServerInfo ¶
ServerInfo is one resolved MCP server candidate (post-merge, post-shadow).
type SkipError ¶
type SkipError struct {
// Server is the server name (or source label) the problem concerns.
Server string
// Reason is a short, human-readable description of the problem.
Reason string
}
SkipError records one non-fatal diagnostic: a server a Source saw but excluded, or a server dropped because a higher-precedence source already claimed its name (a SHADOW notice). It is collected and surfaced by the composition root; it is NEVER returned as a Source's fatal error.
type Source ¶
type Source interface {
Servers(ctx context.Context) ([]mcp.ServerConfig, []SkipError, error)
// Name identifies the source for diagnostics, e.g. "static",
// "toolhive(default)", "toolhive(<group>)".
Name() string
}
Source produces MCP server configs for the composition root to connect.
Servers(ctx) returns:
- the server configs this source knows about (each a candidate connection),
- non-fatal per-server diagnostics (SkipError) for servers the source saw but deliberately excluded (wrong transport, bad name, …),
- a non-nil error ONLY for a hard INFRA fault that prevented consultation entirely. An ABSENT source (e.g. no container runtime, no configured servers) is "zero servers", NOT an error — MCP is opt-in and fail-soft.
func ResolveSources ¶
func ResolveSources(opts ResolveOptions) []Source
ResolveSources builds the ORDERED, highest-precedence-first Source list:
static (explicit --mcp-server, in flag order) [highest] > toolhive(<group or 'default'>) when enabled [lowest]
so an explicit --mcp-server always wins a name collision against a discovered ToolHive workload. The static source is always included (it is harmless when empty); the ToolHive source is included only when ToolHiveEnabled. The result is ready to hand to NewMultiSource.
type SourceInfo ¶
type SourceInfo struct {
// Name is the source's Name(), e.g. "static" or "toolhive(default)".
Name string
// Kind is the coarse source kind: "static" or "toolhive".
Kind string
// Enabled reports whether this source was active in the resolution.
Enabled bool
// Group is the ToolHive group (empty for the static source).
Group string
// Servers are the configs this source contributed (before cross-source
// shadowing — these are the source's own view).
Servers []ServerInfo
// Diagnostics are this source's per-server skip reasons.
Diagnostics []string
}
SourceInfo is the inventory for one resolved Source: its identity, the servers it contributed, and any diagnostics it raised. The name is deliberate (Stage C, the gRPC adapter, consumes source.SourceInfo) even though it reads as a stutter.
func InspectSources ¶
func InspectSources(ctx context.Context, sources []Source, _ ResolveOptions) []SourceInfo
InspectSources consults each resolved Source and returns the per-source inventory snapshot. It is now a thin wrapper over Resolve (which walks once and also produces the merged view); kept for existing callers/tests. opts is no longer consulted — Kind/Group are derived from the concrete source types — but retained in the signature for source compatibility.
type StaticSource ¶
type StaticSource struct {
// Servers is the pre-parsed config list (empty is fine — yields zero servers).
Configs []mcp.ServerConfig
}
StaticSource is the Source backing the operator-configured --mcp-server entries (already parsed into mcp.ServerConfig by the flag). It is the simplest possible Source: it just hands back the pre-parsed configs verbatim, with no diagnostics and no fatal error. It is the highest-precedence source in the conventional resolver so an explicit --mcp-server always wins a name collision against a discovered ToolHive workload.
func (StaticSource) Name ¶
func (StaticSource) Name() string
Name reports the source label used in diagnostics.
func (StaticSource) Servers ¶
func (s StaticSource) Servers(context.Context) ([]mcp.ServerConfig, []SkipError, error)
Servers returns the static configs unchanged. An empty list is "no servers", not an error.
type ToolHiveSource ¶
type ToolHiveSource struct {
// contains filtered or unexported fields
}
ToolHiveSource discovers MCP servers from the running ToolHive workloads in a group, reading each workload's already-populated HTTP proxy URL. Construction of the underlying ToolHive manager is LAZY (first Servers call) and ERROR-GUARDED: if no container runtime is reachable the source degrades to zero servers plus a single diagnostic, NOT a fatal error, so a developer without Podman/Docker can still run the harness.
func NewToolHiveSource ¶
func NewToolHiveSource(group string) ToolHiveSource
NewToolHiveSource builds a ToolHiveSource for the given group (empty -> the effective "default" group), backed by the real ToolHive manager. The manager is not constructed until the first Servers call.
func (ToolHiveSource) Name ¶
func (s ToolHiveSource) Name() string
Name reports the source label, e.g. "toolhive(default)" or "toolhive(<group>)".
func (ToolHiveSource) Servers ¶
func (s ToolHiveSource) Servers(ctx context.Context) ([]mcp.ServerConfig, []SkipError, error)
Servers lists the running ToolHive workloads in the configured group and maps each streamable-http workload to an mcp.ServerConfig.
Fail-soft contract:
- If the ToolHive manager cannot be constructed or the list call fails (no container runtime), return (nil, [one diagnostic], nil) — a degraded but non-fatal state. The harness keeps running with whatever static servers it has.
- A workload whose name contains "__" is skipped (it would corrupt the mcp__<server>__<tool> namespacing).
- A workload whose *effective proxy transport* is not streamable-http is skipped with a diagnostic. This is the proxy mode clients speak (w.ProxyMode), NOT the backend transport: a stdio-backed workload proxied as streamable-HTTP is mapped; only an SSE proxy is skipped (mecatl's MCP client is streamable-HTTP only — no SSE).