Build a plugin
The full SDK reference for plugin developers — sandboxed Python, shipped as a zip, distributed through the marketplace.
SDK v0.5.1 · Plugins are sandboxed Python processes. Write a handler, declare your capabilities, ship a zip — we run it in a locked-down Docker container and stream Discord events to it over JSON-RPC.
Quick start
Five minutes to a running plugin:
pip install mmo-maid-sdktemplate.zip__main__.py in the unpacked folder.Hello, plugin
This whole file is a working plugin. Drop it in a folder with a manifest.json and ship it:
from mmo_maid_sdk import Plugin, Context
plugin = Plugin()
@plugin.on_ready
def ready(ctx: Context):
ctx.log("Plugin booted on server " + ctx.server_id)
@plugin.on_event("message_create")
def on_message(ctx: Context, event: dict):
if event.get("author_bot"):
return # ignore other bots
if "!ping" in event.get("content", ""):
ctx.discord.send_message(
channel_id=event["channel_id"],
content="Pong!",
)
# This is the entrypoint. Without it the plugin process exits immediately.
plugin.run()
Plugin file structure
A plugin is a zip with this layout:
my_plugin/
├── manifest.json # required — id, version, capabilities, …
├── __main__.py # required — your handler code (the entry point)
├── requirements.txt # optional — pip deps to install in the sandbox
├── dashboard_manifest.json # optional — declarative dashboard
└── dashboard/ # optional — iframe-mode dashboard (HTML/CSS/JS)
└── index.html
Limits: 10 MB zipped, 40 MB uncompressed, 200 files max. Anything outside this layout is rejected by the upload validator.
manifest.json
Declares who you are, what version this is, and what you're allowed to do. Minimum viable manifest:
{
"id": "my_plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "Pongs whenever someone types !ping.",
"capabilities_required": [
"discord:send_message"
]
}
Optional fields:
| Field | Purpose |
|---|---|
| author | Public byline (your developer name). |
| icon_url | Square PNG/JPG, ≤ 1 MB, served over HTTPS. |
| tags | Array of search tags. ["moderation", "fun"] |
| capabilities_required | Capabilities you need; install fails if any are denied. See Capabilities. |
| slash_commands | Discord slash commands the platform should register on your behalf. See Slash commands. Declaring this auto-adds interaction:respond to capabilities_required. |
| proxy_domains_requested | Allow-list of hostnames you'll fetch through ctx.http.*. The upload pipeline auto-detects domains from literal "https://…" URLs in your source; this field is your final review. Non-empty values auto-add proxy:http to capabilities_required. |
Plugin lifecycle expanded
The SDK exposes hooks for every meaningful state transition. All five lifecycle decorators take the same handler shape: (ctx) with no event payload.
| Decorator | Fires when |
|---|---|
| @plugin.on_install | The plugin is installed on a server for the first time. Use to seed default settings. |
| @plugin.on_enable | An admin toggles the plugin on. Fires every time, including after a disable. |
| @plugin.on_ready | The plugin process boots and the SDK handshake completes. Fires once per worker start. |
| @plugin.on_disable | An admin toggles the plugin off. Use to pause background work or clear caches. |
| @plugin.on_uninstall | The plugin is being removed. Last chance to clean up KV / SQL state. |
@plugin.on_install
def first_install(ctx: Context):
ctx.kv.set("welcome_channel", "")
ctx.log("installed on " + ctx.server_id)
@plugin.on_enable
def enabled(ctx: Context):
ctx.log("re-enabled")
@plugin.on_uninstall
def cleanup(ctx: Context):
# KV is wiped automatically on uninstall; this hook lets you do
# extra side effects (e.g. post a goodbye message).
pass
plugin.run() # required — starts the event loop and blocks forever.
You must call plugin.run() at the bottom of __main__.py. Without it the process exits before the SDK connects to the runner.
SDK v0.5.1 · Last updated April 2026 · Back to Dev Portal