Skip to content

Cobra binding reference

gitlab.com/phpboyscout/go/mcp/cobra binds a completed Cobra command tree to operations that run each command as a subprocess. It is the replacement for ophis's executor. The package imports Cobra; the module root does not.

Binding

binding, err := cobra.Bind(root,
    cobra.WithExposure(setup.IsExposedToMCP),
    cobra.WithLogger(logger),
)
registry, err := mcp.New(binding.Operations(), mcp.WithPolicy(mcp.AllowRegistered()))

Bind after every command is registered: the snapshot never changes. Passing Operations() to a registry does not transfer ownership; the host still calls Shutdown and reads Status.

What is bound

Walking the tree in Cobra's (sorted) order, a command becomes an operation when all of these hold:

  • it is runnable (a pure group is not);
  • it is not hidden or deprecated, and neither is any ancestor;
  • it is not help or completion at the root, and it does not carry the mcp.phpboyscout.uk/omit annotation (Omit(cmd); the MCP command tree itself carries it);
  • the exposure policy given by WithExposure accepts it.

Those mandatory filters run first; the policy can only remove. Children are visited even when the parent is unexposed, so a descendant's own decision is honoured.

Names

An operation is named <root>_<path joined by _>, ophis's spelling, so tool deploy canary is tool_deploy_canary. WithPrefix replaces the root segment. Two commands that map to one name (foo bar and foo_bar) fail Bind; WithOperationName("foo_bar", "tool_foo-bar") resolves it. Names must match ^[A-Za-z0-9_.-]{1,128}$.

Definition

Definition field Source
Title annotation title
Summary Short, or Execute the <name> command
Description Long
Examples Example
Group WithGroup's answer, else the parent command's name for nested commands
Source cobra
Hints annotations readOnlyHint, destructiveHint, idempotentHint, openWorldHint (true/false; anything else is unset)

Input and output

Input is the compatibility view:

{"flags": {"channel": "general", "retries": 3}, "args": ["hello"]}

flags is keyed by flag name with additionalProperties: false; a flag Cobra marks required is required inside it, and the object itself is then required. args are positional strings. The child receives the stored command path, one --name=value token per encoded value in flag order, then -- and the positional arguments, so an argument that looks like a flag stays an argument.

Output is always:

{"stdout": "...", "stderr": "...", "exitCode": 0, "truncated": false}

with stdout also returned as text content. A non-zero exit is a command_failed failure that still carries this view as diagnostics.

Flags and codecs

Every flag reachable on the command (local, then inherited persistent flags) is published except help, hidden and deprecated flags, and anything WithFlagFilter declines. Each flag's pflag type selects a codec; a type with no codec fails Bind naming the command, the flag and the type, unless the command is excluded. Built-in codecs cover the standard pflag types:

pflag type JSON Notes
string string
bool boolean always sent as --flag=true or --flag=false
intint64, uintuint64, count integer range-checked; never rounded through float64
float32, float64 number
duration string validated as a Go duration
stringSlice array of string one token per element; an element containing a comma is rejected because pflag would split it
stringArray array of string literal
intSlice, int32Slice, int64Slice, uintSlice, float32Slice, float64Slice, boolSlice array
stringToString, stringToInt, stringToInt64 object key=value tokens; keys with , or =, values with , are rejected
ip, ipMask, ipNet, bytesHex, bytesBase64 string the child validates

A value the CLI cannot express faithfully is rejected before any process starts. For a custom pflag.Value whose CLI form is one string:

cobra.WithCodec("mode", cobra.TextCodec(json.RawMessage(`{"enum":["fast","slow"]}`)))

The constraints narrow the published schema; they may not redefine type. A type not registered this way is an error, not a silent string.

Execution

Setting Default Option
executable the current binary, resolved absolute at bind WithExecutable
working directory the host's at bind WithWorkingDirectory
environment a copy of the host's at bind WithEnvironment
stdin closed WithStdin
concurrency 1 Limits.Concurrency
timeout 5 minutes, shortened by the caller's deadline Limits.Timeout
cleanup budget 5 seconds Limits.CleanupBudget
retained output 1 MiB, stdout and stderr combined Limits.OutputBytes

Tool arguments can never choose the executable, directory or environment.

At capacity an invocation fails immediately with the retryable busy failure; search and inspection never take a slot. Output beyond the budget is drained rather than blocking the child, and the view says "truncated": true.

Cancellation and cleanup

The child starts as the leader of a new process group on Unix, or inside a kill-on-close job object on Windows (assigned before its first instruction). On timeout or cancellation the group is asked to terminate, forced halfway through the cleanup budget, and then polled until no managed process remains. Timeout, cancellation, non-zero exit and cleanup failure are distinct failure codes, and the first three still carry the captured output.

Managed descendants are those that stay in the group or job. A process that deliberately leaves (a new session on Unix) is outside the boundary and is not claimed.

A cleanup the budget cannot confirm returns cleanup_failed, keeps its slot, and appears in Status() until the binding sees the process gone, at which point the slot is released. Exhausting a wait is never treated as evidence that work stopped.

Shutdown(ctx) closes admission, cancels active invocations, and waits within ctx for their cleanup. It is idempotent. Register it with the host's lifecycle (controls.WithStopErr) and expose Status in health aggregation.