feat(cli): MCP server mode via structcli WithMCP#41
Merged
Conversation
This was referenced May 3, 2026
46a1b8b to
2d49157
Compare
This was referenced May 3, 2026
2d49157 to
85bf463
Compare
Add --mcp persistent flag turning kfeatures into a Model Context
Protocol server over stdio. Each runnable leaf command becomes an
MCP tool whose input schema mirrors the cobra flag set; agents
introspect via tools/list and invoke via tools/call without scraping
--help. Pure stdlib JSON-RPC inside structcli — no extra heavy SDK.
Tools exposed: probe, check, config. Excluded: version (build
metadata is in MCP serverInfo) and completion-* (no agent value).
mcpServerVersion() mirrors the human-facing version output so MCP
clients can correlate tool output with build metadata, falling back
to 'dev' when built without ldflags.
Stream routing: every command handler now writes through
cmd.OutOrStdout() / cmd.ErrOrStderr() instead of bare os.Stdout /
os.Stderr / fmt.Print*. Required so structcli's per-call buffer
capture works; non-MCP behaviour is bit-for-bit identical because
cobra's OutOrStdout() resolves to os.Stdout when no SetOut was
called. printJSON gained an io.Writer parameter.
Session survival: added inMCPMode(c) helper that detects MCP
execution (Out swapped from os.Stdout) and replaces os.Exit(1) with
return err for FeatureError (in check) and 'kernel config not
available' (in config). os.Exit would terminate the MCP server
mid-session and kill subsequent tools/call requests on the same
stdio connection.
Under MCP, the check FeatureError carve-out is collapsed (skip the
--json {ok,feature,reason} payload and the FAIL: … print) because
the MCP layer discards command stdout/stderr on execErr and emits
the structcli error envelope as isError=true content. The agent
gets the verbatim message in the envelope.
bats: new test/cli_mcp.bats covers flag presence, initialize,
tools/list exclude semantics, inputSchema shape, structcli envelope
routing for invocation errors, JSON-RPC error for excluded tools,
multi-call session survival across a FeatureError, and a stdout
leakage guard.
Co-authored-by: Ona <no-reply@ona.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #40. Review #38 → #39 → #40 first; this PR's diff is minimal once those land. The base will retarget to
mainautomatically when #40 merges.Why
structcli v0.17.0 ships an
--mcpcapability (pure stdlib JSON-RPC over stdio, no extra heavy SDK dependency, ~13 KB binary impact) that turns anyBind-driven cobra tree into a Model Context Protocol server. With #38–#40 in place — typed flags, structured errors, semantic exit codes, JSON Schema discovery —kfeaturesis exactly the shape MCP wants: every runnable leaf becomes a tool whose schema mirrors the cobra flag set, and any failure produces a JSON envelope an agent can route on.This PR wires it up.
What changes
structcli.WithMCP(...)added to theSetupcall. Tools exposed:probe,check,config. Excluded:version(build metadata is in MCPserverInfo) andcompletion-bash/zsh/fish/powershell(no agent value).mcpServerVersion()mirrors the human-facing version output so MCP clients can correlate tool output with build metadata. Falls back to"dev"when built without ldflags, matching the existingversionsubcommand behaviour.cmd.OutOrStdout()/cmd.ErrOrStderr()instead of bareos.Stdout/os.Stderr/fmt.Print*. Required so structcli's per-call buffer capture works; non-MCP behaviour is bit-for-bit identical because cobra'sOutOrStdout()resolves toos.Stdoutwhen noSetOutwas called.printJSONgained anio.Writerparameter.inMCPMode(c)helper (detectsOut != os.Stdout) and replacedos.Exit(1)withreturn errfor theFeatureErrorpath incheckand the "kernel config not available" path inconfig.os.Exitwould terminate the MCP server mid-session and kill subsequenttools/callrequests on the same stdio connection.checkFeatureErrorcarve-out is collapsed: skip the--json{ok,feature,reason}payload and theFAIL: …print, because the MCP layer discards command stdout/stderr onexecErrand emits the structcli error envelope asisError=truecontent. The agent receives the verbatim message in the envelope; emitting the carve-out shape would be wasted work and confusing (two competing JSON shapes for the same outcome).Behaviour matrix
tools/callprobecontent.text= probe output (text or JSON perarguments.json)check --require bpf-syscallisError=false,content.text= "OK: …"check --require bpf-fs(FAIL)FAIL: bpf-fs — …on stderrisError=true,content.text= structcli envelope (exit_code: 1)check --require bpf-fs --json(FAIL){ok:false,feature,reason}stdoutisError=true, structcli envelope (carve-out collapsed)check --require nonexistentinvalid_flag_valueenvelopeisError=true, same envelope as contentconfigwhen/proc/config.gzmissingisError=true,content.text= same messageversionserverInfo.versionis the agent-facing form)completion <shell>Tests
New
test/cli_mcp.bats(8 tests):--mcppersistent flag exists on root.initializereturnsprotocolVersion2024-11-05and the expectedserverInfo.tools/listexposes exactly[check, config, probe](excludes verified).inputSchemaforcheckincludesrequireandjsonproperties.isError=true, exit_code=11 inside).tools/callfor an excluded tool returns a JSON-RPC error.go test ./...clean.cli_common.bats + cli_linux.bats + cli_mcp.bats= 34/34 green.Out of scope
examples/mcp/claude_desktop_config.json snippet — defer until after the user-facing docs PR lands so we can link from one place.