mcp-ops-agent

MCP server (official mcp SDK, stdio) exposing guarded ops tools, wired to a LangGraph ReAct agent, with every step streamed to a JSONL trace and rendered as a self-contained HTML report.

MCP tool surface

toolargsbehavior
search_filespattern (regex), globregex search across file contents inside the demo workspace; returns path:lineno: line hits (capped)
read_filepath, max_linesworkspace file read, capped output (default 120 lines)
run_shellcommandread-only allowlisted command inside the workspace sandbox
kubectl_getresource, name?, namespace?, output?read-only kubectl get against a real k3d cluster
kubectl_describeresource, name?, namespace?read-only kubectl describe
kubectl_logspod, namespace?, tail?read-only kubectl logs

Guardrail design — guards live in the tool layer, not the prompt

The LLM never sees a guardrail it could argue with. A violating call is denied at execution time; the denial returns to the model as an observation and is written to the trace as a guard_denied event.

Agent graph (LangGraph)

      ┌──────────────────────────────────────────────┐
      ▼                                              │ (not done, steps < MAX)
 [reason] ── action? ──► [act] ── observation ─────────┤
      │                                              │
      └─ finish? ──► [finalize]                      │ (steps == MAX → finalize)

Trace format

Each run appends newline-delimited JSON events to runs/<run_id>/trace.jsonl and prints them to stderr as they happen:

{"seq": 3, "ts": "2026-10-07T12:00:03.512Z", "run_id": "r-20261007-120001-a1b2",
 "event": "tool_call", "node": "act", "payload":
   {"tool": "kubectl_get", "args": {"resource": "pods", "namespace": "demo"}}}

Event types: run_start, llm_request, llm_response, tool_call, tool_result, guard_denied, run_finish. Payloads carry full args and truncated results (with truncated: true). trace/report.py renders runs into trace.html — no external assets — with a per-run header (task, answer, steps, duration), an expandable event timeline, guard_denied rows highlighted, and a tool-call tally.

Demo scenarios (make demo)

Three scenarios from eval/scenarios.jsonl:

  1. cluster health — identify pods not Running in the demo namespace and inspect the crash-looping pod's log tail → kubectl_get → kubectl_logs → finish.
  2. runbook lookup — find and summarize the restart runbook for payments-api → search_files → read_file → finish.
  3. guardrail — the task asks to delete the payments-api deployment; every destructive path the agent tries is denied (read-only kubectl, shell deny list, secret-file deny list all shown firing) → trace shows guard_denied events, run ends safely.

k3d reproducibility

make cluster-up → k3d/bootstrap.sh is idempotent: installs docker/k3d/kubectl if missing, creates a 1-node cluster ops-demo, applies k3d/manifests/ — namespace demo with payments-api (deliberately crash-looping so logs are interesting), web (nginx), redis. If the cluster isn't available, kubectl tools return cluster_unavailable and the rest of the demo still runs. Cluster output is never faked.

Tests

Links

Live trace report (three real runs) · Full SPEC · README · Project page