Skip to content

feat(cli): MCP server mode via structcli WithMCP#41

Merged
leodido merged 1 commit into
mainfrom
feat/cli-mcp
May 3, 2026
Merged

feat(cli): MCP server mode via structcli WithMCP#41
leodido merged 1 commit into
mainfrom
feat/cli-mcp

Conversation

@leodido
Copy link
Copy Markdown
Owner

@leodido leodido commented May 1, 2026

Stacked on #40. Review #38#39#40 first; this PR's diff is minimal once those land. The base will retarget to main automatically when #40 merges.

Why

structcli v0.17.0 ships an --mcp capability (pure stdlib JSON-RPC over stdio, no extra heavy SDK dependency, ~13 KB binary impact) that turns any Bind-driven cobra tree into a Model Context Protocol server. With #38#40 in place — typed flags, structured errors, semantic exit codes, JSON Schema discovery — kfeatures is 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

  1. structcli.WithMCP(...) added to the Setup call. Tools exposed: probe, check, config. Excluded: version (build metadata is in MCP serverInfo) and completion-bash/zsh/fish/powershell (no agent value).
  2. 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 existing version subcommand behaviour.
  3. 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.
  4. Session survival. Added inMCPMode(c) helper (detects Out != os.Stdout) and replaced os.Exit(1) with return err for the FeatureError path in check and the "kernel config not available" path in config. os.Exit would terminate the MCP server mid-session and kill subsequent tools/call requests on the same stdio connection.
  5. 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 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

Surface CLI mode MCP tools/call
probe unchanged content.text = probe output (text or JSON per arguments.json)
check --require bpf-syscall exit 0, "OK: …" isError=false, content.text = "OK: …"
check --require bpf-fs (FAIL) exit 1, FAIL: bpf-fs — … on stderr isError=true, content.text = structcli envelope (exit_code: 1)
check --require bpf-fs --json (FAIL) exit 1, {ok:false,feature,reason} stdout isError=true, structcli envelope (carve-out collapsed)
check --require nonexistent exit 11, invalid_flag_value envelope isError=true, same envelope as content
config when /proc/config.gz missing exit 1, "kernel config not available" isError=true, content.text = same message
version unchanged not exposed (excluded; serverInfo.version is the agent-facing form)
completion <shell> unchanged not exposed (excluded)

Tests

New test/cli_mcp.bats (8 tests):

  • --mcp persistent flag exists on root.
  • initialize returns protocolVersion 2024-11-05 and the expected serverInfo.
  • tools/list exposes exactly [check, config, probe] (excludes verified).
  • inputSchema for check includes require and json properties.
  • Invocation errors flow through the structcli envelope (isError=true, exit_code=11 inside).
  • tools/call for an excluded tool returns a JSON-RPC error.
  • 4-request session (init + ok + FeatureError + probe) — server stays alive, all responses well-formed.
  • Stdout leakage guard: every line of stdout in a multi-call session must parse as JSON-RPC.

go test ./... clean. cli_common.bats + cli_linux.bats + cli_mcp.bats = 34/34 green.

Out of scope

  • README / CONTRIBUTING surfacing of MCP usage and the structured-errors story (PR 4).
  • Optional: an examples/mcp/ claude_desktop_config.json snippet — defer until after the user-facing docs PR lands so we can link from one place.

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>
@leodido leodido merged commit 0cdb880 into main May 3, 2026
6 checks passed
@leodido leodido deleted the feat/cli-mcp branch May 3, 2026 22:42
@leodido leodido added the enhancement New feature or request label May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant