Documentation
¶
Overview ¶
Package proxy is the MCP stdio pump: it sits between an MCP client and the governed server, forwarding newline-delimited JSON-RPC frames byte-for-byte while observing them, and giving the harness two abilities the transport alone would not: injecting its own requests toward the server (with their responses routed out of band, never to the client) and — in later phases — replacing a frame with a synthesized refusal.
Byte-fidelity is the default and the exception is narrow: the only frames ever rewritten are client requests whose id trespasses on the harness's reserved namespace (see reservedPrefix), which would otherwise let a client collide with an injected request's id and confuse response routing.
Index ¶
Constants ¶
const MaxFrameBytes = 64 * 1024 * 1024
MaxFrameBytes bounds one MCP stdio line. Tool results legitimately carry base64 screenshots, so this is far larger than the control channel's limit.
Variables ¶
var ErrFrameTooLong = errors.New("proxy: frame exceeds MaxFrameBytes")
ErrFrameTooLong reports a line over MaxFrameBytes on either stream.
Functions ¶
This section is empty.
Types ¶
type Interceptor ¶ added in v0.3.0
Interceptor can refuse a client→server request. For each client frame it returns a non-nil response frame to refuse — that frame is written to the client verbatim and the request is NOT forwarded to the server — or nil to forward normally. This is the seam the policy engine sits on: the governed server never sees a request the harness refuses.
Intercept runs on the client→server pump goroutine, so it must not block indefinitely; a policy decision that needs to wait (an approval, a signal round-trip) is the caller's responsibility to bound.
The same seam answers calls to harness-injected tools: for a tools/call of a tool the harness added to the manifest (see ToolInjector), Intercept returns the tool's result frame — a success, not a refusal — so the call is answered by the harness and never reaches the server, which does not have that tool.
type NopObserver ¶
type NopObserver struct{}
NopObserver observes nothing.
func (NopObserver) OnClientFrame ¶
func (NopObserver) OnClientFrame([]byte)
func (NopObserver) OnServerFrame ¶
func (NopObserver) OnServerFrame([]byte)
type Observer ¶
type Observer interface {
// OnClientFrame sees each client→server frame before forwarding.
OnClientFrame(raw []byte)
// OnServerFrame sees each server→client frame before forwarding. Frames
// consumed as injected-request responses are not reported.
OnServerFrame(raw []byte)
}
Observer sees every frame. Callbacks run on the pumping goroutine for their direction: cheap work only, and no re-entrant Inject calls.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy pumps frames between a client (stdin/stdout of the harness) and a server (stdin/stdout of the child process).
func New ¶
func New(clientIn io.Reader, clientOut io.Writer, serverIn io.Writer, serverOut io.Reader, obs Observer) *Proxy
New builds a proxy over the four streams. obs must not be nil; use NopObserver.
func (*Proxy) Inject ¶
Inject sends a harness-originated request to the server and returns the raw response frame. The response is consumed by the pump and never reaches the client. body is the request WITHOUT an id; Inject assigns one from the reserved namespace.
func (*Proxy) Run ¶
Run pumps both directions until either stream ends or ctx is canceled. A clean client EOF (the MCP host closed stdin) returns nil; anything else returns the first error. Both directions stop before Run returns.
func (*Proxy) SetInterceptor ¶ added in v0.3.0
func (p *Proxy) SetInterceptor(i Interceptor)
SetInterceptor installs (or replaces) the refusal seam. It is safe to call while Run is pumping — the enforcement decider is built once the servant connects, after the pump has started — so a session can begin fail-closed and swap to the policy decider without a restart.
func (*Proxy) SetToolInjector ¶ added in v0.7.0
func (p *Proxy) SetToolInjector(i ToolInjector)
SetToolInjector installs (or replaces) the tool-injection seam. Safe to call while pumping, for the same reason SetInterceptor is: the injected manifest is decided once the servant connects.
type ToolInjector ¶ added in v0.7.0
type ToolInjector interface {
Tools() []json.RawMessage
}
ToolInjector adds tools to the manifest the client sees. Tools returns the tool definitions (each the raw JSON of one MCP Tool object) to append to every tools/list result the server sends. The client's manifest is then the server's tools plus these, and the observer fingerprints that combined surface — so the injected tools sit inside the rug-pull baseline, not outside it. Calls to the injected tools are answered by the Interceptor.