Agent Sandbox
MCP Sandbox
A self-contained example of running a Model Context Protocol (MCP) server
inside an Agent Sandbox, using nothing but kubectl. The host acts as the
MCP client, the sandbox hosts the MCP server, and kubectl exec is the
stdio transport between them.
How it works
kubectl exec -i POD -- python3 -u /app/mcp_server.py is a long-lived
bidirectional stdio channel — exactly what MCP’s stdio transport wants.
The host-side MCP client uses kubectl exec as its “subprocess,” and
kubectl pipes JSON-RPC frames into and out of the pod:
┌────────── host ──────────┐ ┌──────── sandbox pod ────────┐
│ │ │ │
│ client.py │ │ mcp_server.py │
│ └─ MCP ClientSession │ ◄──── stdio ───► │ └─ MCP FastMCP server │
│ │ via kubectl exec│ │
│ │ │ /workspace ──► PVC │
│ │ │ │
└──────────────────────────┘ └─────────────────────────────┘
No agent-sandbox Python SDK, no warm pool, no sandbox-router, no in-sandbox driver. The MCP server runs inside the sandbox, owns the PVC mount, and is the only thing in the pod that touches files. The host’s only job is to be the MCP client.
The example exercises the full create → execute → I/O → return → terminate loop, and also proves the PVC is doing real work — not just sitting in the manifest unused:
envsubst < sandbox.yaml | kubectl apply -f -creates aSandboxwith a 1Gi PVC mounted at/workspace.- Session 1:
client.pyopens an MCP session and callslist_blobs(expects empty) →write_random_blob(writes random bytes to the PVC, returns sha256). - Suspend → Resume:
client.pypatchessandbox/mcp-sandboxwithspec.operatingMode: Suspended— the controller deletes the pod but keeps the Sandbox object and the PVC. Then it patches back toRunningand the controller creates a fresh pod with the same PVC reattached. - Session 2:
client.pyopens a second MCP session against the new pod, callslist_blobs(expects['random.bin']) andread_blob(expects the same sha256). If the sha256 doesn’t match, the data didn’t persist — i.e. the PVC isn’t working. OnemptyDiror the container overlay this step would fail. client.pythenkubectl cps the file out and re-hashes locally, confirming the bytes round-trip back to the host.kubectl delete -f sandbox.yamltears everything down.
Files
| File | Role |
|---|---|
sandbox.yaml |
Bare Sandbox CRD. One container running sleep infinity, one 1Gi PVC mounted at /workspace. Uses ${IMAGE} so you can envsubst your registry path. |
Dockerfile |
python:3.11-slim + pip install mcp + copy mcp_server.py. Nothing else. |
mcp_server.py |
Custom MCP server (FastMCP). Exposes list_blobs, write_random_blob(name, size_bytes), read_blob(name). All operate on /workspace (override with MCP_WORKSPACE). |
client.py |
Host-side MCP client. Uses kubectl exec -i as the stdio transport, runs the Suspend→Resume cycle, then kubectl cps the result file out and verifies its sha256. |
requirements.txt |
mcp (Python MCP SDK) for the host. |
Prerequisites
- A Kubernetes cluster with the Agent Sandbox controller installed. Only the core CRDs are needed — no extensions bundle, no sandbox-router.
kubectlconfigured against the cluster, plusdocker,envsubst, and Python 3.10+ on the host.
Run it (local cluster, e.g. Kind)
# 1. Build the image and load it into Kind.
export IMAGE=mcp-sandbox:latest
docker build -t "${IMAGE}" .
kind load docker-image "${IMAGE}" --name agent-sandbox
# 2. Create the sandbox.
envsubst < sandbox.yaml | kubectl apply -f -
kubectl wait --for=condition=Ready sandbox/mcp-sandbox --timeout=120s
# 3. Run the host-side MCP client.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python3 client.py
# 4. Tear down.
kubectl delete -f sandbox.yaml
rm returned-random.bin
Expected client.py output, in order:
============================================================
Session 1 — write a random blob to the PVC
============================================================
[host] tools advertised by server: ['list_blobs', 'write_random_blob', 'read_blob']
[host] list_blobs (before write) -> []
[host] write_random_blob('random.bin', 256) -> {'path': '/workspace/random.bin', 'bytes_written': 256, 'sha256': '...'}
============================================================
Suspend → Resume — the PVC persists, the container fs does not
============================================================
[host] patching sandbox/mcp-sandbox operatingMode=Suspended (controller will delete the pod)...
[host] pod mcp-sandbox is gone (Sandbox is Suspended)
[host] patching sandbox/mcp-sandbox operatingMode=Running (controller will recreate the pod)...
[host] waiting up to 180s for pod mcp-sandbox to be Ready again...
[host] pod mcp-sandbox is Ready again
============================================================
Session 2 — read the blob back from the PVC
============================================================
[host] list_blobs (after restart) -> ['random.bin']
[host] read_blob('random.bin') -> {'path': '/workspace/random.bin', 'size_bytes': 256, 'sha256': '...'}
[host] OK — sha256 matches across pod restart: ...
============================================================
Return the file to the host and re-hash locally
============================================================
[host] kubectl cp mcp-sandbox:/workspace/random.bin -> returned-random.bin
[host] returned 256 bytes; host sha256=...
[host] OK — PVC contents survived pod restart and round-trip back to the host
Run it against a remote cluster (e.g. GKE)
GKE
# 1. One-time: create the cluster and configure auth.
gcloud container clusters create-auto mcp-demo --region=us-central1
gcloud container clusters get-credentials mcp-demo --region=us-central1
gcloud auth configure-docker us-central1-docker.pkg.dev
# 2. Build and push the image to Artifact Registry.
export IMAGE=us-central1-docker.pkg.dev/${PROJECT}/${REPO}/mcp-sandbox:latest
docker build -t "${IMAGE}" .
docker push "${IMAGE}"
# 3. From here the flow is identical to local.
envsubst < sandbox.yaml | kubectl apply -f -
kubectl wait --for=condition=Ready sandbox/mcp-sandbox --timeout=120s
python3 client.py
kubectl delete -f sandbox.yaml
EKS / AKS / other
Same flow — only the registry push and credential commands differ:
- EKS:
aws eks update-kubeconfig --name <cluster>+ push to ECR (aws ecr get-login-password ...). - AKS:
az aks get-credentials -g <rg> -n <cluster>+ push to ACR (az acr login -n <registry>). - Any cluster: push to Docker Hub (
docker push docker.io/<user>/mcp-sandbox).
Once ${IMAGE} resolves to something the cluster’s nodes can pull, the
envsubst / kubectl apply / python3 client.py / kubectl delete
sequence is unchanged.
Latency note
Every MCP request from the host hops through kubectl exec → kube-apiserver
→ kubelet → pod, so round-trip latency is noticeably higher than a local
stdio session. Fine for demos, interactive tooling, and CI; if you need
production-grade throughput for many concurrent MCP sessions, graduate to
the agentic-sandbox-client
SDK, which uses a router/gateway path instead of per-call apiserver hops.
References
- Model Context Protocol — the protocol spec.
- Python MCP SDK — what
mcp_server.pyandclient.pyare built on. - Official MCP servers — drop-in replacements (filesystem, git, time, etc.). To use one, swap the
command/argsinclient.py’sStdioServerParametersand install the server in theDockerfile. - agentic-sandbox-client — when you outgrow
kubectl exec(multi-tenant agent platforms, warm pools, gateway-routed traffic), this is the SDK to graduate to.