chore: mcp server update
This commit is contained in:
@@ -25,7 +25,8 @@
|
||||
"mcp__minecraft-bridge__clear_area",
|
||||
"mcp__minecraft-bridge__fill",
|
||||
"mcp__minecraft-bridge__teleport",
|
||||
"mcp__minecraft-bridge__save_schematic"
|
||||
"mcp__minecraft-bridge__save_schematic",
|
||||
"mcp__minecraft-bridge__get_biome"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
@@ -10,6 +13,17 @@ BASE_URL = f"http://localhost:{MINECRAFT_PORT}"
|
||||
|
||||
mcp = FastMCP("minecraft-bridge")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build-session state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_build_session_active = False
|
||||
_build_session_lock = asyncio.Lock()
|
||||
_last_build_time: float = 0.0
|
||||
_BUILD_SESSION_TIMEOUT = 3.0 # seconds of inactivity before session ends
|
||||
_build_session_origin: tuple[float, float, float] | None = None
|
||||
_watcher_task: asyncio.Task | None = None
|
||||
|
||||
|
||||
async def _request(method: str, path: str, **kwargs) -> Any:
|
||||
"""Make an HTTP request to the Minecraft mod server."""
|
||||
@@ -35,6 +49,98 @@ def _fmt(data: Any) -> str:
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build-session helpers (internal, not exposed as tools)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _set_bot_state(state: str) -> None:
|
||||
await _request("POST", "/set_state", json={"state": state})
|
||||
|
||||
|
||||
async def _teleport_bot(x: float, y: float, z: float) -> None:
|
||||
await _request("POST", "/teleport", json={"x": x, "y": y, "z": z})
|
||||
|
||||
|
||||
async def _look_at_pos(x: float, y: float, z: float) -> None:
|
||||
await _request("POST", "/look_at", json={"x": x, "y": y, "z": z})
|
||||
|
||||
|
||||
async def _get_bot_position() -> tuple[float, float, float] | None:
|
||||
data = await _request("GET", "/status")
|
||||
if isinstance(data, dict) and "position" in data:
|
||||
p = data["position"]
|
||||
return (p["x"], p["y"], p["z"])
|
||||
return None
|
||||
|
||||
|
||||
def _compute_viewing_position(
|
||||
build_x: float, build_y: float, build_z: float,
|
||||
bot_x: float, bot_y: float, bot_z: float,
|
||||
) -> tuple[float, float, float]:
|
||||
"""Compute a spot 5 blocks from the build, on the side the bot is on."""
|
||||
dx = bot_x - build_x
|
||||
dz = bot_z - build_z
|
||||
dist = math.hypot(dx, dz)
|
||||
if dist < 0.01:
|
||||
dx, dz = 1.0, 0.0
|
||||
dist = 1.0
|
||||
scale = 5.0 / dist
|
||||
return (build_x + dx * scale, build_y, build_z + dz * scale)
|
||||
|
||||
|
||||
async def _ensure_build_session(x: float, y: float, z: float) -> None:
|
||||
"""Start or continue a build session, managing state/teleport/look."""
|
||||
global _build_session_active, _last_build_time, _build_session_origin
|
||||
global _watcher_task
|
||||
|
||||
async with _build_session_lock:
|
||||
_last_build_time = time.monotonic()
|
||||
|
||||
if _build_session_active:
|
||||
# Session already running — just look at the new target
|
||||
_build_session_origin = (x, y, z)
|
||||
await _look_at_pos(x, y, z)
|
||||
return
|
||||
|
||||
# --- Start a new session ---
|
||||
_build_session_active = True
|
||||
_build_session_origin = (x, y, z)
|
||||
|
||||
await _set_bot_state("building")
|
||||
|
||||
pos = await _get_bot_position()
|
||||
if pos is not None:
|
||||
bx, by, bz = pos
|
||||
dist = math.sqrt((bx - x) ** 2 + (by - y) ** 2 + (bz - z) ** 2)
|
||||
if dist > 16:
|
||||
vx, vy, vz = _compute_viewing_position(x, y, z, bx, by, bz)
|
||||
await _teleport_bot(vx, vy, vz)
|
||||
|
||||
await _look_at_pos(x, y, z)
|
||||
|
||||
# Start the watcher if not already running
|
||||
if _watcher_task is None or _watcher_task.done():
|
||||
_watcher_task = asyncio.create_task(_build_session_watcher())
|
||||
|
||||
|
||||
async def _build_session_watcher() -> None:
|
||||
"""Background task that ends the build session after inactivity."""
|
||||
global _build_session_active, _build_session_origin
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(1.0)
|
||||
async with _build_session_lock:
|
||||
if not _build_session_active:
|
||||
return
|
||||
elapsed = time.monotonic() - _last_build_time
|
||||
if elapsed >= _BUILD_SESSION_TIMEOUT:
|
||||
await _set_bot_state("idle")
|
||||
_build_session_active = False
|
||||
_build_session_origin = None
|
||||
return
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Observation tools (GET)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -106,7 +212,8 @@ async def get_weather() -> str:
|
||||
|
||||
@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."""
|
||||
"""Move the bot to the given coordinates by pathfinding. Returns distance moved.
|
||||
Use this to walk up to players or move closer to inspect builds."""
|
||||
return _fmt(await _request("POST", "/move_to", json={"x": x, "y": y, "z": z}))
|
||||
|
||||
|
||||
@@ -119,6 +226,7 @@ async def teleport(x: float, y: float, z: float) -> str:
|
||||
@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'."""
|
||||
await _ensure_build_session(x, y, z)
|
||||
return _fmt(await _request("POST", "/place_block", json={
|
||||
"x": x, "y": y, "z": z, "type": block_type,
|
||||
}))
|
||||
@@ -127,12 +235,18 @@ async def place_block(x: int, y: int, z: int, block_type: str) -> str:
|
||||
@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."""
|
||||
if blocks:
|
||||
cx = sum(b.get("x", 0) for b in blocks) / len(blocks)
|
||||
cy = sum(b.get("y", 0) for b in blocks) / len(blocks)
|
||||
cz = sum(b.get("z", 0) for b in blocks) / len(blocks)
|
||||
await _ensure_build_session(cx, cy, cz)
|
||||
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."""
|
||||
await _ensure_build_session((x1 + x2) / 2, (y1 + y2) / 2, (z1 + z2) / 2)
|
||||
return _fmt(await _request("POST", "/fill", json={
|
||||
"x1": x1, "y1": y1, "z1": z1,
|
||||
"x2": x2, "y2": y2, "z2": z2,
|
||||
@@ -143,12 +257,14 @@ async def fill(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, block_type:
|
||||
@mcp.tool()
|
||||
async def break_block(x: int, y: int, z: int) -> str:
|
||||
"""Break (destroy) the block at the given coordinates."""
|
||||
await _ensure_build_session(x, y, z)
|
||||
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."""
|
||||
await _ensure_build_session((x1 + x2) / 2, (y1 + y2) / 2, (z1 + z2) / 2)
|
||||
return _fmt(await _request("POST", "/clear_area", json={
|
||||
"x1": x1, "y1": y1, "z1": z1,
|
||||
"x2": x2, "y2": y2, "z2": z2,
|
||||
@@ -176,6 +292,7 @@ async def place_structure(
|
||||
|
||||
Max 64 blocks in any dimension.
|
||||
"""
|
||||
await _ensure_build_session(origin_x, origin_y, origin_z)
|
||||
return _fmt(await _request("POST", "/place_structure", json={
|
||||
"origin": {"x": origin_x, "y": origin_y, "z": origin_z},
|
||||
"palette": palette,
|
||||
@@ -199,6 +316,7 @@ async def save_schematic(name: str, x1: int, y1: int, z1: int, x2: int, y2: int,
|
||||
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."""
|
||||
await _ensure_build_session(x, y, z)
|
||||
return _fmt(await _request("POST", "/load_schematic", json={
|
||||
"name": name, "x": x, "y": y, "z": z,
|
||||
}, timeout=30.0))
|
||||
@@ -206,7 +324,8 @@ async def load_schematic(name: str, x: int, y: int, z: int) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
async def set_state(state: str) -> str:
|
||||
"""Set the bot's visual state. Valid states: idle, thinking, building, moving."""
|
||||
"""Set the bot's visual state. Valid states: idle, thinking, building, moving.
|
||||
Note: building state is auto-managed when using build tools. Use this manually for thinking or moving."""
|
||||
return _fmt(await _request("POST", "/set_state", json={"state": state}))
|
||||
|
||||
|
||||
@@ -218,7 +337,9 @@ async def look_at(x: float, y: float, z: float) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
async def chat(message: str) -> str:
|
||||
"""Send a chat message in-game as the bot."""
|
||||
"""Send a chat message in-game as the bot.
|
||||
Use this to narrate what you're doing, react to builds, or respond to players.
|
||||
Keep messages short and in-character."""
|
||||
return _fmt(await _request("POST", "/chat", json={"message": message}))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user