Plugin Development
Mumimo plugins are self-contained packages consisting of a Python module, a plugin.toml
manifest, and tests. Plugins can be core (built-in, always available) or extensions
(optional, loaded on demand).
Plugin Structure
Section titled “Plugin Structure”Every plugin lives in its own directory with a fixed layout:
mumimo/plugins/extensions/<name>/├── __init__.py # Empty or package init├── <name>.py # class Plugin(PluginBase)├── plugin.toml # Metadata, commands, roles, aliases└── tests/ └── test_<name>.py # pytest testsCore plugins follow the same structure under mumimo/plugins/core/<name>/.
The Plugin Manifest (plugin.toml)
Section titled “The Plugin Manifest (plugin.toml)”Every plugin must include a plugin.toml that declares its metadata and commands:
[plugin]name = "my_plugin"version = "1.0.0"type = "extension"audio = falsethreading = "single"wait_for = []
[commands.greet]description = "Say hello"help = "!greet — Says hello to the caller."roles = ["user"]aliases = ["hello"]flags = []Plugin metadata fields
Section titled “Plugin metadata fields”| Field | Type | Description |
|---|---|---|
name |
string | Plugin identifier (must match directory name) |
version |
string | Semantic version string |
type |
string | "core" or "extension" |
audio |
boolean | Whether the plugin uses the audio engine |
threading |
string | "single" or "worker" — see Threading Modes |
wait_for |
list | Command names that must complete before other commands are processed |
Command declaration fields
Section titled “Command declaration fields”Each [commands.<name>] table declares a command the plugin handles:
| Field | Type | Description |
|---|---|---|
description |
string | Short description of the command |
help |
string | Help text shown to users |
roles |
list | Roles required to use the command (e.g. ["user"], ["admin"]) |
aliases |
list | Alternative command names |
flags |
list | Supported flags (e.g. ["--quiet", "-q"]) |
The Plugin Class
Section titled “The Plugin Class”Every plugin module must define a class Plugin that inherits from PluginBase:
from mumimo.plugins.plugin_base import PluginBase, command, CommandData, PluginContext
class Plugin(PluginBase): def __init__(self, ctx: PluginContext) -> None: super().__init__(ctx)
def start(self) -> None: """Called when the plugin is loaded. Acquire resources here.""" super().start()
def stop(self) -> None: """Called when the plugin is stopped. Release resources here.""" super().stop()
def quit(self) -> None: """Called when the bot shuts down. Final cleanup.""" super().quit()
@command("greet") def cmd_greet(self, data: CommandData) -> None: self.ctx.gui.gui_info(f"Hello, {data.actor}!")Lifecycle methods
Section titled “Lifecycle methods”| Method | When Called | Purpose |
|---|---|---|
start() |
Plugin loaded | Acquire resources, initialise state |
stop() |
Plugin stopped | Release resources (can be restarted) |
quit() |
Bot shutting down | Final cleanup (terminal) |
All lifecycle methods are idempotent — calling them when already in the target state is
a no-op. Always call super() first when overriding.
Command Handlers
Section titled “Command Handlers”Commands are implemented as methods named cmd_<name> and decorated with @command("<name>"):
@command("roll")def cmd_roll(self, data: CommandData) -> None: """Handle !roll <sides>.""" if not data.args: self.ctx.gui.gui_error("Usage: !roll <sides>") return sides = int(data.args[0]) result = random.randint(1, sides) self.ctx.gui.gui_result(f"Rolled a {result}")CommandData fields
Section titled “CommandData fields”The data parameter provides everything about the incoming command:
| Field | Type | Description |
|---|---|---|
name |
str |
The command name that was invoked |
args |
list[str] |
Positional arguments (flags stripped) |
args_raw |
str |
Full raw string after the command name |
raw |
str |
Complete original message |
actor |
User |
The user who sent the command |
channel |
Channel | None |
Channel where the command was sent |
flags |
dict[str, bool] |
Parsed flags (--flag → True/False) |
PluginContext
Section titled “PluginContext”Every plugin receives a PluginContext via self.ctx — the capability handle that
provides access to all bot subsystems:
| Capability | Access | Description |
|---|---|---|
log |
self.ctx.log |
structlog BoundLogger |
config |
self.ctx.config |
Bot configuration dataclass |
paths |
self.ctx.paths |
Data directory, log directory, etc. |
gui |
self.ctx.gui |
GuiService for sending messages |
audio |
self.ctx.audio |
AudioEngine for playback |
mumble |
self.ctx.mumble |
MumbleSession (channels, users) |
db |
self.ctx.db |
SQLite database access |
events |
self.ctx.events |
Synchronous event bus |
scheduler |
self.ctx.scheduler |
Scheduled task runner |
aliases |
self.ctx.aliases |
AliasService for command aliases |
history |
self.ctx.history |
Per-user command history |
plugins |
self.ctx.plugins |
PluginRegistry for plugin lookup |
privileges |
self.ctx.privileges |
PrivilegeService (RBAC dispatch gate) |
role_service |
self.ctx.role_service |
RoleService for runtime role management |
theme_registry |
self.ctx.theme_registry |
Theme registry for dynamic themes |
boot_time |
self.ctx.boot_time |
float timestamp of bot startup |
GUI Output Helpers
Section titled “GUI Output Helpers”Use the semantic helpers on self.ctx.gui instead of raw quick_gui where possible:
| Helper | Usage |
|---|---|
gui_error(msg) |
Display an error message (red header) |
gui_info(msg) |
Display an informational message |
gui_result(content) |
Display a result |
gui_list(title, items) |
Display a list of items |
gui_reply(actor, prompt, reply) |
Reply to a specific user’s message |
All helpers accept an optional channel keyword argument to target a specific channel.
self.ctx.gui.gui_error("Something went wrong!", channel=data.channel)self.ctx.gui.gui_list("Available dice", ["d4", "d6", "d8", "d10", "d12", "d20"])Threading Modes
Section titled “Threading Modes”Declared in plugin.toml via the threading field:
-
"single"(default) — Commands execute in the main command dispatch thread. Simpler and sufficient for most plugins that return quickly. -
"worker"— Commands execute on a worker thread via an executor. Use this for long-running operations (network requests, heavy computation) that would otherwise block other commands.
The wait_for field lists command names that must complete before other commands from
the same user are processed. This is useful for serialising access to shared state.
RBAC (Role-Based Access Control)
Section titled “RBAC (Role-Based Access Control)”Gate commands by declaring required roles in plugin.toml:
[commands.kick]description = "Kick a user"roles = ["admin"]- Commands with
roles = ["user"]are available to everyone. - Commands with
roles = ["admin"]require the admin role. - The admin role bypasses all gates.
- Operators can override command roles at runtime via
[command_overrides]inconfig.toml.
The Contract
Section titled “The Contract”Adding a New Plugin
Section titled “Adding a New Plugin”-
Create the plugin directory
Terminal window mkdir -p mumimo/plugins/extensions/my_plugin/tests -
Write the plugin module
Create
my_plugin.pywith aclass Plugin(PluginBase)that implements your commands. -
Write the manifest
Create
plugin.tomlwith[plugin]metadata and[commands.<name>]tables for each command. Make sure every command has a matching@commandhandler. -
Write tests
Create
tests/test_my_plugin.py. Standard practice is to parseplugin.tomland assert the command count matches the number of@commandmethods, and that the version string is correct. -
Test and iterate
Run the test suite to verify:
Terminal window pytest mumimo/plugins/extensions/my_plugin/tests/ -v
Reference Plugins
Section titled “Reference Plugins”- Simple:
mumimo/plugins/extensions/randomizer/— stateless dice rolls and coin flips. Good starting point for a new plugin. - Complex:
mumimo/plugins/extensions/ai_assistant/— OpenRouter-backed chat, TTS, emotion tracking, karma, soundboard, and tool integration. Demonstrates advanced patterns like worker threading, event subscriptions, and database access.