feat(mcp): serve MCP from the stoat binary - #68
Conversation
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (72)
WalkthroughThe pull request replaces the Python MCP process with an embedded Go server in ChangesEmbedded MCP server
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant StoatCLI
participant MCPServer
participant Core
participant GuestVM
Client->>StoatCLI: start mcp serve
StoatCLI->>MCPServer: serve over stdio or loopback HTTP
Client->>MCPServer: call MCP tool
MCPServer->>MCPServer: validate, rate-limit, and redact
MCPServer->>Core: execute host operation
MCPServer->>GuestVM: run guest operation over sshx.Run
Core-->>MCPServer: return result
GuestVM-->>MCPServer: return output and exit code
MCPServer-->>Client: return wire DTO
Poem
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
A live Debian 13 boot showed exec_bg failing with 'cannot create /run/stoat/jobs/<id>': /run/stoat is root-owned, and the mkdir ran as the ssh user. The runner also had no nohup, so a job could die with the ssh session. Signed-off-by: NovusEdge <novusedge0@gmail.com>
|
Live gate at 756bb38 on Debian 13 cloud (2048 MiB, 2 CPUs), VM created with |
What changed
stoat mcpserves the MCP protocol from the binary.internal/mcpsrvregisters every tool ongithub.com/modelcontextprotocol/go-sdk, runs the guards, callscore, and returnswireDTOs, so the--jsoncontract and the MCP schema are one set of types.New: the four
agent_accesslevels (none,observe,manage,exec) with a legacyallow_execmapping on load and a hidden--allow-execalias; in-VM tools for files, directories, processes, services, packages and background jobs oversshx.Run, the single argv quoter, so no tool builds a shell string from input; the recipe index tools; secret redaction over every tool output including the SDK's text fallback;stoat mcp install <client>andstoat mcp doctor; streamable HTTP on loopback only.mcp/and its CI job are deleted. Every test inmcp/testshas a Go counterpart, checked by the port table test ininternal/mcpsrv.Why
Spec
docs/specs/2026-09-04-mcp-in-go-design.md. fastmcp shipped two breaking majors since the pin, and a static binary needs no interpreter, venv oruvfor a client to launch it.Tests run
go build ./... && go test ./... && golangci-lint run ./...clean at the tip, after merging main.stoat exec, and over MCP stdioexecwith env,write_file,read_file,list_dir,exec_bg,job_status,job_output,list_jobs,mcp doctor. Evidence in the comment below.Deviations from the plan, all reviewed
tools/call; the receiving path wraps the handler result and also rewrites theTextContentfallback the SDK fills before any middleware.registertakes an explicit class argument. The tool table lives intable_test.go, so production code cannot read it;TestAnnotationsMatchTablestill checks every registered tool against the table.config.VM.AllowExecstays besideAgentAccess. Pre-existing round-trip goldens assert it; the loader maps a legacyallow_exec = truetoexec.Found by the live gate
exec_bgfailed on a real guest withcannot create /run/stoat/jobs/<id>:/run/stoatis root-owned and the mkdir ran as the ssh user. The runner also lacked the spec'snohup. Fixed in 756bb38: the directory is made escalated and chowned to the ssh user, the wrapper runs undernohup, and the pid file names the command itself sojob_killsignals it.Known follow-ups, not blockers
createstill writesallow_exec = trueregardless of the chosen level, andwire.VMstill emits it besideagent_access; no reader enforces it.Summary by CodeRabbit
New Features
mcp installandmcp doctor.Documentation
Refactor