chore: initial commit

This commit is contained in:
2026-02-15 10:50:17 +01:00
commit 055b93817a
78 changed files with 23243 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# minecraft-mcp
MCP server that bridges Claude Code to a Minecraft bot via the Clod mod's HTTP API.
## Installation
```bash
cd minecraft-mcp
pip install -e .
```
## Register with Claude Code
```bash
claude mcp add minecraft-bridge -- python -m minecraft_mcp.server
```
Verify it's registered:
```bash
claude mcp list
```
## Usage
1. Launch Minecraft with the Clod mod (HTTP server runs on port 8766)
2. Start Claude Code — the MCP server connects automatically
3. Ask Claude to interact with the bot: "check the bot status", "move to 100 64 100", etc.
## Configuration
Set `MINECRAFT_PORT` environment variable to override the default port (8766):
```bash
claude mcp add minecraft-bridge -e MINECRAFT_PORT=9000 -- python -m minecraft_mcp.server
```
## Available Tools
### Observation
- `get_bot_status` — position, state, dimension
- `get_blocks` — 3D block array in a bounding box (max 32x32x32)
- `get_block` — single block type and properties
- `get_nearby_entities` — entities within radius (default 16, max 64)
- `get_inventory` — bot's inventory contents
- `get_biome` — biome at X/Z coordinates
- `list_block_types` — all block IDs, with optional filter
### Actions
- `move_to` — pathfind to coordinates
- `teleport` — instant teleport
- `place_block` — place a single block
- `place_blocks` — place up to 10,000 blocks
- `fill` — fill a bounding box (max 32x32x32)
- `break_block` — destroy a block
- `set_state` — set bot visual state (idle/thinking/building/moving)
- `look_at` — aim at coordinates
- `chat` — send in-game chat message
### Interaction
- `wait_for_instruction` — long-poll for player `!clod` commands (300s timeout)
+16
View File
@@ -0,0 +1,16 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "minecraft-mcp"
version = "0.1.0"
description = "MCP server bridging Claude Code to a Minecraft bot via the Clod mod"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.0",
"httpx",
]
[project.scripts]
minecraft-mcp = "minecraft_mcp.server:main"
@@ -0,0 +1,3 @@
from minecraft_mcp.server import main
main()
+248
View File
@@ -0,0 +1,248 @@
import json
import os
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
MINECRAFT_PORT = os.environ.get("MINECRAFT_PORT", "8766")
BASE_URL = f"http://localhost:{MINECRAFT_PORT}"
mcp = FastMCP("minecraft-bridge")
async def _request(method: str, path: str, **kwargs) -> Any:
"""Make an HTTP request to the Minecraft mod server."""
kwargs.setdefault("timeout", 5.0)
try:
async with httpx.AsyncClient(base_url=BASE_URL) as client:
resp = await client.request(method, path, **kwargs)
resp.raise_for_status()
return resp.json()
except httpx.ConnectError:
return {"error": "Cannot connect to Minecraft. Is the game running with the Clod mod?"}
except httpx.HTTPStatusError as e:
try:
return e.response.json()
except Exception:
return {"error": f"Minecraft error ({e.response.status_code}): {e.response.text}"}
except Exception as e:
return {"error": str(e)}
def _fmt(data: Any) -> str:
"""Format response data as a JSON string."""
return json.dumps(data, indent=2)
# ---------------------------------------------------------------------------
# Observation tools (GET)
# ---------------------------------------------------------------------------
@mcp.tool()
async def get_bot_status() -> str:
"""Get the bot's current status including position, state, and dimension."""
return _fmt(await _request("GET", "/status"))
@mcp.tool()
async def get_blocks(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> str:
"""Get all blocks in a bounding box. Returns a 3D array of block IDs.
Max volume is 32x32x32 blocks."""
return _fmt(await _request("GET", "/blocks", params={
"x1": x1, "y1": y1, "z1": z1,
"x2": x2, "y2": y2, "z2": z2,
}))
@mcp.tool()
async def get_block(x: int, y: int, z: int) -> str:
"""Get the block type and properties at the given coordinates."""
return _fmt(await _request("GET", "/block", params={"x": x, "y": y, "z": z}))
@mcp.tool()
async def get_nearby_entities(radius: int = 16) -> str:
"""Get entities near the bot. Returns type, name, position, and distance.
Radius defaults to 16, max 64."""
return _fmt(await _request("GET", "/nearby_entities", params={"radius": radius}))
@mcp.tool()
async def get_inventory() -> str:
"""Get the bot's inventory. Returns slot number, item ID, and count for each item."""
return _fmt(await _request("GET", "/inventory"))
@mcp.tool()
async def get_biome(x: int, z: int) -> str:
"""Get the biome at the given X/Z coordinates (uses bot's current Y)."""
return _fmt(await _request("GET", "/biome", params={"x": x, "z": z}))
@mcp.tool()
async def list_block_types(filter: str = "") -> str:
"""List all available block type IDs, optionally filtered by a substring."""
return _fmt(await _request("GET", "/block_types", params={"filter": filter}))
@mcp.tool()
async def get_time() -> str:
"""Get the current world time. Returns total game ticks, day-time ticks, and whether it is daytime."""
return _fmt(await _request("GET", "/get_time"))
@mcp.tool()
async def get_weather() -> str:
"""Get the current weather. Returns whether it is raining and/or thundering."""
return _fmt(await _request("GET", "/get_weather"))
# ---------------------------------------------------------------------------
# Action tools (POST)
# ---------------------------------------------------------------------------
@mcp.tool()
async def move_to(x: float, y: float, z: float) -> str:
"""Move the bot to the given coordinates by pathfinding. Returns distance moved."""
return _fmt(await _request("POST", "/move_to", json={"x": x, "y": y, "z": z}))
@mcp.tool()
async def teleport(x: float, y: float, z: float) -> str:
"""Teleport the bot instantly to the given coordinates."""
return _fmt(await _request("POST", "/teleport", json={"x": x, "y": y, "z": z}))
@mcp.tool()
async def place_block(x: int, y: int, z: int, block_type: str) -> str:
"""Place a block at the given coordinates. block_type is a Minecraft block ID like 'stone' or 'oak_planks'."""
return _fmt(await _request("POST", "/place_block", json={
"x": x, "y": y, "z": z, "type": block_type,
}))
@mcp.tool()
async def place_blocks(blocks: list[dict]) -> str:
"""Place multiple blocks at once. Each entry is {x, y, z, type}. Max 10,000 blocks per call."""
return _fmt(await _request("POST", "/place_blocks", json={"blocks": blocks}))
@mcp.tool()
async def fill(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, block_type: str) -> str:
"""Fill a bounding box with a single block type. Max volume 32x32x32."""
return _fmt(await _request("POST", "/fill", json={
"x1": x1, "y1": y1, "z1": z1,
"x2": x2, "y2": y2, "z2": z2,
"type": block_type,
}))
@mcp.tool()
async def break_block(x: int, y: int, z: int) -> str:
"""Break (destroy) the block at the given coordinates."""
return _fmt(await _request("POST", "/break_block", json={"x": x, "y": y, "z": z}))
@mcp.tool()
async def clear_area(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> str:
"""Clear all blocks in a bounding box (replace with air). Max volume 64x64x64."""
return _fmt(await _request("POST", "/clear_area", json={
"x1": x1, "y1": y1, "z1": z1,
"x2": x2, "y2": y2, "z2": z2,
}))
@mcp.tool()
async def place_structure(
origin_x: int, origin_y: int, origin_z: int,
palette: dict[str, str], layers: list[list[str]]
) -> str:
"""Place a structure defined by a palette and layer strings.
Each palette key is a single character mapped to a block ID (e.g. {"1": "minecraft:stone"}).
Layers is a list of Y-slices (bottom to top). Each slice is a list of Z-row strings.
Each character in a row string places one block along X. Spaces are skipped (air).
Example — a 3x3x3 hollow stone cube:
palette: {"1": "minecraft:stone"}
layers: [
["111", "111", "111"], # bottom (solid)
["111", "1 1", "111"], # middle (hollow)
["111", "111", "111"] # top (solid)
]
Max 64 blocks in any dimension.
"""
return _fmt(await _request("POST", "/place_structure", json={
"origin": {"x": origin_x, "y": origin_y, "z": origin_z},
"palette": palette,
"layers": layers,
}, timeout=30.0))
@mcp.tool()
async def save_schematic(name: str, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> str:
"""Save all non-air blocks in a bounding box to a named schematic file.
Name may only contain letters, numbers, underscore, and hyphen.
Max volume 64x64x64. Schematics are saved in the world's schematics folder."""
return _fmt(await _request("POST", "/save_schematic", json={
"name": name,
"x1": x1, "y1": y1, "z1": z1,
"x2": x2, "y2": y2, "z2": z2,
}, timeout=15.0))
@mcp.tool()
async def load_schematic(name: str, x: int, y: int, z: int) -> str:
"""Load a previously saved schematic and place it at the given origin coordinates.
Name may only contain letters, numbers, underscore, and hyphen."""
return _fmt(await _request("POST", "/load_schematic", json={
"name": name, "x": x, "y": y, "z": z,
}, timeout=30.0))
@mcp.tool()
async def set_state(state: str) -> str:
"""Set the bot's visual state. Valid states: idle, thinking, building, moving."""
return _fmt(await _request("POST", "/set_state", json={"state": state}))
@mcp.tool()
async def look_at(x: float, y: float, z: float) -> str:
"""Make the bot look at the given coordinates."""
return _fmt(await _request("POST", "/look_at", json={"x": x, "y": y, "z": z}))
@mcp.tool()
async def chat(message: str) -> str:
"""Send a chat message in-game as the bot."""
return _fmt(await _request("POST", "/chat", json={"message": message}))
# ---------------------------------------------------------------------------
# Interaction tool (long-poll)
# ---------------------------------------------------------------------------
@mcp.tool()
async def wait_for_instruction() -> str:
"""Wait for a player to send a !clod command in Minecraft chat.
Long-polls for up to 300 seconds. Returns {player, message} or {timeout: true}.
The !clod prefix is already stripped by the mod."""
return _fmt(await _request("GET", "/chat/wait", timeout=310.0))
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
main()