backend

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package backend defines the transport-agnostic connection to one MCP server (stdio subprocess or remote streamable-http endpoint).

Index

Constants

View Source
const (
	TransportStdio          = "stdio"
	TransportStreamableHTTP = "streamable-http"
	// TransportSSE is the legacy HTTP+SSE transport (2024-11-05): GET
	// event stream + POST-to-endpoint. Prefer streamable-http; several
	// hosted servers (Atlassian, Monday, Square…) still require this.
	TransportSSE = "sse"
)

Transport identifiers accepted in config.Backend.Transport.

Variables

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

ErrClosed is returned by Send after the backend terminated.

Functions

This section is empty.

Types

type Backend

type Backend interface {
	// Name is the configured backend name (used as tool prefix in
	// aggregation and as the audit backend label).
	Name() string
	// Start establishes the connection: it spawns the child or probes the
	// remote. Call it once before Send.
	Start(ctx context.Context) error
	// Send delivers one JSON-RPC message (a complete envelope) to the
	// server.
	Send(ctx context.Context, msg []byte) error
	// Recv yields messages from the server. The channel closes when the
	// backend terminates.
	Recv() <-chan []byte
	// Done closes when the backend has terminated for any reason.
	Done() <-chan struct{}
	// Err reports the termination cause after Done closes; nil on clean
	// shutdown.
	Err() error
	// Close terminates the connection and reaps any subprocess. Idempotent.
	Close() error
}

Backend is one live connection to an MCP server. One instance serves one MCP session, and the gateway serializes Send calls per session.

type ErrUnauthorized

type ErrUnauthorized struct {
	// Backend is the configured backend name.
	Backend string
	// Challenge is the verbatim WWW-Authenticate header ("" if absent).
	Challenge string
	// Body is a bounded excerpt of the response body, for diagnostics.
	Body string
}

ErrUnauthorized reports a 401 from a remote backend. Callers unwrap it to read the WWW-Authenticate challenge, which carries the resource-metadata URL that drives OAuth discovery (RFC 9728).

func (*ErrUnauthorized) Error

func (e *ErrUnauthorized) Error() string

type Factory

type Factory func(ctx context.Context) (Backend, error)

Factory builds a Backend for one session from its configuration.

func NewFactory

func NewFactory(name string, cfg config.Backend, ts func(ctx context.Context) (string, error)) Factory

NewFactory returns a Factory building one unstarted Backend per session from cfg.

ts, when non-nil, mints the outbound bearer token for each request to a remote backend. Static, passthrough and OAuth outbound auth all plug in here. Stdio backends ignore it and inherit credentials through their environment instead.

Configuration errors (unknown transport, missing command/url) surface from the returned Factory rather than from NewFactory, so a misconfigured backend fails its own session instead of the whole daemon's wiring. config.Validate already rejects the same conditions.

type RemoteBackend

type RemoteBackend struct {
	// TokenSource, when non-nil, mints the outbound bearer token for each
	// request. Passthrough and OAuth outbound auth plug in here, and the
	// minted token overrides any Authorization in the static headers.
	TokenSource func(ctx context.Context) (string, error)

	// Client overrides the per-instance HTTP client. Set before Start.
	Client *http.Client
	// contains filtered or unexported fields
}

RemoteBackend speaks MCP streamable-HTTP: every client message is POSTed to a single endpoint, and the remote answers with one JSON object or with an SSE stream carrying several messages. Send normalizes both shapes into the Recv channel, so callers cannot tell them apart.

func NewRemote

func NewRemote(name string, cfg config.Backend, log *slog.Logger) *RemoteBackend

NewRemote builds an unstarted streamable-HTTP backend. cfg.URL must be set; cfg.Headers are sent verbatim on every request.

func (*RemoteBackend) Close

func (b *RemoteBackend) Close() error

Close stops accepting new traffic, aborts in-flight streams and closes Recv. Idempotent.

func (*RemoteBackend) Done

func (b *RemoteBackend) Done() <-chan struct{}

func (*RemoteBackend) Err

func (b *RemoteBackend) Err() error

Err always returns nil. A remote backend has no asynchronous failure mode of its own: transport errors surface from Send, and only Close closes Done.

func (*RemoteBackend) Name

func (b *RemoteBackend) Name() string

func (*RemoteBackend) Recv

func (b *RemoteBackend) Recv() <-chan []byte

func (*RemoteBackend) Send

func (b *RemoteBackend) Send(ctx context.Context, msg []byte) error

Send POSTs one JSON-RPC message and routes whatever comes back into Recv. It returns once it has read the response headers and, for JSON responses, consumed the body. A background goroutine drains SSE bodies so a long tool call does not block the caller.

func (*RemoteBackend) SessionID

func (b *RemoteBackend) SessionID() string

SessionID exposes the remote-assigned Mcp-Session-Id, for tests and logs.

func (*RemoteBackend) Start

func (b *RemoteBackend) Start(_ context.Context) error

Start validates configuration and prepares the HTTP client. It issues no request: streamable-HTTP has no handshake of its own, so the first POST (the client's initialize) doubles as the connection probe.

type SSEBackend

type SSEBackend struct {
	// TokenSource, when non-nil, mints the outbound bearer token for each
	// request (stream GET and message POSTs alike). Overrides any
	// Authorization in the static headers.
	TokenSource func(ctx context.Context) (string, error)

	// Client overrides the per-instance HTTP client. Set before Start.
	Client *http.Client
	// contains filtered or unexported fields
}

SSEBackend speaks the legacy MCP HTTP+SSE transport (protocol revision 2024-11-05), which several hosted servers still require (Atlassian, Monday, Square, Replicate, …):

GET  <url>            → long-lived text/event-stream
  event: endpoint     → data carries the URL to POST messages to
  event: message      → data carries one JSON-RPC message
POST <endpoint URL>   → one JSON-RPC message; responses arrive on the
                        GET stream, not in the POST body (usually 202)

The newer streamable-HTTP transport (RemoteBackend) replaced this design. Point new integrations there and reach for SSEBackend only when the remote offers nothing else.

func NewSSE

func NewSSE(name string, cfg config.Backend, log *slog.Logger) *SSEBackend

NewSSE builds an unstarted legacy HTTP+SSE backend. cfg.URL is the SSE stream URL; cfg.Headers are sent on every request.

func (*SSEBackend) Close

func (b *SSEBackend) Close() error

Close aborts the stream and releases resources. Idempotent. The stream reader observes the aborted body and finishes the teardown (closing recv and done), so Close never races it on the channels.

func (*SSEBackend) Done

func (b *SSEBackend) Done() <-chan struct{}

Done implements Backend.

func (*SSEBackend) Err

func (b *SSEBackend) Err() error

Err reports why the stream terminated; nil on clean Close.

func (*SSEBackend) Name

func (b *SSEBackend) Name() string

Name implements Backend.

func (*SSEBackend) Recv

func (b *SSEBackend) Recv() <-chan []byte

Recv implements Backend.

func (*SSEBackend) Send

func (b *SSEBackend) Send(ctx context.Context, msg []byte) error

Send POSTs one JSON-RPC message to the endpoint URL. Responses arrive on the event stream rather than in the POST body, so any 2xx counts as success here.

func (*SSEBackend) Start

func (b *SSEBackend) Start(ctx context.Context) error

Start opens the SSE stream and returns once the response headers arrive. The endpoint event may follow later; Send waits for it separately.

type StdioBackend

type StdioBackend struct {
	// ShutdownGrace bounds each step of Close's escalation (stdin EOF,
	// SIGTERM, then SIGKILL). Zero means defaultShutdownGrace.
	ShutdownGrace time.Duration
	// contains filtered or unexported fields
}

StdioBackend runs an MCP server as a child process and speaks newline-delimited JSON-RPC over its stdin/stdout. It logs the child's stderr at debug level, since MCP servers use that stream for human-readable diagnostics rather than protocol traffic.

Start puts the child in its own process group so Close reaps the whole tree. The common launcher (`npx some-mcp-server`) forks a node grandchild that would otherwise survive and keep holding the pipes.

func NewStdio

func NewStdio(name string, cfg config.Backend, log *slog.Logger) *StdioBackend

NewStdio builds an unstarted stdio backend. cfg.Command must be non-empty; cfg.Env is layered over the parent environment.

func (*StdioBackend) Close

func (b *StdioBackend) Close() error

Close escalates until the child is gone: EOF on stdin (the polite exit signal for stdio MCP servers), SIGTERM to the process group, then SIGKILL. Signals target the group rather than the pid, so an `npx` launcher's node grandchild dies with it. Close blocks until reap finishes and tolerates repeated calls.

func (*StdioBackend) Done

func (b *StdioBackend) Done() <-chan struct{}

func (*StdioBackend) Err

func (b *StdioBackend) Err() error

func (*StdioBackend) Name

func (b *StdioBackend) Name() string

func (*StdioBackend) Recv

func (b *StdioBackend) Recv() <-chan []byte

func (*StdioBackend) Send

func (b *StdioBackend) Send(_ context.Context, msg []byte) error

Send writes one JSON-RPC message to the child's stdin.

func (*StdioBackend) Start

func (b *StdioBackend) Start(ctx context.Context) error

Start spawns the child. Cancelling ctx terminates the whole process group.

Jump to

Keyboard shortcuts

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