chore: initial commit
This commit is contained in:
@@ -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)
|
||||
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
Reference in New Issue
Block a user