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.
Cookbook
Copy-pasteable starter snippets for the most common plugin patterns. Each recipe is a minimal working example — drop it into your __main__.py and adapt. All capabilities used are auto-detected on upload so you don't need to manually edit manifest.json.
1) Welcome new members
Greet every new member in a channel an admin configures via /welcome-channel.
from mmo_maid_sdk import Plugin, Context
from mmo_maid_sdk.events import MemberJoin
plugin = Plugin()
@plugin.on_event("member_join")
def greet(ctx: Context, event: MemberJoin):
channel_id = ctx.kv.get("welcome_channel_id")
if not channel_id:
return
name = event.get("display_name") or event.get("username") or "friend"
ctx.discord.send_message(channel_id=str(channel_id),
content=f"Welcome, **{name}**! :wave:")
@plugin.on_slash_command("welcome-channel")
def set_channel(ctx: Context, event: dict):
ctx.kv.set("welcome_channel_id", event["channel_id"])
ctx.interaction.respond(content="This is now the welcome channel.", ephemeral=True)
2) Reaction roles (click ✓ to get a role)
Admin posts a message, configures the role+emoji pair, and any user who reacts gets the role.
from mmo_maid_sdk import Plugin, Context
from mmo_maid_sdk.events import ReactionAdd
plugin = Plugin()
@plugin.on_event("reaction_add")
def grant_role(ctx: Context, event: ReactionAdd):
if event.get("user_bot"):
return # ignore bot reactions, including our own
cfg = ctx.kv.get(f"react_role:{event['message_id']}")
if not cfg:
return
if event["emoji"] == cfg["emoji"]:
ctx.discord.add_role(user_id=event["user_id"], role_id=cfg["role_id"])
@plugin.on_slash_command("react-role-bind")
def bind(ctx: Context, event: dict):
# event["modal_values"] would carry message_id / emoji / role_id from a modal
mv = event.get("modal_values") or {}
ctx.kv.set(f"react_role:{mv['message_id']}", {"emoji": mv["emoji"], "role_id": mv["role_id"]})
ctx.interaction.respond(content="Reaction role bound.", ephemeral=True)
3) Daily scheduled announcement
Post a server-stat recap at 09:00 UTC every day.
from mmo_maid_sdk import Plugin, Context
plugin = Plugin()
@plugin.cron("0 9 * * *") # every day at 09:00 UTC
def morning_recap(ctx: Context):
channel_id = ctx.kv.get("recap_channel_id")
if not channel_id:
return
count = ctx.kv.increment("messages_today_total", reset_ttl_sec=86400)
ctx.discord.send_message(channel_id=str(channel_id),
content=f"Good morning! {count} messages were posted yesterday.")
4) /leaderboard top 10 users by messages
Track per-user message count; `/leaderboard` posts the top 10.
from mmo_maid_sdk import Plugin, Context
from mmo_maid_sdk.events import MessageCreate
plugin = Plugin()
@plugin.on_event("message_create")
def count(ctx: Context, event: MessageCreate):
if event.get("author_bot"):
return
ctx.kv.increment(f"msgs:{event['author_id']}")
@plugin.on_slash_command("leaderboard")
def show(ctx: Context, event: dict):
rows = ctx.kv.list(prefix="msgs:") or []
top = sorted(rows, key=lambda r: int(r["value"] or 0), reverse=True)[:10]
lines = [f"{i+1}. <@{r['key'].split(':')[1]}> — {r['value']}" for i, r in enumerate(top)]
ctx.interaction.respond(content="**Top 10 talkers**\n" + "\n".join(lines))
5) AFK status
/afk <reason> sets the user's status; the bot replies when they're mentioned.
from mmo_maid_sdk import Plugin, Context
from mmo_maid_sdk.events import MessageCreate
plugin = Plugin()
@plugin.on_slash_command("afk")
def set_afk(ctx: Context, event: dict):
reason = (event.get("modal_values") or {}).get("reason") or "afk"
ctx.kv.set(f"afk:{event['user_id']}", reason, ttl_sec=86400)
ctx.interaction.respond(content=f"Got it — set you AFK ({reason}).", ephemeral=True)
@plugin.on_event("message_create")
def notify_mentions(ctx: Context, event: MessageCreate):
if event.get("author_bot") or event.get("mention_count", 0) == 0:
return
# In production you'd parse event['content'] for <@user_id> mentions.
# For brevity this recipe just shows the pattern.
# Look up AFK status for any mentioned user and reply.
pass
6) Custom commands stored in KV
Server admins add their own !shortcut commands without redeploying.
@plugin.on_event("message_create")
def custom(ctx: Context, event: MessageCreate):
content = event.get("content", "").strip()
if not content.startswith("!"):
return
name = content[1:].split(" ", 1)[0]
response = ctx.kv.get(f"cmd:{name}")
if response:
ctx.discord.send_message(channel_id=event["channel_id"], content=str(response))
@plugin.on_slash_command("addcmd")
def add(ctx: Context, event: dict):
mv = event.get("modal_values") or {}
ctx.kv.set(f"cmd:{mv['name']}", mv["response"])
ctx.interaction.respond(content=f"Saved !{mv['name']}.", ephemeral=True)
7) Reaction-based polls
/poll creates a message with ✓/✗ reactions; tallies via reaction count later.
@plugin.on_slash_command("poll")
def poll(ctx: Context, event: dict):
mv = event.get("modal_values") or {}
question = mv.get("question") or "Yes or no?"
msg = ctx.discord.send_message(channel_id=event["channel_id"], content=f"**Poll:** {question}")
ctx.discord.add_reaction(channel_id=event["channel_id"], message_id=msg["id"], emoji="✅")
ctx.discord.add_reaction(channel_id=event["channel_id"], message_id=msg["id"], emoji="❌")
ctx.interaction.respond(content="Poll posted!", ephemeral=True)
8) Message logger
Log every non-bot message to a configurable mod channel (useful for audit trails).
@plugin.on_event("message_create")
def log(ctx: Context, event: MessageCreate):
if event.get("author_bot"):
return
log_channel = ctx.kv.get("mod_log_channel_id")
if not log_channel:
return
ctx.discord.send_message(channel_id=str(log_channel),
content=f"[#{event.get('channel_name')}] <@{event['author_id']}>: {event['content'][:200]}")
9) /stats — server stat card
Show member count, message count today, etc.
@plugin.on_slash_command("stats")
def stats(ctx: Context, event: dict):
msgs_today = ctx.kv.get("messages_today_total") or 0
new_today = ctx.kv.get("new_members_today") or 0
ctx.interaction.respond(content=(
f"**Server stats**\n"
f"Messages today: {msgs_today}\n"
f"New members today: {new_today}"
))
10) Role menu (dropdown)
Admin runs /role-menu, gets a dropdown listing self-assignable roles.
from mmo_maid_sdk import Plugin, Context, ActionRow, SelectMenu, SelectOption
plugin = Plugin()
@plugin.on_slash_command("role-menu")
def menu(ctx: Context, event: dict):
roles = ctx.kv.get("self_roles") or [] # list of {"id": "...", "label": "..."}
options = [SelectOption(label=r["label"], value=r["id"]) for r in roles]
ctx.interaction.respond(content="Pick your role(s):",
components=[ActionRow(SelectMenu("pick_role", options, min_values=1, max_values=len(options)))])
@plugin.on_component("pick_role")
def picked(ctx: Context, event: dict):
selected_ids = event.get("values") or []
for rid in selected_ids:
ctx.discord.add_role(user_id=event["user_id"], role_id=rid)
ctx.interaction.respond(content="Roles updated.", ephemeral=True)
11) Ticket triage: /ticket opens a thread
User runs /ticket <subject>; the bot creates a private thread tagging the support role.
@plugin.on_slash_command("ticket")
def open_ticket(ctx: Context, event: dict):
mv = event.get("modal_values") or {}
subject = mv.get("subject") or "Support request"
msg = ctx.discord.send_message(channel_id=event["channel_id"],
content=f"**Ticket from <@{event['user_id']}>**: {subject}")
# Real plugins would call ctx.discord.create_thread(...) here.
ctx.interaction.respond(content="Ticket opened — staff will reply shortly.", ephemeral=True)
12) Auto-respond to keywords
Trigger a reply when a message contains a configured keyword. Useful for FAQs.
@plugin.on_event("message_create")
def autoresponder(ctx: Context, event: MessageCreate):
if event.get("author_bot"):
return
content = event.get("content", "").lower()
triggers = ctx.kv.get("keyword_triggers") or {} # {keyword: reply}
for kw, reply in (triggers.items() if isinstance(triggers, dict) else []):
if kw.lower() in content:
ctx.discord.send_message(channel_id=event["channel_id"], content=str(reply))
break # one auto-reply per message
13) Birthday reminders
Users register their birthday via /birthday MM-DD; cron at midnight UTC announces the day's birthdays.
@plugin.on_slash_command("birthday")
def set_bday(ctx: Context, event: dict):
mv = event.get("modal_values") or {}
ctx.kv.set(f"bday:{event['user_id']}", mv.get("date"))
ctx.interaction.respond(content=f"Saved {mv.get('date')}.", ephemeral=True)
@plugin.cron("0 0 * * *") # midnight UTC
def announce(ctx: Context):
from datetime import datetime
today = datetime.utcnow().strftime("%m-%d")
bday_users = [r for r in (ctx.kv.list(prefix="bday:") or []) if r.get("value") == today]
channel_id = ctx.kv.get("birthday_channel_id")
if not channel_id or not bday_users:
return
mentions = " ".join(f"<@{r['key'].split(':')[1]}>" for r in bday_users)
ctx.discord.send_message(channel_id=str(channel_id),
content=f"🎂 Happy birthday {mentions}!")
14) Member of the week
Sunday 09:00 UTC: announce the top message-sender of the past week.
@plugin.cron("0 9 * * 0") # Sunday 09:00 UTC
def weekly_mvp(ctx: Context):
rows = ctx.kv.list(prefix="msgs_week:") or []
if not rows:
return
rows.sort(key=lambda r: int(r["value"] or 0), reverse=True)
top = rows[0]
uid = top["key"].split(":", 1)[1]
channel_id = ctx.kv.get("announce_channel_id")
if channel_id:
ctx.discord.send_message(channel_id=str(channel_id),
content=f"🏆 Member of the week: <@{uid}> with {top['value']} messages.")
# Reset weekly counters
for r in rows:
ctx.kv.delete(r["key"])
15) Voice channel time tracker
Track how long each user spends in voice channels. Useful for activity-based rewards.
from mmo_maid_sdk.events import VoiceStateUpdate
import time
@plugin.on_event("voice_state_update")
def track(ctx: Context, event: VoiceStateUpdate):
uid = event["user_id"]
if event.get("after_channel_id") and not event.get("before_channel_id"):
# joined voice
ctx.kv.set(f"voice_in:{uid}", str(int(time.time())))
elif event.get("before_channel_id") and not event.get("after_channel_id"):
# left voice
join_t = ctx.kv.get(f"voice_in:{uid}")
if join_t:
duration = int(time.time()) - int(join_t)
ctx.kv.increment(f"voice_secs:{uid}", amount=duration)
ctx.kv.delete(f"voice_in:{uid}")
Tip: Each snippet calls the SDK methods that auto-detect their required capabilities — you can drop any of these into a fresh mmo new my_plugin and the platform will figure out the manifest entries on upload. Use mmo validate before pushing to catch any gaps.
SDK v0.5.1 · Last updated April 2026 · Back to Dev Portal