miller/pkg/lib/halfpipe.go
John Kerl 41f5188bd0
Add mlr mcp MCP server + agent playbook, with a --no-shell gate (#2098 PR7) (#2133)
* Plan: flesh out PR7 (MCP server + Agent Skill) design

stdio transport (no HTTP port), mlr mcp terminal in the main binary,
SDK-vs-handroll decision, tool list, in-process vs subprocess split,
run-tool safety (--no-shell prerequisite), single-sourced skill, tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add mlr mcp: MCP server + agent playbook; --no-shell gate (#2098 PR7)

New terminal `mlr mcp` runs a Model Context Protocol server over stdio
(spawned by MCP clients; no network port), exposing five tools --
list_capabilities, which, validate_dsl, describe_data, run -- plus an
agent playbook as MCP prompt/resource. Catalog tools are served
in-process from the help registries; the rest subprocess this same
binary with MLR_ERRORS_JSON=1, a timeout, and an output cap.

Prerequisite: a new --no-shell flag / MLR_NO_SHELL env var (one-way
gate) disables the DSL system/exec functions, piped redirects, and
--prepipe/--prepipex; the MCP server sets it on the commands it runs
unless started with --allow-shell.

Adds the github.com/modelcontextprotocol/go-sdk dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Force LF checkout for the embedded SKILL.md (Windows CI fix)

go:embed embeds checkout bytes, so a CRLF checkout on Windows made the
embedded playbook differ per platform and failed
TestPlaybookHasFrontmatter. Pin the file to eol=lf in .gitattributes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Move no-shell test DSL into per-case mlr files (Windows CI fix)

Inline single-quoted DSL in cmd files is mangled by the Windows shell
(single quotes are not quote characters there); the harness's
put -f ${CASEDIR}/mlr pattern avoids shell quoting entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:41:49 -04:00

100 lines
2.7 KiB
Go

package lib
import (
"fmt"
"os"
"github.com/johnkerl/miller/v6/pkg/platform"
)
// OpenOutboundHalfPipe returns a handle to a process. Writing to that handle
// writes to the process' stdin. The process' stdout and stderr are the current
// process' stdout and stderr.
//
// This is for pipe-output-redirection in the Miller put/filter DSL.
//
// Note I am not using os.exec.Cmd which is billed as being simpler than using
// os.StartProcess. It may indeed be simpler when you want to handle the
// subprocess' stdin/stdout/stderr all three within the parent process. Here I
// found it much easier to use os.StartProcess to let the stdout/stderr run
// free.
func OpenOutboundHalfPipe(commandString string) (*os.File, error) {
if !shellOutEnabled {
return nil, fmt.Errorf("piped redirects are disabled by --no-shell / MLR_NO_SHELL")
}
readPipe, writePipe, err := os.Pipe()
if err != nil {
return nil, err
}
var procAttr os.ProcAttr
procAttr.Files = []*os.File{
readPipe,
os.Stdout,
os.Stderr,
}
// /bin/sh -c "..." or cmd /c "..."
shellRunArray := platform.GetShellRunArray(commandString)
process, err := os.StartProcess(shellRunArray[0], shellRunArray, &procAttr)
if err != nil {
return nil, err
}
go func() { _, _ = process.Wait() }()
return writePipe, nil
}
// OpenInboundHalfPipe returns a handle to a process. Reading from that handle
// reads from the process' stdout. The process' stdin and stderr are the
// current process' stdin and stderr.
//
// This is for the Miller prepipe feature.
//
// Note I am not using os.exec.Cmd which is billed as being simpler than using
// os.StartProcess. It may indeed be simpler when you want to handle the
// subprocess' stdin/stdout/stderr all three within the parent process. Here I
// found it much easier to use os.StartProcess to let the stdin/stderr run
// free.
func OpenInboundHalfPipe(commandString string) (*os.File, error) {
if !shellOutEnabled {
return nil, fmt.Errorf("--prepipe/--prepipex are disabled by --no-shell / MLR_NO_SHELL")
}
readPipe, writePipe, err := os.Pipe()
if err != nil {
return nil, err
}
var procAttr os.ProcAttr
procAttr.Files = []*os.File{
os.Stdin,
writePipe,
os.Stderr,
}
// /bin/sh -c "..." or cmd /c "..."
shellRunArray := platform.GetShellRunArray(commandString)
process, err := os.StartProcess(shellRunArray[0], shellRunArray, &procAttr)
if err != nil {
return nil, err
}
// TODO comment somewhere
// https://stackoverflow.com/questions/47486128/why-does-io-pipe-continue-to-block-even-when-eof-is-reached
// TODO comment
go func(process *os.Process, readPipe *os.File) {
_, err := process.Wait()
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
}
_ = readPipe.Close()
}(process, readPipe)
return readPipe, nil
}