registry

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package registry manages the gateway's downstream MCP sessions: a bounded, supervised stdio subprocess pool per downstream plus remote HTTP sessions (design §4, §6.1, ADR-0002/0004/0006). This file implements the transport- agnostic pool; the stdio and HTTP session factories that back it are built on top of it.

Index

Constants

This section is empty.

Variables

View Source
var ErrBroken = errors.New("downstream broken: too many consecutive failures")

ErrBroken is returned by SupervisedFactory.New while the wrapped downstream is in its broken cooldown: too many consecutive New failures tripped the breaker, so calls fast-fail without spawning until the cooldown elapses (design §5).

View Source
var ErrClientSessionClosed = errors.New("client session closed")

ErrClientSessionClosed is returned by Dispatch after the ClientSession has been closed.

View Source
var ErrClosed = errors.New("pool closed")

ErrClosed is returned by Acquire after the pool has been closed.

View Source
var ErrPoolExhausted = errors.New("downstream pool exhausted")

ErrPoolExhausted is returned by Acquire when no session is available within the acquire timeout. The call fails fast rather than queueing unboundedly (design §4, ADR-0004).

Functions

This section is empty.

Types

type ClientSession

type ClientSession struct {
	// contains filtered or unexported fields
}

ClientSession is one client connection's set of downstream sessions and the domain.Dispatcher for that connection. It is safe for concurrent Dispatch. Close must not run concurrently with Dispatch: the composition root drains in-flight calls before closing a client session (design §5 shutdown order).

func (*ClientSession) Close

func (cs *ClientSession) Close() error

Close discards every cached per_client session, returning each to its pool, and marks the client session closed. It is idempotent. Shared-mode downstreams hold nothing between calls, so they need no teardown here.

func (*ClientSession) Dispatch

func (cs *ClientSession) Dispatch(ctx context.Context, call *domain.Call) (*domain.Result, error)

Dispatch routes a call to this client session's session for the call's downstream and executes it. An unknown downstream is a clean error, never a panic (design §7). In Shared mode the session is acquired from the pool for the call and returned afterward (or discarded if the call failed at the transport); in PerClient mode the session is cached for reuse across the client session's calls and discarded (so the next call re-acquires) only if a call fails at the transport.

type DownstreamSession

type DownstreamSession interface {
	Session
	// CallTool invokes a downstream tool by its un-namespaced name with the given
	// raw JSON arguments, returning the result for the outbound pipeline.
	CallTool(ctx context.Context, tool string, args json.RawMessage) (*domain.Result, error)
	// ListTools returns one page of the downstream's tools plus the next
	// pagination cursor ("" when the listing is complete).
	ListTools(ctx context.Context, cursor string) (tools []ToolInfo, next string, err error)
	// Ping checks that the downstream is responsive.
	Ping(ctx context.Context) error
}

DownstreamSession is a live session to one downstream MCP server. It extends the pool's Session (which only needs Close) with the operations the dispatcher performs against a downstream: calling tools, discovering them, and pinging for liveness. It is transport-agnostic — both the stdio (subprocess) and remote-HTTP session types implement it.

type Factory

type Factory interface {
	New(ctx context.Context) (Session, error)
}

Factory creates a new downstream session, spawning whatever transport backs it (an stdio subprocess, a remote HTTP connection).

type HTTPConfig

type HTTPConfig struct {
	// URL is the downstream's MCP endpoint (the Streamable HTTP endpoint).
	URL string
	// Headers are connection-level headers set on every request to the
	// downstream, including any resolved connection-level secrets such as an
	// Authorization bearer token. The composition root resolves each declared
	// secret's env var to a value and passes it here, so the client never holds
	// the credential (ADR-0012). The gateway never reads these back to a client.
	Headers map[string]string
	// HTTPClient optionally overrides the HTTP client backing the transport (for
	// example to set timeouts or a custom dialer). When nil a zero-value client is
	// used. Configured Headers are injected regardless of which client backs it.
	HTTPClient *http.Client
}

HTTPConfig configures a remote Streamable HTTP downstream transport.

type HTTPFactory

type HTTPFactory struct {
	// contains filtered or unexported fields
}

HTTPFactory creates remote-HTTP DownstreamSessions and satisfies the pool's Factory interface, so a supervised pool of remote sessions is just NewPool(NewHTTPFactory(cfg), poolCfg) — the same composition as stdio.

func NewHTTPFactory

func NewHTTPFactory(cfg HTTPConfig) *HTTPFactory

NewHTTPFactory returns a factory that dials the configured downstream endpoint for each new session.

func (*HTTPFactory) New

func (f *HTTPFactory) New(ctx context.Context) (Session, error)

New dials the downstream endpoint, performs the MCP handshake, and returns a ready DownstreamSession. Connection-level headers (including injected secrets) are applied to every request via a wrapping RoundTripper, so the credential reaches the downstream without ever being supplied by the client. If the handshake fails the SDK tears the transport down before New returns the error.

type ManagedDownstream

type ManagedDownstream struct {
	Name string
	Mode SessionMode
	Pool *Pool
}

ManagedDownstream registers one downstream with the Manager: its name, its session-sharing mode, and the bounded pool that backs it. The composition root builds the pool from the downstream's supervised factory and pool config.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns the gateway's downstream pools and hands each client connection its own ClientSession. There is one Manager per gateway; it is safe for concurrent use. A background reaper closes held per_client sessions that have gone idle past their downstream's idle_ttl (design §4): such sessions stay in-use from the pool's view and so never face the pool's own idle reaper.

func NewManager

func NewManager(downstreams []ManagedDownstream) (*Manager, error)

NewManager builds a Manager from the managed downstreams and starts its idle reaper. It fails fast on an empty downstream name, a nil pool, or a duplicate name, so a wiring mistake surfaces at startup rather than at request time.

func (*Manager) Close

func (m *Manager) Close() error

Close stops the reaper and closes every downstream pool. Call it during graceful shutdown after all client sessions have been closed; it returns the first close error, if any, and is idempotent.

func (*Manager) NewClientSession

func (m *Manager) NewClientSession() *ClientSession

NewClientSession returns a fresh per-connection session set and registers it with the reaper. Each client session implements domain.Dispatcher over its own downstream sessions, so a call dispatched on one client session can never reach another's (ADR-0013).

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is a bounded, idle-reaping pool of downstream sessions. At most Max sessions are live at once; an Acquire past Max waits up to AcquireTimeout then fails fast. It is safe for concurrent use.

The permit channel enforces the bound and gives Acquire a clean timeout; the mutex guards the idle set and counters. Idle sessions hold no permit, so the reaper can close them independently.

func NewPool

func NewPool(factory Factory, cfg PoolConfig) *Pool

NewPool builds a pool and starts its idle reaper.

func (*Pool) Acquire

func (p *Pool) Acquire(ctx context.Context) (Session, error)

Acquire returns a session, reusing an idle one or creating a new one. If Max sessions are already live it waits up to AcquireTimeout, then returns ErrPoolExhausted. It respects context cancellation.

func (*Pool) Close

func (p *Pool) Close() error

Close stops the reaper and closes all idle sessions. In-use sessions are closed when the caller returns them; the composition root drains in-flight calls before closing the pool.

func (*Pool) Discard

func (p *Pool) Discard(sess Session)

Discard closes a session (e.g. one whose subprocess crashed) and frees its slot so a fresh session can take its place. Use it instead of Release for an unhealthy session.

func (*Pool) IdleTTL

func (p *Pool) IdleTTL() time.Duration

IdleTTL is how long an idle session is kept before being reaped. The Manager reads it to reap held per_client sessions on the same schedule, since those stay in-use from the pool's view and so never face the pool's own reaper.

func (*Pool) Release

func (p *Pool) Release(sess Session)

Release returns a healthy session to the idle set for reuse.

func (*Pool) Stats

func (p *Pool) Stats() Stats

Stats returns a snapshot of pool occupancy. After Close the counts are best-effort (the pool is shutting down and no longer maintains its invariants for observability).

type PoolConfig

type PoolConfig struct {
	// Max is the maximum number of live sessions.
	Max int
	// AcquireTimeout bounds how long Acquire waits for a slot before failing fast.
	AcquireTimeout time.Duration
	// IdleTTL is how long an idle session is kept before being reaped.
	IdleTTL time.Duration
}

PoolConfig bounds and tunes a pool. Zero fields fall back to package defaults.

type Session

type Session interface {
	Close() error
}

Session is a poolable downstream session. The pool only needs to be able to close one; richer behavior (calling tools, pinging) is added by the concrete session types built on the pool.

type SessionMode

type SessionMode int

SessionMode selects how a downstream's sessions are shared across client sessions (design §4, ADR-0002).

const (
	// PerClient gives each client session its own dedicated session to the
	// downstream. It is the default and is correct for stateful servers: a client
	// session reuses one held session across its calls.
	PerClient SessionMode = iota
	// Shared checks a session out of and back into the pool per call, so a
	// stateless downstream's sessions cycle across client sessions.
	Shared
)

type Stats

type Stats struct {
	InUse   int
	Idle    int
	Created int64
}

Stats is a snapshot of a pool's occupancy.

type StdioConfig

type StdioConfig struct {
	// Command is the downstream's argv; Command[0] is the executable.
	Command []string
	// Env are extra environment variables for the child, layered over the
	// gateway's own environment so PATH and friends still resolve. Includes
	// resolved connection-level secrets.
	Env map[string]string
	// TerminateDuration is how long Close waits after closing stdin before
	// escalating to SIGTERM. Zero uses the SDK default.
	TerminateDuration time.Duration
}

StdioConfig configures a downstream subprocess transport. Env is the full set of extra environment variables handed to the child, including any resolved connection-level secrets — the composition root resolves each declared secret's env var to a value and passes it here (ADR-0012).

type StdioFactory

type StdioFactory struct {
	// contains filtered or unexported fields
}

StdioFactory creates stdio DownstreamSessions and satisfies the pool's Factory interface, so a bounded pool of downstream subprocesses is just NewPool(NewStdioFactory(cfg), poolCfg).

func NewStdioFactory

func NewStdioFactory(cfg StdioConfig) *StdioFactory

NewStdioFactory returns a factory that spawns the configured downstream command for each new session.

func (*StdioFactory) New

func (f *StdioFactory) New(ctx context.Context) (Session, error)

New spawns the downstream subprocess, performs the MCP handshake, and returns a ready DownstreamSession. The child is started in its own process group with platform-appropriate death signaling (ADR-0006), and its environment is the gateway's environment plus the configured Env (so injected secrets reach the child while inherited variables like PATH still resolve). If the handshake fails the partially started transport is torn down by the SDK before New returns the error.

type SupervisedFactory

type SupervisedFactory struct {
	// contains filtered or unexported fields
}

SupervisedFactory wraps a Factory with crash-loop storm prevention. While healthy it forwards every New to the delegate. After MaxConsecutiveFailures consecutive failures it trips a breaker: subsequent New calls fast-fail with ErrBroken without touching the delegate until BrokenCooldown elapses. The first New after the cooldown is a single probe that calls the delegate again; a successful probe clears the breaker, and a failed probe re-arms the cooldown.

It satisfies Factory, so it composes with the pool by wrapping any factory:

NewPool(NewSupervisedFactory(stdioFactory, supCfg), poolCfg)

It is safe for concurrent use; multiple pool Acquires call New concurrently.

func NewSupervisedFactory

func NewSupervisedFactory(delegate Factory, cfg SupervisorConfig) *SupervisedFactory

NewSupervisedFactory wraps delegate with crash-loop storm prevention. Zero or negative config fields fall back to package defaults.

func (*SupervisedFactory) New

New returns a session from the delegate, or storm-prevents. While the breaker is open and its cooldown has not elapsed it returns ErrBroken immediately, without calling the delegate. Otherwise it calls the delegate: success resets the consecutive-failure count and clears the breaker; failure increments the count and, on reaching MaxConsecutiveFailures, opens the breaker for BrokenCooldown. The first New after the cooldown is a single probe — a successful probe clears the breaker, a failed probe re-arms the cooldown. The recovery probe is single-flight: while broken, exactly one caller past the cooldown reaches the delegate and the rest fast-fail with ErrBroken, so a burst at the cooldown boundary cannot become its own little spawn storm. When healthy, New does not serialize — concurrent spawns proceed in parallel.

type SupervisorConfig

type SupervisorConfig struct {
	// MaxConsecutiveFailures is how many consecutive New failures trip the breaker.
	// The failure that reaches this count is the one that marks the downstream
	// broken (it still returns the delegate's error; the next call fast-fails).
	MaxConsecutiveFailures int
	// BrokenCooldown is how long the breaker stays open before allowing a single
	// probe attempt.
	BrokenCooldown time.Duration
}

SupervisorConfig tunes a SupervisedFactory. A field that is <= 0 falls back to the corresponding package default.

type ToolInfo

type ToolInfo struct {
	// Name is the downstream's own tool name (not yet namespaced).
	Name string
	// Title is the tool's optional human-readable display name.
	Title string
	// Description is the human-readable tool description, if any.
	Description string
	// InputSchema is the tool's JSON Schema as raw JSON, or nil if the downstream
	// advertised none. Kept as raw bytes so it round-trips unmodified.
	InputSchema json.RawMessage
	// OutputSchema is the tool's optional structured-result JSON Schema as raw
	// JSON, or nil. Clients validate structured results against it.
	OutputSchema json.RawMessage
	// Annotations is the tool's optional annotations (display and behavior hints)
	// as raw JSON, or nil. Kept opaque; the gateway does not interpret them.
	Annotations json.RawMessage
	// Icons is the tool's optional icon set as raw JSON, or nil.
	Icons json.RawMessage
}

ToolInfo is a downstream's raw, un-namespaced tool as discovered over the wire. The composition root maps these to the gateway's aggregate listing, applying the per-downstream namespace; this package stays transport-level and leaves namespacing and policy filtering to higher layers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL