Project setup

Set up a Python plugin project with uv (or pip) and VS Code.

This guide gets a Python plugin building and running. We start from the official example plugin - a small, working plugin you can rename and grow into your own.

Prerequisites

  • Endstone installed with the virtual-environment method (uv or pip) - plugins run in that same environment. See the installation guide if you haven't.
  • Python 3.10 or newer
  • VS Code with the Python extension
  • uv (recommended) - a fast Python package and project manager. Every step also shows the pip equivalent if you'd rather not install uv.

Get the project

Clone the example plugin - or open its GitHub page and choose Use this template to start your own repository from it:

git clone https://github.com/EndstoneMC/python-example-plugin.git my-plugin
cd my-plugin

Open in VS Code

Open the folder and point VS Code at the environment, so imports resolve and you get autocompletion:

  1. File → Open Folder and select the project.
  2. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run Python: Select Interpreter.
  3. Choose the virtual environment where you installed Endstone - the one from the installation guide. VS Code lists the environments it detects; if yours isn't shown, pick Enter interpreter path and point it at that environment's Python (.venv\Scripts\python.exe on Windows, .venv/bin/python on macOS and Linux).

Your plugin and Endstone have to live in the same virtual environment: that's what lets the server load your plugin, and what gives VS Code the endstone types for autocompletion.

Project anatomy

A plugin is an ordinary Python package under src/:

pyproject.toml
__init__.py
plugin.py
config.toml

Two pieces make it a plugin Endstone can load:

  • The plugin class in plugin.py extends Plugin and sets api_version to the major.minor of the Endstone API you target:
src/endstone_example/plugin.py
from endstone.plugin import Plugin

class ExamplePlugin(Plugin):
    api_version = "0.11"
  • The entry point in pyproject.toml tells the server which class to load. The entry-point name (your project name without the endstone- prefix) becomes the plugin's name; the value is package:Class:
pyproject.toml
[project]
name = "endstone-example"

[project.entry-points."endstone"]
example = "endstone_example:ExamplePlugin"

Rename it

The example is published as endstone-example. To start your own plugin, rename it - say to endstone-my-plugin. Several names are linked, and the loader rejects mismatches, so change them together.

1. pyproject.toml - the distribution name, the entry point, and the version-file path:

pyproject.toml
[project]
name = "endstone-my-plugin"

[project.entry-points."endstone"]
my-plugin = "endstone_my_plugin:MyPlugin"

[tool.hatch.build.hooks.vcs]
version-file = "src/endstone_my_plugin/_version.py"

2. The package - rename the folder and the class:

  • src/endstone_example/src/endstone_my_plugin/
  • class ExamplePluginclass MyPlugin in plugin.py

The package should now look like this:

__init__.py
plugin.py
config.toml

3. __init__.py - update the import and export to match:

src/endstone_my_plugin/__init__.py
from .plugin import MyPlugin

__all__ = ["MyPlugin"]

Then reinstall so the renamed entry point is re-registered:

uv pip install -e .
pip install -e .

Why they have to line up

  • The distribution name must start with endstone- - the loader enforces it.
  • The entry-point name must equal the distribution name without that prefix: endstone-my-plugin pairs with the entry point my-plugin. If the two don't match, the plugin won't load - and the error tells you exactly which one to change.
  • The entry-point value is package:Class, so it has to point at the renamed package (endstone_my_plugin) and class (MyPlugin).
  • The entry-point name, with dashes turned into underscores, becomes the plugin's runtime name - my_plugin - which is also its data folder under plugins/ and the prefix for its permissions.
Distribution nameEntry pointLoads?
endstone-my-pluginmy-plugin = "endstone_my_plugin:MyPlugin"Yes
endstone-my-pluginexample = "endstone_my_plugin:MyPlugin"No - entry-point name must be my-plugin
my-pluginmy-plugin = "endstone_my_plugin:MyPlugin"No - distribution name must start with endstone-

Package folders use underscores by Python convention, while distribution and entry-point names use dashes - that's why it's endstone_my_plugin in src/ but endstone-my-plugin in pyproject.toml.

Run it on a server

Because the environment now has both endstone and your plugin installed, you can launch a server straight from it - and since the plugin is installed editable, the server loads it from your source:

uv run endstone
endstone

The first launch downloads the Bedrock Dedicated Server into a bedrock_server/ folder and starts it with your plugin loaded. After editing your plugin's code, run /reload in the server console to apply the changes - no rebuild, no restart.

The server runs plugins from the same Python environment as endstone. Installing your plugin editable into that environment (the step above) is what lets /reload pick up your source changes directly.

Third-party libraries

Your plugin can depend on any package from PyPI. Declare runtime dependencies in the [project] table of pyproject.toml, exactly as you would for any Python project - the same name and version-specifier syntax pip understands:

pyproject.toml
[project]
name = "endstone-my-plugin"
# ...
dependencies = [
    "aiohttp>=3.9",      # an async HTTP client
    "pydantic>=2,<3",    # validation, pinned to the v2 line
    "rich",              # any version
]

When your plugin loads, Endstone installs the wheel into the server's private plugins/.local prefix and pip resolves these dependencies from PyPI at the same time - whether the plugin was pip installed or just dropped into plugins/ as a .whl. You don't ship or vendor them yourself.

Because dependencies are fetched on load, the server needs network access the first time a plugin (or a new version) starts. They're cached in plugins/.local, so later starts are offline-friendly. Pin meaningful lower bounds (>=) so an old, incompatible release never gets resolved.

Development-only dependencies

Tools you need while building the plugin but that shouldn't be installed on a user's server - the endstone package itself, a linter, test runners - belong in an optional group, not in dependencies:

pyproject.toml
[project.optional-dependencies]
dev = ["endstone", "ruff", "pytest"]

Install them locally with uv pip install -e ".[dev]" (or pip install -e ".[dev]"); they stay out of the shipped wheel's runtime requirements, so they're never pulled onto a server.

When you're ready to share the plugin, see Publishing to PyPI.

On this page