envmcp

package
v0.60.10 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package envmcp implements the `codex-app-gateway env-mcp` subcommand: a stateless MCP server that codex spawns as a child process. It exposes a fixed tool set (list_environments, shell, exec_command, write_stdin, read_output, terminate, read_file, apply_patch) to codex; tool calls are multiplexed across the workspace's connected executors via a per-exe BridgeClient pool keyed by environment name.

Index

Constants

View Source
const (
	ExecMethodInitialize       = "initialize"
	ExecMethodInitialized      = "initialized" // notification
	ExecMethodProcessStart     = "process/start"
	ExecMethodProcessRead      = "process/read"
	ExecMethodProcessWrite     = "process/write"
	ExecMethodProcessTerminate = "process/terminate"
	ExecMethodProcessExited    = "process/exited" // notification (informational; we poll instead)
	ExecMethodProcessClosed    = "process/closed" // notification (informational)
	ExecMethodFsReadFile       = "fs/readFile"
	ExecMethodFsWriteFile      = "fs/writeFile"
	ExecMethodFsRemove         = "fs/remove"
	ExecMethodFsCopy           = "fs/copy"
)

Method names — must match codex-rs/exec-server/src/protocol.rs.

Variables

View Source
var ErrRelayDisabled = fmt.Errorf("relay: HTTP relay path disabled (no exec-gateway-internal-url or secret)")

ErrRelayDisabled is returned by CreateRelay when the gateway URL or internal secret is empty (config-disabled).

Functions

func ApplyHunks added in v0.51.0

func ApplyHunks(source string, hunks []PatchHunk) (string, error)

ApplyHunks applies a list of hunks to a source file body, returning the resulting body. Context (and removed) lines must match the source byte-for-byte.

func Run

func Run(ctx context.Context, args RunArgs, stdin io.Reader, stdout, stderr io.Writer, logger *slog.Logger) error

Run constructs the BridgePool, builds the tool registry, and serves the MCP loop on stdin/stdout until EOF or context cancellation.

stdout is the MCP JSON-RPC stream; do not write to it from outside MCPServer.Serve. Diagnostic output flows through logger (gateway supervisor pipes our stderr into the pod's stderr with a `[codex-subproc]` prefix). The `stderr` parameter is reserved for future direct writes (e.g., panic dumps) and currently unused.

Types

type ApplyPatchTool added in v0.51.0

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

ApplyPatchTool implements `apply_patch`. The patch text is parsed locally (in env-mcp) into structured FileOps; each op is then translated into fs/readFile + fs/writeFile + fs/remove + fs/copy RPCs on the remote.

Per-file outcomes are reported as `path: ok` / `path: error: ...` lines so the LLM sees which files succeeded even on partial failure.

func NewApplyPatchTool added in v0.51.0

func NewApplyPatchTool(pool *BridgePool, resolver *NameResolver) *ApplyPatchTool

func (*ApplyPatchTool) Call added in v0.51.0

func (*ApplyPatchTool) Description added in v0.51.0

func (t *ApplyPatchTool) Description() string

func (*ApplyPatchTool) InputSchema added in v0.51.0

func (t *ApplyPatchTool) InputSchema() json.RawMessage

func (*ApplyPatchTool) Name added in v0.51.0

func (t *ApplyPatchTool) Name() string

type BridgeClient

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

BridgeClient wraps one WebSocket connection to /bridge/{exe_id} on codex-exec-gateway and exposes a JSON-RPC client interface that env-mcp uses to talk codex's exec-server protocol.

Concurrency model: a single background goroutine reads frames and dispatches them to a per-id reply channel; Call() blocks on its channel until the goroutine delivers, the context is cancelled, or the connection closes.

func DialBridge

func DialBridge(ctx context.Context, wsURL, authToken string, logger *slog.Logger) (*BridgeClient, error)

DialBridge dials wsURL and, when authToken is non-empty, sets `Authorization: Bearer <authToken>` on the upgrade request. Returns once the WebSocket handshake completes; subsequent reads are pumped by a background goroutine.

nhooyr.io/websocket does NOT request `permessage-deflate` by default — we rely on that, because codex's exec-server closes connections that do (see spec § PoC #2 gotchas).

func (*BridgeClient) Call

func (bc *BridgeClient) Call(ctx context.Context, method string, params json.RawMessage) (json.RawMessage, error)

Call sends a JSON-RPC request and blocks until the response arrives, the context is cancelled, or the connection closes.

func (*BridgeClient) Close

func (bc *BridgeClient) Close()

Close shuts the connection. Safe to call repeatedly; first call wins.

func (*BridgeClient) Notify

func (bc *BridgeClient) Notify(ctx context.Context, method string, params json.RawMessage) error

Notify sends a JSON-RPC notification (no id, no reply expected).

type BridgePool added in v0.51.0

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

BridgePool maintains one BridgeClient per exe_id, dialed lazily on first Get and reused across calls. Closed connections (detected via the BridgeClient's `closed` channel) are dropped and redialed transparently. Used by env-mcp tools to multiplex multiple executor targets behind one stdio MCP server.

func NewBridgePool added in v0.51.0

func NewBridgePool(gatewayBaseURL, token string, logger *slog.Logger) *BridgePool

NewBridgePool returns a pool. gatewayBaseURL should be the prefix to which `/<exe_id>` is appended (i.e. include `/bridge` but no trailing slash); token is the workspace-scoped capability token issued for this turn.

func (*BridgePool) Close added in v0.51.0

func (p *BridgePool) Close()

Close shuts down every pooled connection. Idempotent.

func (*BridgePool) Get added in v0.51.0

func (p *BridgePool) Get(ctx context.Context, exeID string) (*BridgeClient, error)

Get returns a live BridgeClient for exeID. Dials on first use (with codex exec-server `initialize` handshake performed in-band so the caller can start issuing process/* and fs/* calls immediately). On subsequent calls, returns the cached client if it's still open.

Race-safety: dial happens outside the pool lock so a slow dial for exe_a doesn't block Get(exe_b). If two goroutines race to dial the same exe_id, both will dial but only one connection ends up in the map; the loser closes its connection and returns the winner's.

type CopyPathTool added in v0.55.0

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

CopyPathTool copies a file or directory between executors.

v0.56.0: prefers HTTPS out-of-band relay (curl PUT on src + curl GET on dst → bytes flow direct executor↔gateway↔executor, never through env-mcp's ws bridge). When the relay path is unavailable (gateway not configured, executor missing curl, or recursive copy below) it falls back to the v0.55.x ws cat-pump path that shuttles each chunk through process/read+process/write RPCs.

See:

  • docs/superpowers/specs/2026-05-18-copy-path-http-relay.md (v0.56.0)
  • docs/superpowers/specs/2026-05-18-env-mcp-transfer-tool.md (v0.55.x)

func NewCopyPathTool added in v0.55.0

func NewCopyPathTool(pool *BridgePool, resolver *NameResolver, relay *RelayClient) *CopyPathTool

func (*CopyPathTool) Call added in v0.55.0

func (*CopyPathTool) Description added in v0.55.0

func (t *CopyPathTool) Description() string

func (*CopyPathTool) InputSchema added in v0.55.0

func (t *CopyPathTool) InputSchema() json.RawMessage

func (*CopyPathTool) Name added in v0.55.0

func (t *CopyPathTool) Name() string

type ExecInitializeParams

type ExecInitializeParams struct {
	ClientName      string  `json:"clientName"`
	ResumeSessionID *string `json:"resumeSessionId,omitempty"`
}

ExecInitializeParams matches codex-rs's InitializeParams (camelCase).

type ExecInitializeResult

type ExecInitializeResult struct {
	SessionID string `json:"sessionId"`
}

type FileOp added in v0.51.0

type FileOp struct {
	Kind    FileOpKind
	Path    string
	NewPath string      // only set when Kind == OpMove
	Content string      // full file body for OpAdd; empty for others
	Hunks   []PatchHunk // only set when Kind == OpUpdate or OpMove
}

FileOp is one entry in the parsed patch.

func ParsePatch added in v0.51.0

func ParsePatch(input string) ([]FileOp, error)

ParsePatch parses a complete apply_patch document.

type FileOpKind added in v0.51.0

type FileOpKind int

FileOpKind identifies the kind of operation.

const (
	OpAdd FileOpKind = iota + 1
	OpUpdate
	OpDelete
	OpMove
)

type FsCopyParams added in v0.51.0

type FsCopyParams struct {
	SourcePath      string `json:"sourcePath"`
	DestinationPath string `json:"destinationPath"`
	Recursive       bool   `json:"recursive,omitempty"`
}

FsCopyParams is the request body for fs/copy.

type FsReadFileParams added in v0.51.0

type FsReadFileParams struct {
	Path string `json:"path"`
}

FsReadFileParams is the request body for fs/readFile.

type FsReadFileResult added in v0.51.0

type FsReadFileResult struct {
	DataBase64 string `json:"dataBase64"`
}

FsReadFileResult: dataBase64 is the file's full content (codex returns the entire file; we expose offset/limit slicing in the MCP tool wrapper).

type FsRemoveParams added in v0.51.0

type FsRemoveParams struct {
	Path      string `json:"path"`
	Recursive bool   `json:"recursive,omitempty"`
}

FsRemoveParams is the request body for fs/remove.

type FsWriteFileParams added in v0.51.0

type FsWriteFileParams struct {
	Path       string `json:"path"`
	DataBase64 string `json:"dataBase64"`
	// CreateMissing controls whether intermediate directories are
	// created. Codex's default is true.
	CreateMissing bool `json:"createMissing,omitempty"`
}

FsWriteFileParams is the request body for fs/writeFile.

type HunkLine added in v0.51.0

type HunkLine struct {
	Kind HunkLineKind
	Text string
}

type HunkLineKind added in v0.51.0

type HunkLineKind int
const (
	HunkContext HunkLineKind = iota + 1
	HunkAdd
	HunkRemove
)

type JSONRPCError

type JSONRPCError struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data,omitempty"`
}

type JSONRPCMessage

type JSONRPCMessage struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      *int64          `json:"id,omitempty"`
	Method  string          `json:"method,omitempty"`
	Params  json.RawMessage `json:"params,omitempty"`
	Result  json.RawMessage `json:"result,omitempty"`
	Error   *JSONRPCError   `json:"error,omitempty"`
}

JSONRPCMessage is the JSON-RPC 2.0 envelope shared by both MCP (over stdio) and exec-server (over ws). The ID field is a pointer so notifications (which have no ID) marshal cleanly without the field.

type ListEnvironmentsTool added in v0.51.0

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

ListEnvironmentsTool returns the workspace's connected executors. Per v0.54.0 the LLM-facing view shows only name + description + last_seen (no exe_id). The shared NameResolver populates its cache as a side effect of every call, so subsequent shell/apply_patch/etc tool calls can look up name → exe_id.

func NewListEnvironmentsTool added in v0.51.0

func NewListEnvironmentsTool(resolver *NameResolver) *ListEnvironmentsTool

func (*ListEnvironmentsTool) Call added in v0.51.0

func (*ListEnvironmentsTool) Description added in v0.51.0

func (t *ListEnvironmentsTool) Description() string

func (*ListEnvironmentsTool) InputSchema added in v0.51.0

func (t *ListEnvironmentsTool) InputSchema() json.RawMessage

func (*ListEnvironmentsTool) Name added in v0.51.0

func (t *ListEnvironmentsTool) Name() string

type MCPCallToolParams

type MCPCallToolParams struct {
	Name      string          `json:"name"`
	Arguments json.RawMessage `json:"arguments"`
}

MCPCallToolParams is the request body of `tools/call`.

type MCPCallToolResult

type MCPCallToolResult struct {
	Content []MCPToolContent `json:"content"`
	IsError bool             `json:"isError"`
}

MCPCallToolResult is the response body of `tools/call`.

type MCPInitializeResult

type MCPInitializeResult struct {
	ProtocolVersion string         `json:"protocolVersion"`
	Capabilities    map[string]any `json:"capabilities"`
	ServerInfo      MCPServerInfo  `json:"serverInfo"`
}

MCPInitializeResult is the response to `initialize`.

type MCPListToolsResult

type MCPListToolsResult struct {
	Tools []MCPTool `json:"tools"`
}

MCPListToolsResult is the response to `tools/list`.

type MCPServer

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

MCPServer is a minimal newline-delimited JSON-RPC stdio MCP server that exposes a fixed set of tools through a registry. Concurrency: requests are handled sequentially in the order they arrive; this matches the MCP stdio model and keeps the server free of intra-process synchronization other than the write-mutex.

func NewMCPServer

func NewMCPServer(name string, tools []Tool, logger *slog.Logger) *MCPServer

NewMCPServer constructs a server bound to a registry. Tool order is preserved as supplied (LLM clients sometimes rely on consistent ordering for caching).

func (*MCPServer) Serve

func (s *MCPServer) Serve(ctx context.Context, in io.Reader, out io.Writer) error

Serve reads requests from in until EOF and writes responses to out. Returns nil on clean EOF, error on unrecoverable read/write failure.

type MCPServerInfo

type MCPServerInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

type MCPTool

type MCPTool struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	InputSchema json.RawMessage `json:"inputSchema"`
}

type MCPToolContent

type MCPToolContent struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type NameResolver added in v0.54.0

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

NameResolver maintains a workspace-scoped name → exe_id map by periodically refreshing from app-gateway's /internal/connected. Tools that take an environment_id (semantically a name) call Resolve to get the underlying exe_id for BridgePool.Get.

Cache strategy:

  • First Resolve populates the cache.
  • Subsequent Resolves use the cache if its age is under cacheTTL.
  • A Resolve miss forces an immediate refresh before erroring.

func NewNameResolver added in v0.54.0

func NewNameResolver(loopbackURL, loopbackToken string, logger *slog.Logger) *NameResolver

func (*NameResolver) LLMView added in v0.54.0

func (r *NameResolver) LLMView(ctx context.Context) ([]byte, error)

LLMView returns the entries reshaped for the LLM (omits exe_id). Always refreshes to keep the LLM's view fresh.

func (*NameResolver) Resolve added in v0.54.0

func (r *NameResolver) Resolve(ctx context.Context, name string) (string, error)

Resolve returns the exe_id bound to name in the current workspace. If name isn't in the cache, refreshes once before reporting not-found.

type PatchHunk added in v0.51.0

type PatchHunk struct {
	Context string
	Lines   []HunkLine
}

PatchHunk is one @@-delimited block in an Update File entry. Lines are stored in their original order so the application step can reconstruct the modified file. Context (when non-empty) is the "@@ <text>" anchor that the applier locates first; this matters when the body context appears in multiple places in the source.

type ProcessOutputChunk

type ProcessOutputChunk struct {
	Seq    uint64 `json:"seq"`
	Stream string `json:"stream"` // "stdout" | "stderr"
	Chunk  string `json:"chunk"`
}

ProcessOutputChunk: chunk is base64-encoded raw bytes (per codex's ByteChunk wrapper that uses serde_with for base64 encoding).

type ProcessReadParams

type ProcessReadParams struct {
	ProcessID string `json:"processId"`
	AfterSeq  uint64 `json:"afterSeq"`
	MaxBytes  int    `json:"maxBytes"`
	WaitMs    int    `json:"waitMs"`
}

type ProcessReadResult

type ProcessReadResult struct {
	Chunks   []ProcessOutputChunk `json:"chunks"`
	NextSeq  uint64               `json:"nextSeq"`
	Exited   bool                 `json:"exited"`
	ExitCode *int                 `json:"exitCode"`
	Closed   bool                 `json:"closed"`
	Failure  *string              `json:"failure"`
}

type ProcessStartParams

type ProcessStartParams struct {
	ProcessID string            `json:"processId"`
	Argv      []string          `json:"argv"`
	Cwd       string            `json:"cwd"`
	Env       map[string]string `json:"env"`
	TTY       bool              `json:"tty"`
	PipeStdin bool              `json:"pipeStdin"`
	Arg0      *string           `json:"arg0"`
}

type ProcessStartResult

type ProcessStartResult struct {
	ProcessID string `json:"processId"`
}

type ProcessTerminateParams added in v0.51.0

type ProcessTerminateParams struct {
	ProcessID string `json:"processId"`
}

ProcessTerminateParams is the request body for process/terminate.

type ProcessWriteParams added in v0.51.0

type ProcessWriteParams struct {
	ProcessID string `json:"processId"`
	Chunk     string `json:"chunk"` // base64 raw bytes
}

ProcessWriteParams is the request body for process/write. The `chunk` field name matches upstream codex's WriteParams (see codex-rs/exec-server/src/protocol.rs); writing with `data` here would 400 with "missing field `chunk`".

type ReadFileTool added in v0.51.0

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

ReadFileTool implements `read_file` via exec-server fs/readFile. Offset/limit are applied to the decoded bytes after fetching the full file from the remote, matching codex's local read_file semantics (exec-server doesn't support partial reads server-side).

func NewReadFileTool added in v0.51.0

func NewReadFileTool(pool *BridgePool, resolver *NameResolver) *ReadFileTool

func (*ReadFileTool) Call added in v0.51.0

func (*ReadFileTool) Description added in v0.51.0

func (t *ReadFileTool) Description() string

func (*ReadFileTool) InputSchema added in v0.51.0

func (t *ReadFileTool) InputSchema() json.RawMessage

func (*ReadFileTool) Name added in v0.51.0

func (t *ReadFileTool) Name() string

type ReadOutputTool added in v0.51.0

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

ReadOutputTool drains stdout/stderr buffered for a session.

func NewReadOutputTool added in v0.51.0

func NewReadOutputTool(pool *BridgePool, store *sessionStore) *ReadOutputTool

func (*ReadOutputTool) Call added in v0.51.0

func (*ReadOutputTool) Description added in v0.51.0

func (t *ReadOutputTool) Description() string

func (*ReadOutputTool) InputSchema added in v0.51.0

func (t *ReadOutputTool) InputSchema() json.RawMessage

func (*ReadOutputTool) Name added in v0.51.0

func (t *ReadOutputTool) Name() string

type RelayClient added in v0.57.0

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

RelayClient mints HTTPS relay tickets on the codex-exec-gateway's /api/exec-gateway/relay/create endpoint. Used by CopyPathTool to route file bytes around the bridge ws path.

nil-safe: when ExecGatewayInternalURL or InternalSecret is empty, CreateRelay returns a sentinel error and the caller falls back to the ws cat-pump path.

func NewRelayClient added in v0.57.0

func NewRelayClient(baseURL, secret, workspaceID string, logger *slog.Logger) *RelayClient

func (*RelayClient) CreateRelay added in v0.57.0

func (c *RelayClient) CreateRelay(ctx context.Context, srcExeID, dstExeID string, ttl time.Duration, maxBytes int64) (*RelayTicket, error)

CreateRelay mints a ticket good for one PUT + one GET on the gateway's /relay/<ticket> endpoint.

func (*RelayClient) Enabled added in v0.57.0

func (c *RelayClient) Enabled() bool

Enabled reports whether the relay path can be used (both URL + secret present).

type RelayTicket added in v0.57.0

type RelayTicket struct {
	Ticket      string    `json:"ticket"`
	UploadURL   string    `json:"upload_url"`
	DownloadURL string    `json:"download_url"`
	ExpiresAt   time.Time `json:"expires_at"`
}

RelayTicket is the gateway's response to relay/create.

type RunArgs

type RunArgs struct {
	WorkspaceID        string // --workspace-id
	ExecGatewayURL     string // --exec-gateway-url; pool appends /<exe_id>
	AppGatewayInternal string // --app-gateway-internal; list_environments calls /internal/connected here
	WorkspaceTokenEnv  string // --workspace-token-env (workspace-scoped cap token)
	LoopbackTokenEnv   string // --loopback-token-env (for /internal/connected)
	// ExecGatewayInternalURL is the http(s):// base for codex-exec-gateway's
	// internal API (NOT the ws /bridge URL). copy_path's HTTP relay path
	// POSTs to <base>/api/exec-gateway/relay/create here. Empty disables
	// the HTTP relay path; copy_path falls back to the ws cat-pump.
	ExecGatewayInternalURL    string // --exec-gateway-internal-url
	ExecGatewayInternalSecret string // --exec-gateway-internal-secret-env (env var name; value injected by gateway)
}

RunArgs is the parsed CLI input for `codex-app-gateway env-mcp`. Per the 2026-05-16 fixed-tools redesign, env-mcp is workspace-scoped rather than per-executor; one child binary handles every executor in the workspace via environment_id routing.

type ShellTool added in v0.51.0

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

ShellTool implements the synchronous-shell MCP tool. Each call dispatches process/start on the selected executor then polls process/read until the process exits or the timeout elapses.

func NewShellTool added in v0.51.0

func NewShellTool(pool *BridgePool, resolver *NameResolver) *ShellTool

func (*ShellTool) Call added in v0.51.0

func (*ShellTool) Description added in v0.51.0

func (t *ShellTool) Description() string

func (*ShellTool) InputSchema added in v0.51.0

func (t *ShellTool) InputSchema() json.RawMessage

func (*ShellTool) Name added in v0.51.0

func (t *ShellTool) Name() string

type TerminateTool added in v0.51.0

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

TerminateTool sends process/terminate then drops the session entry.

func NewTerminateTool added in v0.51.0

func NewTerminateTool(pool *BridgePool, store *sessionStore) *TerminateTool

func (*TerminateTool) Call added in v0.51.0

func (*TerminateTool) Description added in v0.51.0

func (t *TerminateTool) Description() string

func (*TerminateTool) InputSchema added in v0.51.0

func (t *TerminateTool) InputSchema() json.RawMessage

func (*TerminateTool) Name added in v0.51.0

func (t *TerminateTool) Name() string

type Tool added in v0.51.0

type Tool interface {
	Name() string
	Description() string
	InputSchema() json.RawMessage
	Call(ctx context.Context, args json.RawMessage) (MCPCallToolResult, error)
}

Tool is implemented by every MCP tool env-mcp exposes. tools/list builds its response by querying each registered Tool's metadata; tools/call dispatches by Name.

type UnifiedExecTool added in v0.51.0

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

UnifiedExecTool starts a long-lived process and returns a session_id the LLM uses with write_stdin / read_output / terminate.

func NewUnifiedExecTool added in v0.51.0

func NewUnifiedExecTool(pool *BridgePool, store *sessionStore, resolver *NameResolver) *UnifiedExecTool

func (*UnifiedExecTool) Call added in v0.51.0

func (*UnifiedExecTool) Description added in v0.51.0

func (t *UnifiedExecTool) Description() string

func (*UnifiedExecTool) InputSchema added in v0.51.0

func (t *UnifiedExecTool) InputSchema() json.RawMessage

func (*UnifiedExecTool) Name added in v0.51.0

func (t *UnifiedExecTool) Name() string

type WriteStdinTool added in v0.51.0

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

WriteStdinTool writes bytes to a session's stdin via process/write.

func NewWriteStdinTool added in v0.51.0

func NewWriteStdinTool(pool *BridgePool, store *sessionStore) *WriteStdinTool

func (*WriteStdinTool) Call added in v0.51.0

func (*WriteStdinTool) Description added in v0.51.0

func (t *WriteStdinTool) Description() string

func (*WriteStdinTool) InputSchema added in v0.51.0

func (t *WriteStdinTool) InputSchema() json.RawMessage

func (*WriteStdinTool) Name added in v0.51.0

func (t *WriteStdinTool) Name() string

Jump to

Keyboard shortcuts

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