AI × Minecraft × AWS
You type "move 3 blocks east" into a web UI. The bot inside a Minecraft world starts thinking. Its thinking streams back to your browser. You see the tool calls — get-position → move-to-position — and finally an answer: "I'm now at (7, 69, 7)."
This post is the full story of how I built that on Strands Agents + Amazon Bedrock AgentCore Runtime + Sonnet 4.6 (Extended Thinking) + MCP + Mineflayer, deployed it to production via Vercel and EC2, and added 8-language support along the way.
What it looks like
- Live demo: https://minecraft-ai-lovat.vercel.app
- Architecture:
| Layer | Tech |
|---|---|
| Frontend | Next.js 16 (App Router) on Vercel, 8-language switcher, collapsible thinking view |
| Agent | Strands Agents (Python) on Amazon Bedrock AgentCore Runtime |
| LLM | Claude Sonnet 4.6 (Extended Thinking, interleaved-thinking beta) via Amazon Bedrock |
| Bot control | minecraft-mcp-server (Mineflayer + MCP) packaged inside the Runtime container |
| Minecraft | Java Edition 1.21.11 on EC2 (ap-northeast-1, t4g.small) |
| Spectate | prismarine-viewer running locally on a Mac, connecting up to EC2 |
| Observability | CloudWatch GenAI Observability (OTLP / SigV4) |
Why this stack
It started as "I want to control Minecraft with AI". Then the requirement list grew: use Strands Agents, ship to AgentCore Runtime, send traces to AgentCore Observability, expose Extended Thinking, build a real web UI, support multiple languages. The stack above is what survived.
Two non-obvious design decisions:
-
How to run MCP from the Runtime.
minecraft-mcp-serveris Node.js. The default AgentCore Runtime image is Python-only. I customized the Dockerfile to bundle Node.js inside the same container and call the MCP server via stdio. Cleaner than running a separate HTTP MCP server. - Where to put the Minecraft server. The bot connects to Minecraft over plain TCP 25565. The Runtime is in the cloud, so the Minecraft server has to be reachable from the cloud. I put it on EC2 in ap-northeast-1, opening 25565 to 0.0.0.0/0. With offline-mode + creative + peaceful, the blast radius is small.
A walk through the layers
1. Strands agent entrypoint
To run on AgentCore Runtime, the entrypoint must be a bedrock_agentcore.BedrockAgentCoreApp with @app.entrypoint. Inside, we just call the Strands agent and yield each event from stream_async() as a JSON line.
from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
from strands.telemetry import StrandsTelemetry
from mcp import stdio_client, StdioServerParameters
StrandsTelemetry().setup_otlp_exporter() # send traces to CloudWatch
app = BedrockAgentCoreApp()
@app.entrypoint
async def handler(payload: dict):
message = payload.get("message", "")
lang = payload.get("lang", "en")
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-6",
region_name="us-east-1",
additional_request_fields={
"anthropic_beta": ["interleaved-thinking-2025-05-14"],
"thinking": {"type": "enabled", "budget_tokens": 4000},
},
)
mcp = MCPClient(lambda: stdio_client(StdioServerParameters(
command="npx",
args=["-y", "github:yuniko-software/minecraft-mcp-server",
"--host", "54.238.212.208", "--port", "25565",
"--username", "StrandsBot"],
)))
with mcp:
tools = mcp.list_tools_sync()
agent = Agent(model=model, tools=tools, system_prompt=SYSTEM_PROMPT)
async for ev in agent.stream_async(f"[respond in: {lang}]\n{message}"):
# turn reasoning / data / current_tool_use / message into JSON lines
...
The important bit is additional_request_fields — the interleaved-thinking-2025-05-14 beta header and thinking: enabled. With that, Sonnet 4.6 returns reasoning blocks interleaved with tool calls, and you can stream them out as they happen.
2. Dockerfile that bundles Node.js
To run npx minecraft-mcp-server inside an AgentCore Runtime container, I added Node.js 20 to the Python 3.11 base image and pre-pulled the npx package at build time:
FROM --platform=linux/arm64 public.ecr.aws/docker/library/python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates gnupg gcc git \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] \
https://deb.nodesource.com/node_20.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update && apt-get install -y --no-install-recommends nodejs
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Pre-pull the npx package so the first invocation is fast
RUN npx -y github:yuniko-software/minecraft-mcp-server --help 2>/dev/null || true
COPY . .
EXPOSE 8080
CMD ["python", "agentcore_app.py"]
agentcore deploy from bedrock-agentcore-starter-toolkit builds this for ARM64 in CodeBuild, pushes to ECR, and creates the AgentCore Runtime.
3. AgentCore Observability — collectorless OTLP
Call StrandsTelemetry().setup_otlp_exporter() in code, then point OTLP at the CloudWatch endpoint via env vars:
export AGENT_OBSERVABILITY_ENABLED=true
export OTEL_PYTHON_DISTRO=aws_distro
export OTEL_PYTHON_CONFIGURATOR=aws_configurator
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_TRACES_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none # avoid retrying localhost:4318
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="https://logs.us-east-1.amazonaws.com/v1/logs"
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://xray.us-east-1.amazonaws.com/v1/traces"
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="x-aws-log-group=agentcore/minecraft-ai,x-aws-log-stream=agent-runtime-logs"
export OTEL_RESOURCE_ATTRIBUTES="service.name=minecraft-ai-agent,aws.log.group.names=agentcore/minecraft-ai"
opentelemetry-instrument python agent_server.py
After that, aws/spans receives the Bedrock Converse calls, MCP tools/list, and the surrounding agent loop. The CloudWatch GenAI Observability dashboard renders the whole thing as a span tree.
4. Calling Runtime from Next.js (Vercel)
In a Vercel API Route, use the AWS SDK's InvokeAgentRuntimeCommand. The response streams back, but AgentCore Runtime sends data: "<JSON string>" with the inner string itself being an ndjson payload — a double-wrapped SSE format. I missed this at first and the UI showed nothing.
// Two-stage parse
const payload = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
const parsed = JSON.parse(payload);
if (typeof parsed === "string") {
for (const inner of parsed.split("\n")) {
const obj = JSON.parse(inner.trim());
emit(obj); // {type: "thinking", text} / {type: "tool", name, input} / ...
}
} else {
emit(parsed);
}
5. 8 languages + IME safety
i18n is a single dictionary file persisted in localStorage. Japanese, English, French, Spanish, German, Chinese, Korean, Arabic. For Arabic the layout flips automatically by toggling <html dir="rtl">.
I also tag every user message with [respond in: <language>] so that the agent thinks and answers in the user's language.
Small but important: the IME composition-confirm Enter would accidentally submit the form. The fix is to gate the Enter handler on both isComposing and keyCode === 229:
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey
&& !e.nativeEvent.isComposing && e.keyCode !== 229) {
e.preventDefault();
send();
}
}}
Things that bit me
| Symptom | Cause | Fix |
|---|---|---|
CodeBuild POST_BUILD: docker push denied |
Reused a CodeBuild role from another project whose ecr:* was scoped to a different repository |
Added an inline policy granting ecr:* on bedrock-agentcore-*
|
AccessDenied: bedrock-agentcore:InvokeAgentRuntime |
The Vercel-side IAM user policy didn't include the runtime-endpoint/DEFAULT sub-resource ARN |
Added runtime/.../* to the Resource list |
| Nothing shows up in the UI | Runtime stream is data: "<JSON string>" (double-wrapped) |
Parse in two stages |
| No thinking events | Forgot the interleaved-thinking-2025-05-14 beta header |
Added it to additional_request_fields.anthropic_beta
|
prismarine-viewer fails to build on EC2 |
Amazon Linux 2023 + Node 18 + node-canvas combo refused to build cleanly | Skipped EC2; run the viewer locally and point it at EC2 instead |
Monthly cost
- EC2 t4g.small (always on): about $13/month. Stop the instance and it drops to $0.
- AgentCore Runtime: pay-per-invocation; idle costs nothing.
- Bedrock (Sonnet 4.6 with thinking): roughly $0.01–$0.05 per instruction.
- Vercel: stayed inside the free tier.
Demo
(I'll drop a YouTube clip / GIF here at publish time.)
Closing
The "Strands → AgentCore Runtime → Observability" stack is usually demoed on documents, APIs, or pure-text tasks. It turns out it also works just fine for a quasi-physical use case: driving Mineflayer through MCP to push, dig, and walk inside a Minecraft world.
Watching the bot narrate its plan while it places blocks is the most concrete demo of "an agent that actually acts" I've shipped to date. If you have Bedrock access, the first few instructions cost cents — give it a try.
Repo, demo video, and Observability screenshots will be added in updates.

Top comments (0)