stdio

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package stdio is the MCP stdio transport: it runs an MCP server as a child process and speaks the protocol over that child's stdin and stdout.

What this transport owns

It owns the process. The MCP Go SDK ships a command transport that also does (it takes an *exec.Cmd and starts it), and this package deliberately does not use it: that transport starts the command itself — leaving no seam for a host that confines its servers — and signals only the process it started, which leaves anything the server spawned running with the pipes it inherited. This package therefore starts, groups, terminates and reaps the child itself, and uses the SDK for what the SDK is for: framing and the protocol.

Trust

The child is untrusted from the moment it exists. Its stdout is the protocol and is read by nothing but the SDK; its stderr is bounded diagnostics; its exit status is a fact to report, never a fact to believe. Its environment is built from an allowlist rather than inherited, because a server that does not need a credential must not be handed every credential this process holds.

Shutdown

Shutdown is graceful by necessity, not by politeness. The MCP stdio shutdown is "close the child's stdin", and an SDK-based server treats a read of EOF as "stop now" — dropping any reply it had not written yet and exiting on an error. So Close drains the MCP session first (no new requests, in-flight ones finish), and only then closes the stream, terminates the group and reaps it.

Index

Constants

View Source
const DefaultStderrLimit = 8 << 10

DefaultStderrLimit is the stderr capture size used when Config.StderrLimit is zero. It is generous enough to hold a stack trace and small enough that a server looping on its error path costs nothing.

Variables

This section is empty.

Functions

func New

func New(cfg Config) (client.TransportFactory, error)

New validates cfg and returns a stdio TransportFactory.

It fails closed: every violation is an *client.Error of class FailureInvalidConfig. Config errors name the offending field, and the command — which is not a secret — but never an argument's or a variable's value, which may be.

No process is started until Connect. The COMMAND, though, is resolved here: New calls exec.LookPath, which reads $PATH and yields the absolute path stored on the factory (see Config.Command). That is deliberate — a command that does not exist is a configuration error, and it is worth learning at construction rather than on a connection attempt later.

Config.Env is the opposite and stays so: New only validates the allowlist's shape, and the variables' VALUES are read from the environment at Connect, so a factory built early does not capture an environment that has since changed.

Types

type Config

type Config struct {
	// Command is the MCP server executable: either an absolute path, or a bare
	// name resolved on PATH. It is never a shell string — this transport does
	// not have a shell to give it to — and a relative path is rejected, because
	// what it names depends on a working directory the caller did not state.
	//
	// It is resolved once, by New, so a command that does not exist is a
	// configuration error at construction rather than a connection failure
	// later.
	Command string
	// Args are the command's arguments, passed to execve as separate strings.
	// An argument may carry a secret, so args never appear in RedactedOrigin or
	// in any error this package produces.
	Args []string
	// Dir is the child's working directory. It must be absolute if set; empty
	// means this process's working directory.
	Dir string
	// Env is the child's environment allowlist. The zero value gives the child
	// no environment at all.
	Env EnvAllowlist
	// Launcher creates the process. Nil means a plain argv exec, with the child
	// leading its own process group. A host that confines its servers supplies
	// its own; see ProcessLauncher.
	Launcher ProcessLauncher
	// StderrLimit bounds the child's captured stderr, in bytes. Zero selects
	// DefaultStderrLimit; a negative value is a configuration error. The
	// capture keeps the most recent bytes and drops the rest.
	StderrLimit int
}

Config configures a stdio transport.

type EnvAllowlist

type EnvAllowlist struct {
	// Vars are explicit name/value pairs. A name here wins over the same name
	// in PassThrough.
	Vars []Var
	// PassThrough names variables to copy from this process's environment, if
	// they are set. An unset name is simply absent from the child; it is not an
	// error, because "PATH if there is one" is a legitimate thing to ask for.
	PassThrough []string
}

EnvAllowlist is the child's environment, built from nothing.

This is an allowlist and not a filter, and the difference is the whole point: a variable that is not named here is absent from the child, so a credential this process holds for some other purpose cannot reach a server by default. The zero value gives the child an empty environment.

type ExitStatus

type ExitStatus struct {
	// Code is the exit status, or -1 when a signal ended the process.
	Code int
	// Signal names the signal that ended the process ("terminated",
	// "killed", ...), and is empty when the process exited on its own.
	Signal string
}

ExitStatus is how a child process ended, in neutral terms. It never names a syscall type, so a launcher on any platform can report one.

func (ExitStatus) String

func (s ExitStatus) String() string

String renders the status for a diagnostic message.

type Process

type Process interface {
	// Pid reports the process id, for diagnostics only.
	Pid() int
	// Terminate asks the process, and everything it spawned, to shut down.
	Terminate() error
	// Kill destroys the process, and everything it spawned, unconditionally.
	Kill() error
	// Wait blocks until the process has exited and been reaped, and reports how
	// it ended. It is called exactly once. A non-nil error means the process
	// could not be reaped — a non-zero exit is a status, not an error.
	Wait() (ExitStatus, error)
}

Process is a started child process, owned by whoever started it.

It is an interface rather than a concrete handle because termination is part of confinement, not just of exec: a launcher that puts the server in a jail, a cgroup or a container is the only thing that knows how to tear that down, and forcing this module's Kill (a signal to a process group) onto it would be wrong for every launcher whose child is not in this process's group at all. The seam is therefore "start it and destroy it", not "start it and let us signal it".

Every method must tolerate being called after the process has already exited: termination races the process's own death by nature. Terminate and Kill report an error only when the request could not be delivered for a reason other than the process being gone.

type ProcessLauncher

type ProcessLauncher interface {
	Start(ctx context.Context, spec ProcessSpec) (Process, error)
}

ProcessLauncher creates child processes. The default launcher runs the command directly with os/exec; an application that confines its MCP servers supplies its own, which is why this interface names nothing but the stdlib: this module never imports a confinement mechanism, it delegates to one.

Start must honor ctx for the start itself. It must not tie the child's lifetime to ctx: ctx bounds connecting, and the process outlives it. Killing the child when the connect context is cancelled is this transport's job, done through Process.

type ProcessSpec

type ProcessSpec struct {
	// Path is the absolute path of the executable to run. It has already been
	// resolved (via PATH if the configured command was a bare name).
	Path string
	// Args are the arguments after the program name, exactly as os/exec takes
	// them. They are untrusted display-wise — an argument may carry a secret —
	// so a launcher must not log them.
	Args []string
	// Dir is the child's working directory. Empty means the parent's.
	Dir string
	// Env is the child's complete environment, built from an allowlist. An
	// empty (but non-nil) Env means the child gets no environment at all; a
	// launcher must never substitute the parent's for it.
	Env []string
	// Stdin, Stdout and Stderr are the child's three streams. Stdin and Stdout
	// are the MCP transport and carry nothing else. The launcher passes them to
	// the child and does not read, write or close them.
	Stdin  *os.File
	Stdout *os.File
	Stderr *os.File
}

ProcessSpec is the complete, argv-only description of a child process. It is a value: a launcher may read it, but nothing here is a live handle onto this transport's state except the three files, which the child inherits.

There is no shell anywhere in this type, and no field a shell could be smuggled through: Path names an executable and Args are its arguments, passed to execve as separate strings.

type Var

type Var struct {
	// Name is the variable name. It must be non-empty and contain no '=' or
	// NUL byte.
	Name string
	// Value is the value, which may be a secret: it is never logged, never put
	// in an error, and never part of RedactedOrigin.
	Value string
}

Var is one explicit environment entry for the child.

Jump to

Keyboard shortcuts

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