A small client-side NeoForge 1.21.1 mod that adds Pixelmon tools to minecraft-mcp, the mod that exposes a running Minecraft client to AI agents over MCP. With both installed, an agent can set up a party, start NPC, wild or horde battles, and compare what the battle UI shows with what the server knows, without scripting menus or NPC dialogue. For videos, it can quiet a world, stage Pokémon and NPCs, and find things to film.
The tools register through minecraft-mcp's tool provider SPI. Setup tools act on the integrated server, so they work in singleplayer only.
- Minecraft 1.21.1, NeoForge 21.1.200+
- Pixelmon 9.4.x (built against 9.4.1)
- minecraft-mcp 0.1.0-envy.3+ (
com.envyware.minecraftmcp:minecraft_mcp-neoforgeonhttps://maven.envyware.co.uk/releases)
The mod is client-only and is meant for dev and test clients, not for players.
Either drop both jars into the game directory's mods/ folder, or add them as dev-only runtime dependencies so they
load in your dev client but never end up in your published jar or POM. With ModDevGradle:
repositories {
maven { url = 'https://maven.envyware.co.uk/releases' }
}
configurations {
runtimeClasspath.extendsFrom localRuntime
}
dependencies {
localRuntime 'com.envyware.minecraftmcp:minecraft_mcp-neoforge:0.1.0-envy.3'
localRuntime 'com.envyware.pixelmonmcp:pixelmon_mcp:1.1.0'
}Run the client in minecraft-mcp's background mode
(-DminecraftMcp.background=true, and earlyWindowControl = false in config/fml.toml) so it never takes over your
screen.
If Pixelmon's internals have drifted from 9.4.1 (for example in a Pixelmon dev workspace), a tool that hits a missing
method or field returns {"status": "unsupported", "tool": ..., "reason": ..., "pixelmonVersion": ...} instead of
failing. pixelmon.mcp.info reports the versions in play.
Agents connect through the minecraft-mcp stdio bridge,
which stays up while the client is launched, stopped and relaunched. For Claude Code, .mcp.json at the project root:
{
"mcpServers": {
"minecraft": {
"command": "java",
"args": ["-jar", "runs/mcp-bridge.jar", "--game-dir", "runs/client",
"--launch-command", "gradlew.bat runClient --no-daemon", "--launch-cwd", "."]
}
}
}Keep --no-daemon in the launch command: client.stop kills the launched process tree, so a Gradle daemon started by
the launch would be killed with it. On Linux and macOS use ./gradlew runClient --no-daemon.
In this repository, ./gradlew prepareClientRun (also run by runClient) copies the bridge to runs/mcp-bridge.jar,
sets earlyWindowControl = false in runs/client/config/fml.toml and writes a 1600x900, GUI scale 2 options.txt if
there is none. The client run starts in background mode with the endpoint on port 25599.
client.launch. It compiles first and takes a few minutes, so batch code changes before relaunching.mc.world.joinwith{"name": "agent", "preset": "flat"}creates or opens a flat creative world with cheats. It returns immediately; pollmc.client.stateuntilinWorldis true (about a minute for a new world). A fresh player sees the starter picker first, which keepsinWorldfalse: see the gotchas.- Set up state with
pixelmon.party.setandpixelmon.battle.start, or chat commands throughmc.command.run. - Drive the UI with
mc.screen.state,mc.screen.widget.click,mc.screen.click.atandmc.keyboard.press. - Check with
pixelmon.*.state,mc.screenshot.take(saved underruns/client/screenshots/) andclient.logs. client.stopwhen done.
mc.window.show makes the hidden window visible without taking focus; mc.window.hide hands it back to the agent.
Pokémon are always given as Pokémon specs, the same format as Pixelmon commands ("pikachu lvl:50 palette:shiny",
"charizard helditem:charizardite_x"). Resource ids default to the pixelmon namespace.
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.mcp.info |
Versions of Pixelmon MCP, the loaded Pixelmon and minecraft-mcp, and the versions Pixelmon MCP was compiled against |
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.party.state |
The party as the client sees it: species, form, level, HP, status, moves with PP, held item, whether each Pokémon is out, and the selectedSlot |
|
pixelmon.party.set |
pokemon (1 to 6 specs, e.g. ["charizard lvl:50", "lapras lvl:50"]), heal (default true) |
Replaces the party. Refused in battle. Also marks the starter as picked and closes the starter picker, which otherwise stalls battles for a fresh player |
pixelmon.party.heal |
Heals the party. Refused in battle | |
pixelmon.party.select |
slot (0-5) |
Selects a party slot, like the up and down keys |
pixelmon.party.send_out |
slot (default the selected slot) |
Throws the Pokémon out, like the send-out key. It lands a moment later: poll pixelmon.party.state for out: true |
pixelmon.party.retrieve |
slot (default every Pokémon that is out) |
Recalls sent-out Pokémon |
pixelmon.party.gimmicks.state |
Which gimmicks are unlocked (mega, dynamax, terastallize), the equipped key item, the items that can be selected, Tera energy and charm states |
|
pixelmon.party.gimmicks.unlock |
mega, dynamax, terastallize (true unlocks, false locks), teraEnergy, infiniteTeraEnergy |
Unlocks or locks gimmicks. The first unlock equips a matching key item |
pixelmon.party.gimmicks.select |
item: BraceletORAS, BoostNecklace, MegaGlasses, MegaAnchor, MegaTiara, DynamaxBand, TeraOrb or None |
Equips a key item. Its gimmick must be unlocked. Mega Stones are held items: give them in the spec |
pixelmon.party.charms.set |
charms: map of charm id to ACTIVE, AVAILABLE or DISABLED, e.g. {"shiny": "ACTIVE"} |
Sets charm states (shiny, oval, mark, catching, exp) |
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.pc.state |
box (default 0) |
One box (page): name, wallpaper and Pokémon with their slots, plus the name and count of every box |
pixelmon.pc.set |
box; pokemon (up to 30 specs, null or "" for an empty slot) replaces the box, or slot with one pokemon spec (null clears it) sets one slot; name renames the box |
Writes to the PC |
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.pokedex.state |
pokemon (specs to look up) or a page with offset (default 0) and limit (default 30, max 200); dex (default national_dex) |
Seen and caught counts and percentages, and entries with status (UNKNOWN, SEEN, CAUGHT) and how often each was seen and caught |
pixelmon.pokedex.set |
pokemon (specs) or all: true; status (seen or caught, default caught); clear: true |
Registers entries. Pixelmon cannot remove single entries: clear wipes the whole Pokédex first |
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.research.state |
available (true also lists every research id) |
Active research with stage and objective progress, and completed research ids |
pixelmon.research.set |
action (start, discard, complete, complete_stage, claim or clear); id (e.g. pixelmon:battle/gym/fire_1) or uuid of an active research |
Changes research. claim runs the current stage's rewards |
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.battle.start |
One of npc (NPC preset id, e.g. pixelmon:battle/leader/gym/dragon_1), wild (spec) or horde (list of specs). rules (rule set id, default AG), ai (default pixelmon:advanced), canMega, canDynamax, canTerastallize (NPC only, default true), seed (wild only), distance (blocks, default 3) |
Spawns the opponents in front of the player and requests the battle. Horde battles ignore rules and distance |
pixelmon.battle.state |
The client battle: mode, turn, weather, pending messages and tasks, and the Pokémon on display with the HP the UI shows | |
pixelmon.battle.server_state |
The server battle: turn and each participant's active Pokémon with real HP | |
pixelmon.battle.end |
Ends the battle and removes the opponents pixelmon.battle.start spawned |
|
pixelmon.battle.options |
refreshBag |
What the player can do now: canAct (or blockedBy), the choosing Pokémon and its moves, available gimmicks, canSwitch, canFlee, canCatch, the team and enemies with indices, bag items and pending prompts |
pixelmon.battle.move |
move (slot 0-3, name or struggle); target (enemy index) or allyTarget (our-side index); gimmick (mega, ultra_burst, dynamax, terastallize or zmove) |
Uses a move. Targets only matter for moves that need one in multi battles |
pixelmon.battle.switch |
pokemon (team index, UUID or species) |
Switches. Also answers forced switches after a faint, and Revival Blessing (pass the fainted Pokémon) |
pixelmon.battle.item |
item (e.g. pixelmon:potion); pokemon (target, default the choosing Pokémon); move (slot, for Ethers) |
Uses a bag item |
pixelmon.battle.catch |
ball (default pixelmon:poke_ball) |
Throws a ball at the last standing wild opponent |
pixelmon.battle.run |
Runs from a wild battle, or forfeits a trainer battle | |
pixelmon.battle.learn_move |
replace (slot 0-3 to forget, or -1 to skip) |
Answers the next "learn a new move" prompt |
pixelmon.battle.dismiss_level_ups |
Dismisses level-up screens, which hold up the next turn |
Pixelmon 9.4.1 has no Terastallization option for NPC battles: canTerastallize on pixelmon.battle.start is accepted
but ignored, and the result says so. The terastallize gimmick on pixelmon.battle.move works.
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.film.prepare |
stopSpawning (default true); clear (pokemon, npcs, wormholes or all, one or a list, default none); radius (blocks around the player for clear, default the whole level); party (1 to 6 specs); heal (default true) |
Gets the world ready for filming at runtime, without editing configs or restarting. Sets the pixelmonSpawning gamerule to false, vetoes every spawn by Pixelmon's spawners (wild Pokémon, legendaries, wormholes and the NPCs that spawn around villages), and turns off legendary spawning and the legendary broadcast. Fishing, Headbutt, Rock Smash, Sweet Scent, curry and Pixelmon grass still work. clear removes wild Pokémon (not owned, not in battle), NPCs and wormholes. party works as in pixelmon.party.set. Reports what it changed and removed, and anything it could not change as unsupported |
pixelmon.film.restore |
Puts back what pixelmon.film.prepare changed: the gamerule, the spawner veto, the legendary settings and the party. Removed entities stay removed |
Positions are [x, y, z]. With origin ("spawn", "player" or [x, y, z]), pos and lookAt are relative to it.
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.stage.pokemon |
pokemon (spec); pos (default distance blocks in front of the player, on the ground); distance (default 4); ground (true moves pos to the surface); yaw (degrees) or lookAt (default the player); origin; frozen (default true); animation (e.g. special) |
Spawns a Pokémon for a shot. Frozen means no AI, no gravity, no despawn and invulnerable, so it stays where it was put. Returns id, uuid, pos, yaw and species |
pixelmon.stage.npc |
npc (NPC preset id, e.g. pixelmon:battle/leader/gym/dragon_1); name; pos, distance, ground, yaw, lookAt, origin, frozen as above |
Spawns an NPC from a preset. Frozen NPCs are also made immovable. Returns id, uuid, pos, yaw, npc and name |
pixelmon.stage.clear |
uuid (one or a list, default every staged entity) |
Removes staged Pokémon and NPCs |
Staged entities are kept by pixelmon.film.prepare clears.
mc.command.run returns no command output, so these return positions as JSON. They take pos (default the player)
and origin as the staging tools do; with an origin, results are relative to it too.
| Tool | Arguments | What it does |
|---|---|---|
pixelmon.locate.pokemon |
pos, origin; radius (default 64); limit (default 10); species (one or a list); wild (true only wild, false only owned) |
Loaded Pokémon, nearest first: id, uuid, pos, distance, species, level, and form, shiny, owner, inBattle and staged when they apply. total counts every match |
pixelmon.locate.npcs |
pos, origin; radius (default 64); limit (default 10) |
Loaded NPCs, nearest first: id, uuid, pos, distance, name, and preset and staged when they apply |
pixelmon.locate.structure |
structure (ids, id prefixes such as gyms or den, or #tags, one or a list; the pixelmon namespace is the default); list (true only lists the matching ids); pos, origin; radius (blocks, default 1600); limit (default 5) |
The nearest structure of each matching id within radius, nearest first: structure, x, z and distance. Uses the same search as /locate, so it also finds structures in chunks not generated yet. Without structure it lists Pixelmon's structure ids |
The battle tools send the same packets as the battle screen and update its state the same way, so the UI and the server stay in step. A turn looks like this:
- Poll
pixelmon.battle.optionsuntilcanActis true. Until then it reportsblockedBy: messages still playing, an animation, or the opponent choosing. - Choose with
pixelmon.battle.move,switch,item,catchorrun. In doubles and triples each Pokémon chooses in turn: the result hassent: falseand thenextPokémon until the last choice sends them all. - After a faint the mode is
ENFORCED_SWITCH: answer withpixelmon.battle.switch. - Compare
pixelmon.battle.statewithpixelmon.battle.server_stateto check what the UI shows.
The bag comes from the player's inventory (give items with mc.command.run, e.g. /give @s pixelmon:potion 5). If
pixelmon.battle.options shows an empty bag, pass refreshBag: true and read it again.
To drive the real UI instead, click with mc.screen.click.at in GUI coordinates, which are screenshot pixels divided by
the GUI scale. At 1600x900 and GUI scale 2, the four move buttons of a single battle are centred around (486, 386),
(640, 386), (486, 424) and (640, 424); they are already shown in MAIN_MENU. turn counts from 0, while the UI shows
"Turn 1" for the first turn.
The film tools fit minecraft-mcp's film tools
(mc.film.run, mc.camera.play, mc.hud.set, in minecraft-mcp 0.1.0-envy.3 and later). A scene's setup.steps can
call them:
"setup": {
"world": {"name": "film_pixelmon", "seed": 424242},
"steps": [
{"tool": "pixelmon.film.prepare", "args": {"party": ["pikachu lvl:50"], "clear": "all", "radius": 160}},
{"tool": "pixelmon.stage.pokemon",
"args": {"pokemon": "charizard palette:shiny", "origin": "spawn", "pos": [4, 0, 6], "ground": true, "lookAt": [0, 2, 0]}},
{"tool": "pixelmon.stage.npc",
"args": {"npc": "pixelmon:battle/leader/gym/dragon_1", "origin": "spawn", "pos": [-3, 0, 6], "ground": true, "lookAt": [4, 0, 6]}}
]
}"origin": "spawn"on the staging tools matches a scene's"origin": "spawn", so staged entities and camera paths share coordinates.- Use a staged entity's
uuidas a shot'starget({"uuid": "..."}) to orbit or follow it. Find other subjects withpixelmon.locate.pokemon,pixelmon.locate.npcsandpixelmon.locate.structure("origin": "spawn"gives scene coordinates). mc.world.joincreates worlds without structures unlessstructures: true;pixelmon.locate.structurewarns when the world has none.- Hide the HUD with
mc.hud.set; shots and recordings hide it by default. Pixelmon's party sidebar and other overlays are hidden with it. - Call
pixelmon.film.restorewhen done. ThepixelmonSpawninggamerule is saved with the world, so a world left while prepared keeps Pixelmon spawning off until a restore or/gamerule pixelmonSpawning true.
- A fresh player gets Pixelmon's starter picker. While it is open,
mc.client.statereportsinWorld: falseandrawInWorld: true; poll for either, then callpixelmon.party.set, which closes the picker. pixelmon.battle.startreturns as soon as the battle is requested; NPC battles start about half a second later. Pollpixelmon.battle.stateuntilbattlingis true.- After
pixelmon.battle.end, heal only oncepixelmon.battle.statereports the battle is over. The end-of-battle write-back runs later and would undo an earlier heal. - Comparing
pixelmon.battle.statewithpixelmon.battle.server_stateshows client/server desyncs, such as HP that never updates. - Rule sets that cap levels (arena presets cap the player at 25) apply to
pixelmon.battle.startas they do in game. - The world is creative and peaceful. Use
mc.command.runfor anything else (/gamemode survival,/time set day). pixelmon.research.setwithclaimruns the rewards, which can open a professor dialogue screen. A battle does not start while another screen is open: checkmc.client.stateand close it withmc.keyboard.press{"key": "escape"}.- A sent-out Pokémon's ball flies where the player looks and can hit something in the way, such as another sent-out
Pokémon. Check
outinpixelmon.party.staterather than assuming it landed. - Pixelmon's client picks a Pokémon's animation from how it moves, so
animationonpixelmon.stage.pokemononly holds forspecialon a Pokémon standing on the ground. A frozen Pokémon placed in the air playsfly. - Only stop clients you launched. A developer may be running their own Minecraft clients next to the agent's.
./gradlew build # build/libs/pixelmon_mcp-<version>.jar
./gradlew publishToMavenLocal # com.envyware.pixelmonmcp:pixelmon_mcp:<version>
./gradlew publish # EnvyWare Maven, needs envywareUsername and envywarePasswordThe published POM has no dependencies: Pixelmon and minecraft-mcp are provided by the game.
MIT