-
Notifications
You must be signed in to change notification settings - Fork 3
docs: rewrite README for operators and clients #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alexeykazakov
merged 5 commits into
codeready-toolchain:master
from
alexeykazakov:docs/rewrite-readme
Jul 29, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,119 +1,157 @@ | ||
| # cli-mcp-server | ||
|
|
||
| MCP server that provides AI agents with a sandboxed bash shell inside per-investigation Kubernetes pods. | ||
| MCP server that gives AI agents a **sandboxed bash shell** inside per-session Kubernetes pods. | ||
|
|
||
| ## Architecture | ||
| Agents call a single `bash` tool — pipes, redirects, chaining, and whatever CLIs are installed in the sandbox image. The MCP server is a stateless control plane: it creates and routes to sandbox pods, then proxies commands. Session state (working directory, env vars, files) lives in the pod’s persistent bash process. | ||
|
|
||
| Two binaries in one repo: | ||
| Designed to run against a Kubernetes (or OpenShift) cluster — the server manages sandbox pods via the API. It is not a local-only shell MCP. | ||
|
|
||
| - **cli-mcp-server** (`cmd/server/`) — Control plane. Manages sandbox pod lifecycle via client-go, exposes the `bash` MCP tool, proxies commands to sandbox agents. | ||
| - **sandbox-agent** (`cmd/agent/`) — Data plane. Runs inside each sandbox pod, manages a persistent bash process with `oc`/`kubectl`/`jq`/`yq`/`curl`. | ||
| ## Features | ||
|
|
||
| ## Project Structure | ||
| - **Persistent bash per session** — cwd, environment, and `/workspace` files survive across tool calls | ||
| - **Full shell, not a command allowlist** — security comes from pod isolation, RBAC, and network policy | ||
| - **Per-session agent auth** — MCP server → sandbox `/exec` uses an HMAC-derived bearer token (defense in depth beyond NetworkPolicy) | ||
| - **Customizable CLIs** — available tools are whatever is in the sandbox agent image (the default image includes `oc`/`kubectl`) | ||
| - **Stateless, multi-replica ready** — any server replica can handle any request; Kubernetes is the source of truth | ||
| - **Optional warm pool** — pre-warmed pods cut cold-start latency when enabled | ||
|
|
||
| ``` | ||
| cli-mcp-server/ | ||
| ├── cmd/ | ||
| │ ├── server/main.go # MCP server entry point | ||
| │ └── agent/main.go # Sandbox agent entry point | ||
| ├── pkg/ | ||
| │ ├── session/ # Pod lifecycle, warm pool, cache | ||
| │ ├── agent/ # HTTP client + shared types | ||
| │ ├── sandbox/ # Bash session + HTTP handlers | ||
| │ ├── server/ # MCP server setup | ||
| │ ├── tools/ # MCP tool handlers (bash) | ||
| │ └── version/ # Build version info | ||
| ├── docs/ | ||
| │ ├── proposals/ # Design documents | ||
| │ └── implementation/ # Implementation plan and Jira stories | ||
| ├── Makefile | ||
| └── go.mod | ||
| ``` | ||
| ## Usage | ||
|
|
||
| ## Build | ||
| Works with any MCP client that can call tools over HTTP (or stdio) and send an `X-Session-ID` header. Over HTTP this is straightforward; over stdio it depends on whether the client/SDK can attach request headers. | ||
|
|
||
| ```bash | ||
| make build # Build both server and agent | ||
| make build-server # Build only the MCP server | ||
| make build-agent # Build only the sandbox agent | ||
| make test # Run tests | ||
| make lint # Run linter | ||
| make build-prod # Production build (static, CGO disabled) | ||
| ### MCP tool: `bash` | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `command` | string | yes | Shell command (full bash — pipes, redirects, chaining) | | ||
| | `timeout` | int | no | Max execution time in seconds (default 60, max 300) | | ||
|
|
||
| Example calls: | ||
|
|
||
| ```json | ||
| {"command": "kubectl get pods -n kube-system --context=prod"} | ||
| {"command": "kubectl get pods -o json | jq '.items[].metadata.name'", "timeout": 120} | ||
| ``` | ||
|
|
||
| ## Session & Pod Lifecycle | ||
| Returns `stdout`, `stderr`, `exit_code`, and `duration_ms`. Non-zero exit codes are tool results (not transport errors) so the agent can use failure output. | ||
|
|
||
| Each investigation gets its own sandbox pod. The MCP server is stateless — session | ||
| state lives in Kubernetes (pod labels/annotations) and is cached in memory for performance. | ||
| ### Session routing | ||
|
|
||
| ### Request Flow | ||
| Every request must include: | ||
|
|
||
| ```http | ||
| X-Session-ID: <session-id> | ||
| ``` | ||
| TARSy (LLM) | ||
| │ MCP tool call + X-Session-ID header | ||
| ▼ | ||
| MCP Server (stateless, multi-replica) | ||
| │ HTTP POST /exec + Bearer HMAC token | ||
| ▼ | ||
| Sandbox Pod (per-session, persistent bash) | ||
| │ stdout / stderr / exit_code | ||
| ▼ | ||
| MCP Server → TARSy | ||
| ``` | ||
|
|
||
| ### GetOrCreatePod — Three-Tier Lookup | ||
| The server only uses this value to find or create a sandbox pod. Session IDs must be RFC 1123 DNS labels (`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`); invalid IDs fail at pod create. | ||
|
|
||
| Typical client practice (agent harness / orchestrator — not the LLM): | ||
|
|
||
| 1. Generate a new session ID when starting work that should share one sandbox | ||
| 2. Send that same ID on every follow-up `bash` call to reuse the persistent shell and `/workspace` | ||
| 3. Call `DELETE /sessions/{id}` when finished (HTTP transport only) — removes the sandbox pod, auth secret, and cache entry | ||
|
|
||
| When a command arrives for a session, `SessionManager.GetOrCreatePod` resolves the | ||
| target pod using a three-tier strategy: | ||
| If the client never deletes the session, the sandbox is garbage-collected after `--idle-timeout` (default 30m). | ||
|
|
||
| | Tier | Method | Latency | Description | | ||
| |------|--------|---------|-------------| | ||
| | 1 | **PodCache** | ~0ms | In-memory map of session ID → pod IP with 30s TTL | | ||
| | 2 | **Label discovery** | ~50-100ms | `List` pods by `tarsy.redhat.com/session-id` label | | ||
| | 3 | **Create** | ~3-8s | Create auth Secret + Pod, wait for readiness probe | | ||
| ## Configuration | ||
|
|
||
| Cache misses fall through to tier 2; if no pod exists, tier 3 creates one | ||
| idempotently (handles `AlreadyExists`). | ||
| ### Server flags | ||
|
|
||
| ### Warm Pool | ||
| | Flag | Default | Description | | ||
| |------|---------|-------------| | ||
| | `--transport` | `stdio` | `stdio` or `http` (`http` requires `--stateless`) | | ||
| | `--address` | `localhost:8080` | Listen address (HTTP; must be loopback) | | ||
| | `--stateless` | `false` | Required for HTTP / multi-replica | | ||
| | `--namespace` | `tarsy` | Namespace for sandbox pods | | ||
| | `--sandbox-image` | _(required)_ | Container image for sandbox pods | | ||
| | `--hmac-key-file` | _(required)_ | Path to shared HMAC secret | | ||
| | `--kubeconfig` | _(in-cluster)_ | Kubeconfig for managing sandbox pods | | ||
| | `--idle-timeout` | `30m` | Delete idle sandbox pods after this duration | | ||
| | `--warm-pool-size` | `0` | Pre-warmed pods (`0` = create on demand) | | ||
|
|
||
| When `--warm-pool-size N` is set, N pre-warmed pods are maintained in a ready state. | ||
| New sessions are assigned a warm pod instantly (~0ms) instead of waiting for a cold | ||
| start. Disabled by default (`N=0`). | ||
| ### Sandbox image (available CLIs) | ||
|
|
||
| ### Pod Spec | ||
| CLIs available to the agent are determined by the **sandbox agent image**, not by server code. | ||
|
|
||
| Each sandbox pod runs with a hardened security context: | ||
| The default image (`Containerfile.agent`) is based on `oc-client-base-minimal` and includes `oc`, `kubectl`, plus utilities such as `jq`, `yq`, and `curl`. | ||
|
|
||
| - **Non-root**: UID/GID 1001, `allowPrivilegeEscalation: false`, all capabilities dropped | ||
| - **Resources**: 100m/500m CPU, 128Mi/512Mi memory | ||
| - **Readiness**: HTTP GET `/health` on port 8090 | ||
| - **Volumes**: `kubeconfig` (read-only Secret mount), `workspace` (emptyDir) | ||
| - **Auth**: HMAC-SHA256(key, sessionID) → bearer token delivered via Secret → env var | ||
| To add other CLIs (for example `helm` or `virtctl`): | ||
|
|
||
| ### Cleanup | ||
| 1. Extend `Containerfile.agent` (or build a custom image from it) | ||
| 2. Build and push the image | ||
| 3. Point the MCP server at it with `--sandbox-image` | ||
|
|
||
| | Method | Trigger | Description | | ||
| |--------|---------|-------------| | ||
| | **Explicit delete** | `DELETE /sessions/{id}` | TARSy calls when investigation ends. Deletes pod, Secret, and cache entry | | ||
| | **Stale cleanup** | Background ticker | Deletes pods idle > 30 minutes (via `last-activity` annotation) | | ||
| No MCP server code changes are required. Tell the LLM what is available via your client’s server instructions (or equivalent); the `bash` tool description stays generic. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ```mermaid | ||
| flowchart TB | ||
| Client[MCP Client] | ||
| Server["cli-mcp-server<br/>stateless · N replicas"] | ||
| Sandbox["Sandbox pods<br/>assigned sessions · optional warm pool"] | ||
| Target["Target infrastructure<br/>e.g. Kubernetes API"] | ||
|
|
||
| Client -->|"bash + X-Session-ID"| Server | ||
| Server -->|"POST /exec + HMAC token"| Sandbox | ||
| Sandbox -->|"CLIs from sandbox image / config"| Target | ||
| ``` | ||
|
|
||
| If a sandbox pod crashes mid-investigation, shell state (env vars, working directory, | ||
| files in emptyDir) is lost. The MCP server detects the transport error, invalidates | ||
| the cache, and creates a new pod on the next call. TARSy retains the investigation | ||
| context. | ||
| Bash commands run in the sandbox pods. What they can reach (for example a Kubernetes API via `kubectl`) depends on the sandbox image and the kubeconfig/RBAC mounted into those pods — not on the MCP server binary. | ||
|
|
||
| ## Design Documents | ||
| ### Scalability | ||
|
|
||
| - [Sketch](docs/proposals/cli-mcp-server-sketch.md) — Problem statement and approach | ||
| - [Design Overview](docs/proposals/cli-mcp-server-design-overview.md) — Architecture overview | ||
| - [Detailed Design](docs/proposals/cli-mcp-server-design.md) — Go types, flows, YAML specs | ||
| The MCP server holds no durable session state. Pod identity is stored in Kubernetes labels; an in-memory cache speeds up routing. Any replica can serve any request, so you can scale the Deployment horizontally behind a load balancer with no sticky sessions. | ||
|
|
||
| Optional `--warm-pool-size` keeps ready pods on hand so new sessions skip cold start (image pull + container boot). | ||
|
|
||
| ### Sandboxing and security | ||
|
|
||
| Each session gets its own pod. That pod is the security boundary: | ||
|
|
||
| - **Isolation** — non-root (runAsNonRoot), no privilege escalation, all capabilities dropped, resource limits | ||
| - **Credentials** — read-only kubeconfig mounted from a dedicated investigation ServiceAccount (typically view/read-only RBAC) | ||
| - **Network** — NetworkPolicy can restrict ingress to the MCP server and egress to intended APIs | ||
| - **Agent auth** — per-session HMAC bearer token; unauthenticated `/exec` calls are rejected | ||
| - **Ephemeral workspace** — `/workspace` is an `emptyDir`; destroyed with the pod | ||
|
|
||
| The server does not filter shell commands. Capability is controlled by what is in the image and what RBAC allows. | ||
|
|
||
| ### Components | ||
|
|
||
| | Component | Role | | ||
| |-----------|------| | ||
| | **cli-mcp-server** | Control plane: MCP `bash` tool, pod lifecycle, command proxy | | ||
| | **sandbox-agent** | Data plane inside each pod: persistent bash over HTTP (`/exec`, `/health`, `/assign`) | | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| make build # Build both server and agent | ||
| make build-server # Build only the MCP server | ||
| make build-agent # Build only the sandbox agent | ||
| make test # Run tests | ||
| make lint # Run linter | ||
| make build-prod # Production build (static, CGO disabled) | ||
| ``` | ||
|
|
||
| ``` | ||
| cli-mcp-server/ | ||
| ├── cmd/server/ # MCP server entry point | ||
| ├── cmd/agent/ # Sandbox agent entry point | ||
| ├── pkg/session/ # Pod lifecycle, warm pool, cache | ||
| ├── pkg/sandbox/ # Bash session + agent HTTP handlers | ||
| ├── pkg/tools/ # MCP tool handlers | ||
| ├── pkg/server/ # MCP server + HTTP mux | ||
| └── docs/ # Design and implementation docs | ||
| ``` | ||
|
|
||
| ## Implementation | ||
| ### Further reading | ||
|
|
||
| - [Implementation Plan](docs/implementation/implementation-plan.md) — Phased plan | ||
| - [Jira Stories](docs/implementation/stories.md) — Epic SANDBOX-1803 breakdown | ||
| - [Sketch](docs/proposals/cli-mcp-server-sketch.md) — problem statement and approach | ||
| - [Design Overview](docs/proposals/cli-mcp-server-design-overview.md) — architecture overview | ||
| - [Detailed Design](docs/proposals/cli-mcp-server-design.md) — types, flows, deployment specs | ||
|
|
||
| ## License | ||
|
|
||
| The code is available under the [Apache License 2.0](LICENSE). | ||
| [Apache License 2.0](LICENSE) | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.