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
helporcompletionat the root, and it does not carry themcp.phpboyscout.uk/omitannotation (Omit(cmd); the MCP command tree itself carries it); - the exposure policy given by
WithExposureaccepts 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 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:
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 |
int… int64, uint… uint64, 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:
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.