Developing Plugins
This guide will walk you through creating your own plugins to extend and customize the Bedrock Server Manager. The plugin system is designed to be simple yet powerful, allowing you to hook into various application events and use the core application’s functions safely.
This guide assumes you have a basic understanding of Python programming.
For a complete list of all available event hooks, see the Plugin Base. For a complete list of all available APIs, see the Available APIs.
1. Getting Started: Your First Plugin
Locate a
plugins
directory:User Plugins: Find the application’s data directory (typically
~/.bedrock-server-manager/
or whereBSM_DATA_DIR
points). Inside, there will be aplugins
folder. This is for your custom plugins.Default Plugins: The application also ships with default plugins located within its installation source at
src/bedrock_server_manager/plugins/default/
. While you can look here for examples, you should place your custom plugins in the user plugins directory.Root
plugins/
folder (for development/examples): The main repository also contains aplugins/
folder in its root. This is primarily for development-time examples and testing of the plugin system itself. For user-created plugins meant for regular use, the user plugins directory is preferred.
Choose your plugin structure: Plugins can be single Python files or complete Python packages (directories). This will be detailed in the next section.
Write the code: Create your plugin file(s) and define a class that inherits from
PluginBase
.
Here is the most basic “Hello World” plugin:
# my_first_plugin.py
from bedrock_server_manager import PluginBase
class MyFirstPlugin(PluginBase):
"""
This is an example description that will be saved in plugins.json
"""
version = "1.0.0" # Mandatory version attribute
def on_load(self):
"""This event is called when the plugin is loaded by the manager."""
self.logger.info("Hello from MyFirstPlugin!")
def after_server_start(self, server_name: str, result: dict):
"""This event is called after a server has started."""
if result.get("status") == "success":
self.logger.info(f"Server '{server_name}' has started successfully!")
Run the application: Start the Bedrock Server Manager.
Enable your plugin: Navigate to the Web UI to activate it. You should see your “Hello from MyFirstPlugin!” message in the logs on the next startup or plugins reload.
2. Plugin Structures: Single File vs. Package
Bedrock Server Manager supports two primary ways to structure your plugin:
2.1. Single-File Plugin
This is the simplest structure, suitable for smaller plugins.
Create a Python file (e.g.,
my_simple_plugin.py
) directly in one of the plugin search paths (e.g., your userplugins
directory).The filename (without the
.py
extension, somy_simple_plugin
in this case) becomes the internal name of your plugin.Your
PluginBase
subclass must be defined within this file.
This is the structure used in the “Hello World” example above.
2.2. Package-Based Plugin (Directory)
For more complex plugins that might include multiple Python modules, templates, static files, or other resources, structuring your plugin as a Python package (a directory) is recommended.
Create a directory in one of the plugin search paths (e.g.,
plugins/my_packaged_plugin/
).The name of this directory (
my_packaged_plugin
) becomes the internal name of your plugin.Inside this directory, you must have an
__init__.py
file.The main
PluginBase
subclass for your plugin should be defined (or imported and made available) in this__init__.py
file.
Example Directory Structure for a Packaged Plugin:
plugins/
└── my_packaged_plugin/ # Plugin Name: my_packaged_plugin
├── __init__.py # Main plugin file, contains MyPluginClass(PluginBase)
├── internal_logic.py # Optional: other Python modules for your plugin
├── templates/ # Optional: For Jinja2 HTML templates
│ └── my_page.html
└── static/ # Optional: For CSS, JS, images
└── css/
└── style.css
This package structure allows for better organization and enables features like serving custom HTML templates and static files, as detailed later. Python’s standard import mechanisms (e.g., from . import internal_logic
) will work within your plugin package.
3. The PluginBase
Class
Every plugin must inherit from bedrock_server_manager.PluginBase
(typically imported as from bedrock_server_manager import PluginBase
). When your plugin is initialized, you are provided with three essential attributes:
self.name
(str): The name of your plugin, derived from its filename.self.logger
(logging.Logger): A pre-configured Python logger. Always use this for logging.self.api
(PluginAPI): Your gateway to interacting with the main application.
Important
Important Plugin Class Requirements:
version
Attribute (Mandatory): Your plugin class must define a class-level attribute namedversion
as a string (e.g.,version = "1.0.0"
). Plugins without a validversion
attribute will not be loaded.Description (from Docstring): The description for your plugin is automatically extracted from the main docstring of your plugin class.
3. Understanding Event Hooks
Event hooks are methods from PluginBase
that you can override. The Plugin Manager calls these methods when the corresponding event occurs.
before_*
events: Called before an action is attempted.after_*
events: Called after an action has been attempted. They are always passed aresult
dictionary that you can inspect to see if the action succeeded or failed.
4. Custom Plugin Events (Inter-Plugin Communication)
Plugins can define, send, and listen to their own custom events for complex interactions.
Sending Events: Use
self.api.send_event("myplugin:custom_action", arg1, kwarg1="value")
.Listening for Events: Use
self.api.listen_for_event("some:event", self.my_callback)
in your plugin’son_load
method.Callback Arguments: Your callback function will receive any
*args
and**kwargs
from the sender.
Example: “I’m Home” Automation (Triggered via HTTP API)
An external system can trigger a plugin to start a server by sending a POST
request to /api/plugins/trigger_event
with a JSON body. The corresponding plugin would listen for this event:
# home_automation_starter_plugin.py
from bedrock_server_manager import PluginBase
TARGET_SERVER_NAME = "main_survival"
class HomeAutomationStarterPlugin(PluginBase):
version = "1.0.0"
def on_load(self):
self.logger.info(f"Listening for 'automation:user_arrived_home' to start '{TARGET_SERVER_NAME}'.")
self.api.listen_for_event("automation:user_arrived_home", self.handle_user_arrival)
def handle_user_arrival(self, **kwargs):
user_id = kwargs.get('user_id', 'UnknownUser')
self.logger.info(f"Received arrival event for user '{user_id}'.")
status = self.api.get_server_running_status(server_name=TARGET_SERVER_NAME)
if status.get("running"):
self.logger.info(f"Server '{TARGET_SERVER_NAME}' is already running.")
return
self.api.start_server(server_name=TARGET_SERVER_NAME, mode="detached")
5. Extending Functionality: Custom FastAPI Endpoints
Plugins can significantly extend Bedrock Server Manager by adding their own custom FastAPI web endpoints. This allows for deep integration and tailored functionality.
To enable this, your plugin class (derived from PluginBase
) needs to override one or both of the following methods:
get_fastapi_routers(self) -> List[fastapi.APIRouter]
: This method should return a list of FastAPIAPIRouter
instances that your plugin wants to add to the main web application.
The Plugin Manager will call these methods on your plugin instance after it’s loaded. The collected commands and routers are then integrated into the main application.
5.1. Adding Custom FastAPI Endpoints (Web APIs and Pages)
To add web endpoints, define your FastAPI APIRouter
instances and return them in a list from get_fastapi_routers()
. These routers will be included in the main FastAPI application.
Example:
# my_web_api_plugin.py
from bedrock_server_manager import PluginBase
from fastapi import APIRouter, Depends
from fastapi.responses import HTMLResponse
# Attempt to import authentication dependency; provide a fallback for isolated testing/robustness
# There are three access roles admin, moderator, and user.
# - get_current_user: User, read only access APIs
# - get_moderator_user: Moderator, basic server management APIs, not including installs, updates, or content management
# - get_admin_user: Admin, full access to all APIs
try:
from bedrock_server_manager.web import get_current_user
HAS_AUTH_DEP = True
except ImportError:
HAS_AUTH_DEP = False
async def get_current_active_user(): return {"username": "anonymous_plugin_user"} # Dummy
# Create an APIRouter instance
plugin_web_router = APIRouter(
prefix="/my_web_plugin", # URL prefix for all routes in this router
tags=["My Web Plugin"], # Tag for OpenAPI documentation (e.g., /docs)
dependencies=[Depends(get_current_user)] if HAS_AUTH_DEP else [] # Secure all routes
)
@plugin_web_router.get("/info")
async def get_plugin_web_info():
"""Returns some information via the plugin's web API."""
return {"plugin_name": "My Web API Plugin", "message": "API is active!"}
@plugin_web_router.post("/submit_data")
async def submit_data_to_plugin(data: dict):
"""A sample POST endpoint for the plugin."""
# In a real plugin, you might use self.api here if you had access to it from the router
# or if the router was created within the plugin instance method that has `self`.
# This example keeps the router definition self-contained for clarity.
return {"status": "success", "received_data": data, "plugin_response": "Data processed by My Web API Plugin."}
@plugin_web_router.get("/custom-html-page", response_class=HTMLResponse)
async def get_plugin_custom_html():
"""Serves a custom HTML page from the plugin."""
html_content = """
<html>
<head><title>My Plugin Page</title></head>
<body><h1>Hello from My Web Plugin's Custom HTML Page!</h1></body>
</html>
"""
return HTMLResponse(content=html_content)
class MyWebAPIPlugin(PluginBase):
version = "1.2.0" # Mandatory
def on_load(self):
self.logger.info(f"{self.name} v{self.version} loaded.")
if not HAS_AUTH_DEP:
self.logger.warning("Auth dependency 'get_current_active_user' not found. Plugin API endpoints might be unsecured.")
def get_fastapi_routers(self):
self.logger.info(f"Providing FastAPI router for '/my_web_plugin'.")
return [plugin_web_router] # Return a list containing your router(s)
After enabling my_web_api_plugin.py
and restarting the Bedrock Server Manager web server, you could access:
GET /my_web_plugin/info
(API endpoint)POST /my_web_plugin/submit_data
(API endpoint, with a JSON body)GET /my_web_plugin/custom-html-page
(HTML page)
These endpoints will also be listed in the OpenAPI documentation (e.g., at /api/openapi.json
or /docs
).
5.1.1. Serving HTML Pages with Application Styling (Jinja2 Templates)
While returning direct HTMLResponse
content is possible, for a look and feel consistent with the main application, plugins can serve HTML pages rendered via Jinja2 templates that extend the application’s base.html
. This requires structuring your plugin as a package.
1. Provide Template Files:
Create a subdirectory in your plugin package to hold your HTML templates (e.g.,
my_packaged_plugin/templates/
).Your HTML template file (e.g.,
my_page.html
) should extend the main application’s base template:{# my_packaged_plugin/templates/my_page.html #} {% extends "base.html" %} {% block title %}{{ super() }} - My Plugin Page Title{% endblock %} {% block content %} <h1>Hello from My Plugin!</h1> <p>This content is specific to the plugin and uses the main app layout.</p> <p>Data from route: {{ my_plugin_data }}</p> {% endblock %}
2. Register Template Directory:
Implement the get_template_paths()
method in your plugin class:
# my_packaged_plugin/__init__.py
from pathlib import Path
from bedrock_server_manager import PluginBase
# ... other imports for your router, etc.
class MyPackagedPlugin(PluginBase):
version = "1.0.0"
# ... other plugin methods ...
def get_template_paths(self) -> list[Path]:
"""Returns the path to this plugin's templates directory."""
plugin_root_dir = Path(__file__).parent
return [plugin_root_dir / "templates"]
def get_fastapi_routers(self):
# Return your router that uses these templates
return [my_plugin_web_router]
3. Render Templates in Route Handlers:
In your plugin’s FastAPI route handlers, import and use the main application’s configured Jinja2 templates
environment.
# my_packaged_plugin/__init__.py or a submodule like web_routes.py
from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse # Keep for other types of responses if needed
from fastapi.templating import Jinja2Templates
from bedrock_server_manager.web import get_templates
my_plugin_web_router = APIRouter(prefix="/my-package", tags=["My Packaged Plugin"])
@my_plugin_web_router.get("/styled-page", response_class=HTMLResponse)
async def get_styled_plugin_page(
request: Request,
templates: Jinja2Templates = Depends(get_templates),
):
# "my_page.html" will be found by Jinja2 if the plugin's template path
# was correctly registered via get_template_paths().
return templates.TemplateResponse(
"my_page.html",
{"request": request, "my_plugin_data": "Some dynamic data!"},
)
The Plugin Manager and the main application will ensure that the path returned by get_template_paths()
is added to the Jinja2 loader’s search path.
5.1.2. Serving Plugin-Specific Static Files (CSS, JS, Images)
If your plugin requires its own static assets (CSS, JavaScript, images) that are not part of the main application’s static files, you can make them available.
1. Organize Static Files:
Create a subdirectory in your plugin package for these static files (e.g.,
my_packaged_plugin/static_files/
).my_packaged_plugin/ └── static_files/ ├── css/ │ └── plugin_style.css └── js/ └── plugin_script.js
2. Register Static Directory Mounts:
Implement the get_static_mounts()
method in your plugin class:
# my_packaged_plugin/__init__.py
from pathlib import Path
from bedrock_server_manager import PluginBase
# ...
class MyPackagedPlugin(PluginBase):
version = "1.0.0"
# ...
def get_static_mounts(self) -> list[tuple[str, Path, str]]:
"""Returns configurations for mounting this plugin's static file directories."""
plugin_root_dir = Path(__file__).parent
plugin_static_dir = plugin_root_dir / "static_files"
# mount_url_path: The URL path your static files will be served from.
# Must be unique (e.g., include plugin name).
# local_directory_path: The actual path to your static files.
# mount_name: A unique name for this static route in FastAPI.
mount_url_path = "/static/my_packaged_plugin"
mount_name = "my_packaged_plugin_static_files"
if plugin_static_dir.is_dir(): # Only add if the directory exists
return [(mount_url_path, plugin_static_dir, mount_name)]
return []
3. Reference Static Files in Templates:
In your plugin’s Jinja2 templates (or even directly in HTMLResponse content if carefully constructed), use FastAPI/Starlette’s request.url_for()
to generate correct URLs for your plugin’s static files, using the name
you provided in get_static_mounts()
.
{# my_packaged_plugin/templates/my_page.html #}
{% extends "base.html" %}
{% block head_styles %}
{{ super() }}
{# Link to this plugin's specific CSS file #}
<link rel="stylesheet" href="{{ request.url_for('my_packaged_plugin_static_files', path='css/plugin_style.css') }}">
{% endblock %}
{% block content %}
<h1>Plugin Page with Custom Styles!</h1>
<!-- ... -->
{% endblock %}
{% block body_scripts %}
{{ super() }}
{# Link to this plugin's specific JS file #}
<script src="{{ request.url_for('my_packaged_plugin_static_files', path='js/plugin_script.js') }}"></script>
{% endblock %}
The Plugin Manager and main application will use the information from get_static_mounts()
to call app.mount()
appropriately for your plugin.
Tip
Tips for Plugin Web Endpoints:
Unique Prefixes & Mount Names: Essential for routers and static mounts to avoid conflicts.
Authentication: Apply as needed to your plugin’s routers or individual routes.
HTML Pages: Tag your HTML routers with
plugin-ui
to have it added to the Web UI
6. Best Practices
Tip
Always use
self.logger
: Do not useprint()
. The provided logger is integrated with the application’s logging system.Handle exceptions: Wrap API calls in
try...except
blocks to handle potential failures gracefully.Check the
result
dictionary: After anafter_*
event, inspect theresult['status']
to confirm the outcome.Avoid blocking operations: Long-running tasks in your event handlers can freeze the application. Offload them to separate threads if necessary.
Use the API for operations: Do not directly manipulate server files or directories. Use the provided
self.api
functions to ensure thread-safety and consistency.