Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 123 additions & 85 deletions README.md
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.
Comment thread
alexeykazakov marked this conversation as resolved.

```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)
Loading