mcpcli
Turn supplied MCP tool schemas into usable Go command-line interfaces. Cobra owns commands, flags, help, and shell completion; the official MCP Go SDK defines tools/results; Google's JSON Schema library validates arguments. The application owns authentication, transports, discovery, and domain behavior.
go get github.com/OlegHQ/mcpcli
Integrate
root, err := mcpcli.NewCommand(mcpcli.Options{
Name: "nudge",
Description: "Work with Nudge",
Tools: tools, // []*mcp.Tool, available offline
Invoke: func(ctx context.Context, name string, args json.RawMessage) (*mcp.CallToolResult, error) {
return session.CallTool(ctx, &mcp.CallToolParams{Name: name, Arguments: args})
},
Bindings: map[string]mcpcli.Binding{
"update_issue": {
Path: []string{"issue", "update"},
Positionals: []string{"id"},
Flags: map[string]string{"patch.title": "title"},
Columns: []mcpcli.Column{{Header: "ID", Path: "id"}},
},
},
})
The supplied schemas produce flag types, descriptions, string-enum completions, and validation. Explicit mappings make human command names intentional. Unmapped tools retain their tool name converted to kebab-case. AddCommands(root, options) adds generated commands to an existing Cobra tree and rejects command/flag collisions, including existing aliases and ancestor authentication/configuration flags. Treat a construction error as fatal and discard that command tree; construction may have already added preceding valid commands.
nudge issue update ISSUE-ID --title 'New title'
nudge issue update --input patch.json
cat patch.json | nudge issue update --input - --output json
nudge issue update --help
Help and completion do not call Invoke. Build a new command tree for each execution, as with ordinary mutable Cobra command objects. The executable controls exit codes: an execution error must produce a nonzero exit. errors.Is(err, mcpcli.ErrTool) distinguishes an MCP isError result.
Run the self-contained example:
go run ./examples/echo echo 'Hello'
go run ./examples/echo echo 'Hello' --output json
Only changed flags and supplied positionals become arguments; schema defaults are not eagerly copied. Primitive fields accept typed flag values. Inline nested objects produce flags such as --patch-title; Binding.Flags can rename them. Ordinary collections do not require JSON:
# An array of strings: one flag per value, preserving commas and empty strings.
nudge issue update ISSUE-ID --patch-labels bug --patch-labels 'customer,urgent'
# Explicit empty collection and null are distinct from omission and literal text.
nudge issue update ISSUE-ID --clear-patch-labels --unset-patch-assignee
nudge issue update ISSUE-ID --patch-assignee null # literal string "null"
Arrays of strings, integers, numbers, or booleans accept repeated flags. Values are not split at commas. Integers retain 64-bit precision, false stays false, and an empty string remains one string element. --clear-FIELD supplies an empty array or object; --unset-FIELD supplies null for a nullable schema. Omit these flags to preserve a field. Do not pass =false to a clear/unset switch: those are actions, not stored booleans.
Arrays of objects
Child flags use a zero-based INDEX=VALUE argument. This creates ordinary Cobra flags with ordinary help and completion; there is no shell-command or dynamic-flag rewriting. For a tool accepting issues with title, labels, and assignee properties:
nudge bulk-create-issues \
--issues-title '0=Fix retries' \
--issues-title '1=Document recovery' \
--issues-labels '0=bug' --issues-labels '0=urgent' \
--clear-issues-labels 1 \
--unset-issues-assignee 1
The indices select objects, so repeated scalar-array flags at the same index append elements to that object's array. Supply every required property of each object. Flag order does not matter; indices must be contiguous from zero. Use only decimal indices without a sign or leading zeroes. Everything after the first = is the value, including any further equals signs.
Nested object arrays use one dot-separated index for each surrounding array. For example, --issues-filters-field '0.1=status' sets the second filter of the first issue. --issues-filters-values '0.1=started' adds one scalar to that filter's values. The corresponding clear action is --clear-issues-filters-values 0.1. An array of arrays uses an -item child flag, for example --matrix-item '0=1' --matrix-item '0=2' --matrix-item '1=3' supplies [[1,2],[3]].
Each scalar field/index may be assigned once. Duplicate scalar flags, parent-plus-child inputs, and collection values combined with clear/unset actions fail before invocation. Helpers follow field aliases (patch.labels aliased to labels gives --clear-labels); indexed binding paths use [], for example issues.[].title. Helper names participate in the same collision checks as other flags.
Full-array JSON flag values remain supported for compatibility, for example --patch-labels '["bug","urgent"]'. A string that is itself a valid JSON array is interpreted as a full-array value; use optional whole-object input when such text must be a literal array element. A full-array value cannot be combined with repeated elements for the same field. Parent object/complex-array flags also retain their JSON form.
--input FILE accepts one complete JSON object; --input - reads stdin. It cannot combine with argument flags or positionals. The default maximum input is 20,000,000 bytes, configurable through Options.MaxInputBytes. Input JSON numbers retain their original representation when passed to Invoke. Validation uses native 64-bit integers and floating-point numbers; larger integers/out-of-range numbers fail explicitly.
The complete schema is validated by github.com/google/jsonschema-go, including properties not expanded into flags. Child flag discovery is bounded to eight nested levels. Nested properties reached only through $ref or unions do not become flattened child flags. $ref, unions, and more complex nested schemas can use the parent JSON flag or --input; this library does not invent a competing JSON Schema engine. Property names containing dots should use whole-object input, because binding/column paths use dot notation. Schema flags conflicting with reserved names (input, output, help), generated helpers, or inherited flags must be renamed through bindings. For example, alias a tool’s url property to resource-url when the application already uses --url for its server endpoint. Command and flag names must start with an ASCII letter or digit and contain only letters, digits, hyphens, and underscores.
Outputs and errors
--output table renders human output with text/tabwriter. Binding.Columns selects stable useful fields; otherwise scalar keys are displayed. Common data and items envelopes are unwrapped for tables, with nextCursor displayed. Terminal control characters in human output are sanitized.
--output json emits the complete MCP CallToolResult, preserving both structuredContent and every content block. This is intentionally not a lossy text-only projection. Machine consumers can inspect .structuredContent directly.
Successful results go to Cobra's stdout writer. MCP tool errors go to its stderr writer and return ErrTool; transport errors propagate to the application's error boundary. No retry, prompt, credential logging, or automatic pagination occurs. Applications should sanitize private transport errors before exposing them. Render(writer, result, format, columns) is also available to existing commands.
The library installs --output only if it is not already inherited or present. Options.OutputFormat can supply an application resolver, for example to implement auto using its own terminal detection. It must return table or json.
Design references
Development
go test ./...
go vet ./...
MIT licensed. This library does not embed server credentials, discover remote servers implicitly, execute shell strings, or require a daemon.