← Documentation  /  Developer Guide

Build a plugin

The full SDK reference for plugin developers — sandboxed Python, shipped as a zip, distributed through the marketplace.

These docs are publicly viewable. To publish a plugin you'll need a free MMO Maid account — sign in to access the Dev Portal.

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:

1
Install the SDK: pip install mmo-maid-sdk
2
Download the starter template: template.zip
3
Edit __main__.py in the unpacked folder.
4
Zip the folder and upload it on the Dev Portal.
5
Submit for review — published plugins appear on the Marketplace.

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:

FieldPurpose
authorPublic byline (your developer name).
icon_urlSquare PNG/JPG, ≤ 1 MB, served over HTTPS.
tagsArray of search tags. ["moderation", "fun"]
capabilities_requiredCapabilities you need; install fails if any are denied. See Capabilities.
slash_commandsDiscord slash commands the platform should register on your behalf. See Slash commands. Declaring this auto-adds interaction:respond to capabilities_required.
proxy_domains_requestedAllow-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.

DecoratorFires when
@plugin.on_installThe plugin is installed on a server for the first time. Use to seed default settings.
@plugin.on_enableAn admin toggles the plugin on. Fires every time, including after a disable.
@plugin.on_readyThe plugin process boots and the SDK handshake completes. Fires once per worker start.
@plugin.on_disableAn admin toggles the plugin off. Use to pause background work or clear caches.
@plugin.on_uninstallThe 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