NOTE: This is a modernized fork of the (end-of-life) upstream zhuowei/RaspberryJuice. It has been updated to build and run on the latest Paper 26.2 servers and Java 25, verified end-to-end. Existing Python
mcpiscripts keep working unchanged: legacy numeric block/entity ids are bridged to the modernBlockDataAPI via Bukkit's built-in legacy conversion. Seedocs/modernization-notes.mdfor details.
A Bukkit/Paper plugin which implements the Minecraft Pi Socket API, letting Python (and other)
mcpi clients drive Minecraft over a simple TCP protocol.
New: a friendly, typed Python client lives in client/ — pip install raspberryjuice,
then from raspberryjuice import Minecraft. It covers blocks, the turtle agent, entity/mob
control, and world/player commands. See client/README.md.
The plugin is listed as RaspberryJuice Reloaded on Modrinth and Hangar, and both Python clients are on PyPI.
-
Install the plugin. Grab
raspberryjuice-*.jarfrom Modrinth, Hangar, or the latest release (or build it — see Build) and drop it in your Paper 26.2 / Java 25 server'splugins/folder, then start the server. It listens onlocalhost:4711by default. -
Install the Python client.
pip install raspberryjuice # or, from a clone: pip install ./client -
Drive Minecraft from Python.
from raspberryjuice import Minecraft mc = Minecraft.connect("localhost", 4711) mc.post_to_chat("Hello from Python!") mc.set_block(0, 10, 0, 41) # place a gold block print(mc.get_block(0, 10, 0)) # -> 41
On a shared or networked server, read SECURITY.md first — set an auth-token and
keep the port on localhost (or a tunnel). Also see CONTRIBUTING.md and the
CHANGELOG.
- world.get/setBlock
- world.getBlockWithData
- world.setBlocks
- world.getPlayerIds
- world.getBlocks
- chat.post
- events.clear
- events.block.hits
- player.getTile
- player.setTile
- player.getPos
- player.setPos
- world.getHeight
- entity.getTile
- entity.setTile
- entity.getPos
- entity.setPos
- Camera angles
Beyond the core mcpi calls above, these are registered too (the definitive list is
buildCommandRegistry() in RemoteSession.java). Note the namespaced names
(world.spawnEntity, events.projectile.hits, …) — the old bare/poll* forms are gone.
Blocks & world
- world.getBlocks(x1,y1,z1,x2,y2,z2) - read a whole cuboid at once (respects
max-blocksandmax-chunks-per-tick) - world.getHeight(x,z) - the highest block y at a column
- world.setSign(x,y,z,blockTypeId,data,line1,line2,line3,line4) - place a sign
- Wall signs (id 68) take facing
data: 2=north, 3=south, 4=west, 5=east - Standing signs (id 63) take rotation
data0-15: 0=south, 4=west, 8=north, 12=east
- Wall signs (id 68) take facing
Players & entities (mutation is owner-scoped — see Entity/mob control)
- world.getPlayerId(name) - the entity id of an online player by name
- entity.getName(id) - the name for an entity id (reverse of getPlayerId)
- world.getEntities([typeId]) - loaded entities, optionally filtered by type id
- world.getEntityTypes() - the entity types this server supports
- world.spawnEntity(x,y,z,typeId) - spawn an entity and return its id (your session owns it). Respects
max-entities-per-session; over the cap the command returnsFailand does not spawn. - world.removeEntity(id) / world.removeEntities([typeId]) - remove entities you spawned
- player.getEntities(dist,typeId) / player.removeEntities(dist,typeId) - near the bound player
- entity.getEntities(id,dist,typeId) / entity.removeEntities(id,dist,typeId) - near an entity
- player.getAbsPos() / player.setAbsPos(x,y,z) - absolute position, ignoring the
locationconfig - {player,entity}.getDirection/setDirection, getRotation/setRotation, getPitch/setPitch - read/aim facing
Session identity
- setPlayer(name[,token]) - bind this connection to an online player (scopes
player.*and the event streams; Python:mc.set_player("Alice")). Withplayer-tokensconfigured the bind is fail-closed and needs that player's token — see SECURITY.md.
Text with commas and pipes
- protocol.escape(1|0) - turn escaping of free text on or off for this connection. Replies
1; an older server repliesFail. Once on,\,\|and\\stand for,|and\inside a value, both in arguments (chat, sign lines, names) and in replies (chat events, projectile names,entity.getName). Both Python clients turn it on automatically. Without it, the classic protocol is unchanged.
Teacher commands (off unless classroom-teacher-token is set)
- classroom.freeze(name,token) / classroom.unfreeze(name,token) / classroom.reset(name,token) -
socket forms of
/rj freeze,/rj unfreezeand/rj reset. Reply1when the work ran, elseFail. Three wrong tokens close the connection.
Event polls (each drains the events queued since the last poll)
- events.block.hits / events.chat.posts / events.projectile.hits - global (subject to
allow-global-events) - player.events.block.hits / .chat.posts / .projectile.hits - scoped to the bound player
- entity.events.block.hits / .chat.posts / .projectile.hits - scoped to an entity
- events.clear / player.events.clear / entity.events.clear - clear queued events
Drive spawned entities (use world.spawnEntity to create one and get its id). Only the
connection that spawned an entity may control it (per-session ownership), so one client
can't move or kill another client's mobs - reads are open, mutation is owner-only:
- entity.moveTo(id,x,y,z) - walk a mob to a point using real pathfinding (mobs only)
- entity.lookAt(id,x,y,z) - turn the entity to face a point
- entity.getHealth(id) - the entity's current health (or
Failif not a living entity) - entity.setHealth(id,health) - set health, clamped to [0, max]
- entity.setName(id,name) - set a visible name tag. Everything after the id is the name, commas included.
- entity.setAI(id,0|1) - disable/enable the mob's AI (freeze or free it)
- world.setTime(ticks) / world.getTime() - set/get the time of day (0-24000; ticks forward)
- world.setWeather(0|1|2) - 0 clear, 1 rain, 2 thunder
- world.clone(x1,y1,z1,x2,y2,z2,dx,dy,dz) - copy a cuboid to a destination corner (respects max-blocks)
- player.setGameMode(0|1|2|3) - survival / creative / adventure / spectator
- player.give(blockId[,count]) - give the current player blocks (default 1)
Poll these (like the other events.* calls) to react to what players do - "when X, do Y":
- events.player.moves - positions the player moved into since the last poll (
x,y,z,name) - events.block.breaks - blocks players broke (
x,y,z,blockId,name) - events.block.places - blocks players placed (
x,y,z,blockId,name) - events.player.deaths - player deaths (
x,y,z,name)
A per-session, code-driven agent (a "turtle"): drive it with relative commands to move,
turn, and place blocks - a hands-on way to teach sequencing, loops, and functions. Movement
is grid-aligned teleport-step. Commands before agent.spawn answer Fail. While a classroom
sandbox is on (plots non-empty), those fire-and-forget moves stay silent instead, so a Fail
cannot desync the next query. agent.getPos still answers Fail until the agent exists.
- agent.spawn() / agent.spawn(x,y,z) - create the agent at the player (or a given block); one per session
- agent.despawn() - remove the agent
- agent.getPos() - the agent's block position (relative coords)
- agent.getRotation() - facing as a cardinal yaw: 0=south, 90=west, 180=north, 270=east
- agent.forward(n) / agent.back(n) - move n blocks along/against facing (n optional, default 1)
- agent.up(n) / agent.down(n) - move n blocks vertically
- agent.turnLeft() / agent.turnRight() - rotate the facing 90°
- agent.setBlock(id[,data]) - place a block at the agent's position (id 0 clears to air)
Example: a loop that draws a 4×4 square outline is just
for _ in range(4): [conn.send(f"agent.setBlock(1)\nagent.forward()\n") ...]; conn.send("agent.turnRight()\n").
Note - extra features are NOT guaranteed to be maintained in future releases, particularly if updates are made to the original Pi API which replace the functionality
Modify config.yml:
- hostname: - address to bind to. Defaults to
localhost(loopback only) because the API socket is unauthenticated. Set to0.0.0.0to accept connections from any host on the network - only do this on a trusted/firewalled network (a security warning is logged when you do). - port: 4711 - the default tcp port can be changed in config.yml
- location: RELATIVE - determine whether locations are RELATIVE to the spawn point (default like pi) or ABSOLUTE
- hitclick: RIGHT - determine whether hit events are triggered by LEFT clicks, RIGHT clicks or BOTH
- max-blocks: 1000000 - maximum blocks a single getBlocks/setBlocks may span; oversized requests are rejected. 0 disables the cap.
- max-blocks-per-tick: 10000000 - cumulative blocks all cuboid ops (getBlocks/setBlocks/clone) may touch in one server tick; bounds a flood of near-cap requests that
max-blocksalone can't. 0 disables the per-tick budget (not recommended). - max-chunks-per-tick: 256 - distinct chunk columns one session may touch in a single tick (getBlock/setBlock/getBlocks/getHeight/spawnEntity/teleport/…). Stops a scatter of far coordinates from force-generating thousands of chunks on the main thread. Already-touched chunks in the same tick are free. This rates generation; it does not cap eventual world size. 0 disables the cap.
- max-sessions: 100 - maximum concurrent socket sessions; connections beyond this are refused. Raise it for a large classroom; 0 disables the cap.
- max-entities-per-session: 1000 - maximum entities one socket session may spawn via
world.spawnEntityover the life of the connection (a lifetime count:removeEntitydoes not free a slot). Over the cap the command returnsFail. Raise it for a lesson that spawns many mobs; lower it on a crowded classroom server. 0 disables the cap. - max-connections-per-minute: 60 - maximum new connections one remote IP may open per minute. Bounds connection-flood churn and backs the auth/
setPlayerbrute-force lockouts (an attacker can't just reconnect for more guesses). 0 disables per-IP rate limiting. - welcome-message: true - broadcast a "Welcome <player>" message on join. Set false to stay silent.
- enable-op-commands: true - allow the power commands
player.setGameMode/player.give. Set false on a shared/survival server so a socket client can't self-grant creative mode or items. - allow-global-events: false - by default the reactive event streams (
events.player.moves/block.breaks/block.places/player.deaths) report only the session's OWN player's activity. A connection picks its player withsetPlayer(<name>)(mc.set_player("Alice")); on a multi-player server an unbound connection receives no events at all (fail closed), so no client is handed an arbitrary player's feed. Set true to instead broadcast every player's events to every socket (a live tracking feed) - only on a trusted single-user server where you want whole-world/region triggers. See SECURITY.md. - auth-token: '' - optional shared secret. When set, clients must send
auth(<token>)before any other command (Minecraft.connect(host, port, token="…")in the Python client). Empty = no auth. The socket is unencrypted, so tunnel the port for confidentiality - see SECURITY.md. - player-tokens: - optional per-player bind secrets (a
name: secretmap). Empty (default) =setPlayer(<name>)binds by name, unchanged. Non-empty = fail closed:setPlayer(<name>,<token>)(Python:mc.set_player("Alice", token="…")) succeeds only for a listed player whose token matches, so on a multi-user server a client can bind to (and observe) only the player it holds a token for. Repeated wrong tokens close the connection. See SECURITY.md. - plots: {} - per-student build plots, six inclusive integers in the same space as
location(minX,minY,minZ,maxX,maxY,maxZ), keyed by thesetPlayername. Empty (default) = no spatial sandbox. A non-empty map is fail-closed: the socket may read and write only inside that player's plot, and an unbound session no longer falls through to the host player. Cross-plotgetBlocks/spawnEntityreturnFail;setBlock,setBlocks,clone, and teleports stay silent. By hand, breaking, placing, or bucketing a block that sits in another student's plot is cancelled; gaps and a shared overlap stay allowed. Flint and steel and spawn eggs are not stopped. Overlapping plots are shared. This is notmax-chunks-per-tick(that only rates chunk generation). Setenable-op-commands: falsewhen you turn plots on. See SECURITY.md. - sandbox-lock-world-rules: true - while
plotsis on,world.setTimeandworld.setWeatherare rejected (they change the world for every student). Ignored whenplotsis empty. Set false for a lesson about the clock or the weather. - sandbox-max-blocks: 0 - tighter single-cuboid cap while
plotsis on (minwithmax-blockswhen both are positive). 0 = no extra cap. Ignored whenplotsis empty. - sandbox-max-blocks-per-tick: 0 - tighter per-tick cuboid budget while
plotsis on. 0 = no extra cap. A class of 30 should set this well under the 10000000 default (for example 20000). - sandbox-max-commands-per-tick: 0 - commands drained per tick per session while
plotsis on. 0 keeps the built-in 9000. Read when the socket connects. 500 logsOver 500 commandswhen a tick defers the rest. - /rj freeze <player> and /rj unfreeze <player> - teacher command (
raspberryjuice.classroom.teacher, default op; console is allowed). Freezes that online player's socket: reads, event polls, and agent turns still run, and every other command keeps its usual reply without changing the world. The player can still walk and build by hand. Freeze survives a reconnect and clears on restart. An offline or unknown name is refused. See SECURITY.md. - /rj reset <player> - same permission. Sets that configured plot to air, removes non-player entities inside it (including a shared overlap), and discards that student's agent. The student may be offline. One name per call. A plot over 100000 blocks, or over a tighter positive
max-blocks/sandbox-max-blocks, is refused and changes nothing. There is no undo, and the entity cap is not refunded. Back up the world first. See SECURITY.md. - classroom-teacher-token: - shared secret for the socket teacher commands (
classroom.freeze/unfreeze/reset). Empty (default) = they answerFail. The socket is not encrypted, so most classrooms should leave it empty and use/rj. No commas or pipes in the token. See SECURITY.md.
Two Python clients ship with this repo:
client/— a new, typedraspberryjuicepackage with a clean modern API (see Quickstart). Best for writing new lessons.mcpi-client/— a modern, drop-inmcpipackage so existing Minecraft-Pi curriculum (Adventures in Minecraft, Raspberry Pi tutorials) runs unchanged:from mcpi.minecraft import Minecraft. Best for running material that's already written.
For reference, the original classic mcpi Java/Python libraries (as modded for RaspberryJuice) are
also bundled at src/main/resources/mcpi.
Requires JDK 25 (Paper 26.2 is compiled to Java 25 bytecode). A Maven Wrapper is included, so you don't need a system Maven install.
git clone https://github.com/sakebomb/RaspberryJuice
cd RaspberryJuice
./mvnw package # use mvnw.cmd on Windows
The plugin jar is produced at target/raspberryjuice-*.jar; drop it in your Paper server's
plugins/ directory. Run the test suite with ./mvnw test. There's also an end-to-end
scripts/smoke_test.py you can run against a live server.
- 2.1.1 - first release on Modrinth, Hangar and PyPI; opt-in
protocol.escapekeeps commas and pipes in chat, signs and names (entity.setNamekeeps commas for every client); typed event records in the Python client; classroom sandbox state logged at startup. See CHANGELOG. - 2.1.0 - programmable education platform: turtle agent, entity/mob control, world/player control, reactive events, and a typed Python client (
client/); security hardening (per-session entity ownership incl. bulk removal,auth-tokenhandshake, per-playersetPlayertokens + brute-force lockout,enable-op-commands, per-tick block budget, bounded socket I/O). See CHANGELOG. - 2.0.0 - modernized fork: runs on Paper 26.2 / Java 25 (down through Paper 1.21); block layer ported off pre-1.13 numeric-ID APIs to Material/BlockData via a legacy-ID bridge (protocol unchanged); Adventure chat/sign APIs; security & concurrency hardening; JUnit 5 + MockBukkit test suite and CI
- 1.12.1 - hostname specified in config.yml
- 1.12 - getEntities, removeEntities, pollProjectileHits, events calls by player and entity
- 1.11 - spawnEntity, setDirection, setRotation, setPitch
- 1.10.1 - bug fixes
- 1.10 - left, right, both hit clicks added to config.yml & fixed minor hit events bug
- 1.9.1 - minor change to improve connection reset
- 1.9 - relative and absolute positions added to config.yml
- 1.8 - minecraft version 1.9.2 compatibility
- 1.7 - added pollChatPosts() & block update performance improvements
- 1.6 - added getPlayerId(playerName), getDirection, getRotation, getPitch
- 1.5 - entity functions
- 1.4.2 - bug fixes
- 1.4 - bug fixes, port specified in config.yml
- 1.3 - getHeight, multiplayer, getBlocks
- 1.2 - added world.getBlockWithData
- 1.1.1 - block hit events
- 1.1 - Initial release