Skip to main content

Module server

Module server 

Source
Expand description

The reusable MCP server base: transport, framing, connection handling, the lifecycle/version machinery, and the resource-subscription registry — agentd’s served self-MCP (and any other embedder’s server) builds its domain surface on top by implementing Handler.

The split mirrors the client: this module owns the protocol (how bytes become requests, how initialize / server/discover / ping are answered across both eras, how a subscriber is pushed a notifications/resources/updated), while the embedder owns the domain (which tools exist, which resources are readable, who may subscribe to what). One Handler trait is the seam.

Transport is deliberately minimal and dependency-light (RFC 0015 §3.6): a blocking listener, one thread per connection, speaking the same NDJSON JSON-RPC codec (crate::rpc::frame) as the client. No async, no mio. ServeStream type-erases unix vs. vsock so the framing, threading, and dispatch are entirely transport-agnostic (“the unix server with the socket type swapped”).

Structs§

Subscriber
A peer subscribed to a resource: which connection, and the writer to push a notifications/resources/updated to. Opaque — fields are private; construct + mutate a registry through register_subscriber / drop_subscription / remove_conn_subscriptions and fire pushes through the notify_* helpers.

Enums§

PeerOrigin
Which transport a connection arrived on, and therefore its trust domain (RFC 0015 §3.3-§3.4). A generic two-domain model the framework only carries and hands to the Handler; the embedder assigns meaning:
ServeStream
The served-MCP transport, type-erased to one concrete enum so the connection registry (SharedWriter, Subscriber) stays monomorphic across transports while the same connection code serves each. The socket variants are Read + Write with a try_clone (their write half is shared with the threads that push NDJSON notifications). The Http variant is a write-only SSE sink: an HTTP subscription stream’s write half, so an HTTP subscriber registers in the SAME SubRegistry and the embedder’s existing notify_* calls reach it transparently — the framing (NDJSON vs SSE data: events) is chosen per-variant in write_notification.

Traits§

Handler
The embedder’s domain seam. The framework owns the transport, the framing, the connection lifecycle, and the subscription registry; the Handler supplies the meaning — which tools/resources exist, who may call/read/subscribe to what.

Functions§

bind_unix
Bind a unix socket for serving, clearing any stale socket file first. Returned separately from the accept loop so the caller can log/act on a successful bind (or propagate the bind error) before the accept thread starts.
broadcast_distinct
Broadcast a payload-free note to every DISTINCT writer currently in the registry — for connection-scoped notifications that aren’t tied to a single uri (e.g. notifications/tools/list_changed after a hot reload changed the tool set). A connection subscribed to several resources is written to once. Dead writers are pruned by their own reader loop.
drop_subscription
Drop conn’s subscription to a single uri (the resources/unsubscribe path). Prunes the uri entry entirely once its last subscriber leaves.
handle_conn
Serve one accepted connection to completion: the blocking NDJSON read loop. Requests get a reply (through the shared writer, which a background thread may also push notifications on — the Mutex serializes them); notifications (initialized, …) are read and dropped. On EOF/hangup the connection’s subscriptions are dropped so no push ever targets a dead socket. A write timeout bounds a stalled-but-alive peer so it can’t pin the writer Mutex (and a pushing thread) forever.
lifecycle_response
Answer the three lifecycle methods every MCP server must handle, in ONE place, version-aware across both eras — the server-side mirror of the client’s version negotiation. Returns Some(response) for initialize / server/discover / ping, or None if req.method is a domain method the Handler must route.
notify_resource_updated
Push notifications/resources/updated{uri} to every current subscriber of uri, consuming the subscription list (the resource changes exactly once — e.g. a subagent run reaching its terminal status — so no entry should linger after its one event). Best-effort: a write to a dead peer fails and is cleaned up when that connection’s reader loop ends. The lock is released before writing, so a slow/blocked peer can’t stall other notifications.
notify_resource_updated_keep
Like notify_resource_updated but keeps the subscriber list — for resources that change REPEATEDLY (a run aggregate on each spawn, a warm session on each turn boundary, config/effective on each reload, an event ring on each batch). Cloning the writers under the lock (then releasing it before writing) keeps the entry intact for the next emission. Dead peers are pruned when their reader loop ends (remove_conn_subscriptions).
register_subscriber
Register conn (with its writer) as a subscriber of uri, idempotently — a second subscribe from the same connection is a no-op rather than a duplicate push target. The embedder does its own gating (which URIs are subscribable, who may subscribe) before calling this.
remove_conn_subscriptions
Drop every subscription held by a (now-closed) connection — called when a connection’s reader loop ends so pushes never target a dead socket.
spawn_accept_unix
Spawn the background accept thread for listener: one blocking thread per connection, each running handle_conn against handler. Peers arrive in the PeerOrigin::Management trust domain (they dialed a listener). Returns once the accept thread is spawned; a thread-spawn failure is surfaced as the error.

Type Aliases§

SharedWriter
A connection’s shared write half — both replies and pushed notifications go through it, serialized by the Mutex (a reply and a notification can’t interleave bytes). The ServeStream enum keeps this one type across unix + vsock peers.
SubRegistry
uri → its subscribers. Pushed when a resource changes. Arc-shared with the background threads that mutate resource state (a run reaching a terminal status, a reload landing, an event-ring growth).