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 (
uvorpip) - 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
pipequivalent 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-pluginOpen in VS Code
Open the folder and point VS Code at the environment, so imports resolve and you get autocompletion:
- File → Open Folder and select the project.
- Open the Command Palette (
Ctrl+Shift+P/Cmd+Shift+P) and run Python: Select Interpreter. - 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.exeon Windows,.venv/bin/pythonon 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/:
Two pieces make it a plugin Endstone can load:
- The plugin class in
plugin.pyextendsPluginand setsapi_versionto themajor.minorof the Endstone API you target:
from endstone.plugin import Plugin
class ExamplePlugin(Plugin):
api_version = "0.11"- The entry point in
pyproject.tomltells the server which class to load. The entry-point name (your project name without theendstone-prefix) becomes the plugin's name; the value ispackage:Class:
[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:
[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 ExamplePlugin→class MyPlugininplugin.py
The package should now look like this:
3. __init__.py - update the import and export to match:
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-pluginpairs with the entry pointmy-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 underplugins/and the prefix for its permissions.
Distribution name | Entry point | Loads? | |
|---|---|---|---|
| ✅ | endstone-my-plugin | my-plugin = "endstone_my_plugin:MyPlugin" | Yes |
| ❌ | endstone-my-plugin | example = "endstone_my_plugin:MyPlugin" | No - entry-point name must be my-plugin |
| ❌ | my-plugin | my-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 endstoneendstoneThe 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:
[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:
[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.