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.
Plugin dashboards
Every plugin can expose a settings/data page on the user-facing dashboard. Two modes — pick one:
- Manifest mode — declare widgets in JSON, write Python data handlers. Themed for free, no HTML to write.
- Iframe mode — full HTML/CSS/JS control. Use when you need a custom UI (drag-and-drop, charts the manifest doesn't cover, custom forms).
Manifest mode
Drop a dashboard_manifest.json next to your manifest.json:
{
"pages": [
{
"id": "overview",
"title": "Overview",
"widgets": [
{
"id": "active_users",
"type": "stat_card",
"title": "Active users",
"rpc_method": "dashboard.get_active_users"
},
{
"id": "messages_chart",
"type": "chart",
"chart_type": "line",
"title": "Messages per day",
"rpc_method": "dashboard.get_messages_chart"
},
{
"id": "settings_form",
"type": "form",
"title": "Settings",
"rpc_method": "dashboard.get_settings",
"save_method": "dashboard.save_settings",
"fields": [
{"name": "channel_id", "label": "Welcome channel", "type": "channel"},
{"name": "message", "label": "Welcome text", "type": "text"}
]
}
]
}
]
}
Supported widget types
| Type | Handler returns |
|---|---|
| stat_card | {"value": 1234, "change": "+12%"} |
| chart | {"labels": [...], "series": [{"name": "...", "data": [...]}]} |
| table | {"rows": [{...}, ...], "total": 100} |
| list | {"items": [{"label": "...", "value": "..."}, ...]} |
| progress_bar | {"value": 75, "max": 100, "label": "…"} |
| text | {"text": "Plain text content"} |
| markdown | {"markdown": "# Heading\n- bullet"} |
| alert | {"level": "info", "message": "…"} — level: info / warn / error |
| form | get → {"values": {...}}; save → {"ok": true} |
Python handlers
@plugin.on_dashboard("get_active_users")
def active_users(ctx: Context, params: dict):
n = ctx.kv.count(prefix="active_user:")
return {"value": n, "change": "+12%"}
@plugin.on_dashboard("get_messages_chart")
def messages_chart(ctx: Context, params: dict):
data = ctx.metrics.query("messages", period="7d")
return {
"labels": data["labels"],
"series": [{"name": "Messages", "data": data["series"][0]["data"]}],
}
@plugin.on_dashboard("get_settings")
def get_settings(ctx: Context, params: dict):
return {"values": {
"channel_id": ctx.kv.get("welcome_channel") or "",
"message": ctx.kv.get("welcome_message") or "Welcome!",
}}
@plugin.on_dashboard("save_settings")
def save_settings(ctx: Context, params: dict):
ctx.kv.set("welcome_channel", params["values"]["channel_id"])
ctx.kv.set("welcome_message", params["values"]["message"])
return {"ok": True}
Dashboard handlers must return within 10 seconds. params always contains discord_srv_id plus any widget-specific arguments (form values for save methods, table pagination, etc.).
Iframe mode
Drop a dashboard/ directory with at least index.html. The platform serves it inside a sandboxed iframe and gives you a JS bridge (MaidSDK) for backend RPC, theming, and lifecycle.
my_plugin/
├── manifest.json
├── __main__.py
└── dashboard/
├── index.html # entry point
├── style.css
└── app.js
Minimal HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/static/dashboard.css">
</head>
<body>
<h1>My plugin</h1>
<div id="stat">Loading…</div>
<script src="/static/mmo-maid-sdk.js"></script>
<script>
MaidSDK.ready(async function () {
const ctx = MaidSDK.getContext();
// ctx = { discord_srv_id, plugin_id, user_id, theme: "dark" | "light" }
const total = await MaidSDK.metrics.total("messages_sent", "30d");
document.getElementById("stat").textContent = total + " messages";
MaidSDK.resize(); // ask the iframe host to resize to fit content
});
</script>
</body>
</html>
MaidSDK JS API
Every method returns a Promise. The bridge is sandboxed — you can only call allow-listed RPC namespaces (kv, metrics, sql, dashboard).
| Call | Purpose |
|---|---|
| MaidSDK.ready(fn) | Run fn after the bridge handshake completes. |
| MaidSDK.getContext() | Sync — returns server / plugin / user / theme. |
| MaidSDK.getTheme() | Sync — "dark" or "light". |
| MaidSDK.on(event, fn) | Listen for "theme", "resize", etc. |
| MaidSDK.off(event, fn) | Remove listener. |
| MaidSDK.resize(height?) | Tell the host iframe to resize. Auto-detects content if no height passed. |
| MaidSDK.navigateTo(pageId) | Switch to another page in your dashboard manifest. |
| MaidSDK.rpc(method, params) | Call a Python @plugin.on_dashboard handler. |
| MaidSDK.kv.get / set / delete / list / getMany / setMany | Direct KV access from the iframe. |
| MaidSDK.metrics.query / total | Pull metrics for charts. |
| MaidSDK.sql.query / execute | SQL access (requires storage:sql capability). |
Theme variables
The iframe inherits these CSS custom properties from the dashboard so your widget blends in without you reverse-engineering colors:
:root {
--gold; --gold-light; --gold-dim; --gold-bg;
--text; --muted; --border;
--surface; --depth; --green; --red;
}
Drop-in style pack new
For plugin-rendered HTML (both manifest widgets and iframe dashboards) the platform ships a small CSS + JS pack so your UI matches the host without you copying styles.
For non-iframe (manifest) dashboards — reference directly:
<link rel="stylesheet" href="/static/sdk/ui.css?v=1">
<script src="/static/sdk/ui.js?v=1" defer></script>
For iframe dashboards — the iframe CSP blocks cross-origin assets, so bundle the file into your zip under dashboard/sdk/ and reference via the sandboxed origin:
<link rel="stylesheet" href="dashboard-assets/sdk/ui.css">
<script src="dashboard-assets/sdk/ui.js" defer></script>
What's in the pack:
.maid-btn/.maid-btn-primary/.maid-btn-danger— buttons matching the platform palette.maid-input/.maid-textarea/.maid-select— form fields with theme-aware focus rings.maid-card— styled container with header/help slots.maid-badge/.maid-pill— status indicators with semantic colors (ok / warn / error / info / muted).maid-table— theme-aware data table.maid-stack/.maid-grid/.maid-row— layout helpers.maid-empty— empty-state container
JS helpers (namespaced under window.MaidUI):
MaidUI.toast("Saved!", "ok");
MaidUI.confirm("Delete this record?").then((yes) => { if (yes) /* ... */ });
MaidUI.fetchJSON("/p/{plugin_id}/dashboard/rpc/save_settings", {
method: "POST",
body: JSON.stringify(MaidUI.serializeForm(myFormElement)),
});
MaidUI.copy("text to clipboard");
SDK v0.5.1 · Last updated April 2026 · Back to Dev Portal