Documentation
¶
Overview ¶
Package shell implements an MCP source that executes shell commands. Connect decodes the source's `connect:` map (working_dir, timeout, max_output_bytes, shell, env) and returns a single run_command tool which spawns `sh -c <command>` and returns the captured output.
This is a thin drop-in replacement for the shell tooling built into MCP hosts (notably Zed): the LLM writes shell syntax and gets back stdout, stderr, and exit code. The surrounding guardrails — working- directory confinement, explicit env, hard timeout, output cap with truncation, and honest DestructiveHint=true annotations — are the safety surface the rest of this meta-server provides.
Security model: this source can execute arbitrary code. The MVP trusts the operator (same trust model as the postgres source for SQL) and the LLM (same trust model as Zed's built-in shell tool). The guardrails narrow the blast radius but do not eliminate it. Operators who want a stricter model should pair this source with a host that gates tool invocation on DestructiveHint.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Connect ¶
Connect decodes the source's `connect:` map, validates it, and returns the run_command tool. The tool advertises ReadOnlyHint=false, DestructiveHint=true, IdempotentHint=false, OpenWorldHint=false — host implementations that gate tool invocation on these hints will treat shell correctly as a write/destructive, local-scoped capability.
After validation, Connect canonicalizes connect.working_dir via filepath.EvalSymlinks and opens an os.Root at the canonical path. Both the root and the canonical path are captured in the handler closure so per-call directory overrides can be validated against the same root every invocation, regardless of whether the LLM passes the symlinked or canonical form of any path inside it.
func RunBefore ¶
func RunBefore(ctx context.Context, cmds []BeforeCommand, opts ...tool.Option)
RunBefore spawns every command in cmds as a long-lived background process under the supplied context, then returns only after every declared Healthcheck has finished — passed, timed out, or canceled.
The contract for failures is intentionally lenient: a failed spawn, a non-zero process exit, a timed-out healthcheck, or a canceled context all log at ERROR (or DEBUG for in-progress healthcheck retries) without aborting the function. RunBefore itself returns nothing; its caller (server-start in cmd/main.go) proceeds to source.Apply regardless of outcomes.
A nil or empty cmds slice is a no-op: no goroutines are started and RunBefore returns immediately.
Spawned children are tied to the supplied context: when the server context cancels (SIGINT/SIGTERM via signal.NotifyContext), every running `sh -c` is killed via the underlying exec.CommandContext machinery, and every in-flight healthcheck dial is canceled.
RunBefore.Wait tracks ONLY healthcheck goroutines. Spawned commands are fire-and-forget — their lifetime is the server context, not RunBefore's wait, because in the typical case they are long-running daemons (kubectl port-forward, etc.) that the operator intends to outlive the wait.
Types ¶
type BeforeCommand ¶
type BeforeCommand struct {
Command string `yaml:"command" json:"command"`
Healthcheck *Healthcheck `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
Restart *RestartPolicy `yaml:"restart,omitempty" json:"restart,omitempty"`
}
BeforeCommand spawns a single sh -c process at server start.
When Restart is non-nil, a non-signal-error exit triggers an automatic respawn of the same command, with the configured Delay between attempts. Restart semantics:
- Restart omitted (Restart == nil): spawn exactly once, current behavior preserved.
- Restart.MaxAttempts == 0 (the Go zero value, also the YAML default when the block is present but empty): restart forever until ctx cancels or the command exits cleanly.
- Restart.MaxAttempts == 1: equivalent to omitting Restart.
- Restart.MaxAttempts == N (N >= 2): up to N total spawn attempts, then ERROR "restart budget exhausted".
type Duration ¶
Duration is a time.Duration that unmarshals from a human-readable string (e.g. "30s", "1m30s") in both YAML and JSON configs. Numbers and other non-string values are rejected at decode time — there is no implicit stringification path.
func (*Duration) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for Duration.
type Healthcheck ¶
type Healthcheck struct {
TCP string `yaml:"tcp" json:"tcp"`
Interval Duration `yaml:"interval,omitempty" json:"interval,omitempty"`
Timeout Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}
Healthcheck is the per-BeforeCommand readiness probe. TCP is required and accepts the standard "host:port" form. Interval is the per-attempt deadline and the sleep between attempts; Timeout is the overall budget. The two defaults (1s and 30s) are applied when the corresponding field is the zero Duration.
type RestartPolicy ¶
type RestartPolicy struct {
MaxAttempts int `yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"`
Delay Duration `yaml:"delay,omitempty" json:"delay,omitempty"`
}
RestartPolicy configures auto-respawn behavior for a BeforeCommand that exits non-zero during the server lifetime. The zero value (MaxAttempts == 0) means "restart forever" — this is the natural default for long-lived tunnels like kubectl port-forward. Set MaxAttempts to a positive N to cap the total number of spawn attempts; on exhaustion, runBeforeOne logs an ERROR and stops.
Delay is the wait between attempts. The zero value uses the defaultRestartDelay constant. The inter-attempt wait honors ctx.Done(), so SIGINT/SIGTERM cancel the loop promptly.