High-density, schema-driven Discord components dialog engine with MCP and ACP backends.
1. π Quick Start & Commands
Commands are registered in the Red-DiscordBot Discord slash command registry.
| Command | Action | Key Options |
|---|---|---|
/dialog-run | Launches a new dialog session | --spec <name> (required), --channel <id> (optional) |
/dialog-list | Lists all active dialog sessions | None |
/dialog-cancel | Terminates an active session | --session <id> (required) |
/dialog-reload | Hot-reloads all spec YAML definitions | None |
2. π§ Core Directory Layout
UniDialog is structured as a standard Red-DiscordBot cog, organizing parser, state, rendering, and adapter systems:
unidialog/
βββ __init__.py # Cog entry point registering the cog instance
βββ info.json # Red Bot cog metadata (author, description, requirements)
βββ unidialog.py # Main cog class, handles Discord commands and event routing
βββ core/
β βββ parser.py # YAML loader and JSON Schema validation for dialog specs
β βββ state.py # Red Config integration and session state store
β βββ engine.py # Interaction loop router & component handler
β βββ renderer.py # Discord UI component layout solver
βββ adapters/
β βββ base.py # Abstract base class for backend adapters
β βββ local.py # Registers & dispatches local in-process Python actions
β βββ mcp.py # SSE client handling JSON-RPC calls to MCP servers
β βββ acp.py # Subprocess orchestrator for ACP over stdio pipelines
βββ specs/ # Local cache of default dialog specification YAMLs
βββ server_deploy.yml # Sample deployment workflow specification3. π Cog API & Component Listeners
UniDialog is implemented as a Python class subclassing Red-DiscordBotβs commands.Cog. It exposes registration interfaces for local extensions and listens directly to Discord interaction events:
from typing import Callable, Dict, Any, Optional
import discord
from redbot.core import commands, Config
class UniDialog(commands.Cog):
"""
Red-DiscordBot cog driving schema-driven components and adapters.
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.config = Config.get_conf(self, identifier=89101112)
self.local_actions: Dict[str, Callable[[discord.Interaction, Any], Any]] = {}
@commands.Cog.listener()
async def on_interaction(self, interaction: discord.Interaction) -> None:
"""
Global event listener intercepting component clicks and select menu changes.
Routes interactions with the 'unidialog:' prefix to the engine.
"""
pass
def register_local_action(self, name: str, callback: Callable[[discord.Interaction, Any], Any]) -> None:
"""
Registers a local Python callback to handle in-process actions
defined in YAML specs using the 'local' adapter.
"""
self.local_actions[name] = callback4. ποΈ Core Architecture (The 5 Layers)
UniDialog executes interactive UI experiences on Discord by organizing operations into a strict 5-layer pipeline.
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spec Layer β
β (YAML Parsing, Schema Validation) β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β State Layer β
β (Red-DiscordBot Config, User Sessions) β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Renderer Layer β
β (Layout Constraints, Paging/Grid) β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Adapter Layer β
β (Local Python, MCP SSE, ACP Stdio) β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Loop Layer β
β (Event Handlers, Deferrals, Redraws) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ| Layer | Primary Role | Core Logic / Operations |
|---|---|---|
| Spec Layer | Declarative YAML Parsing | Loads, validates, and builds in-memory representation of Dialog, Screen, and Block definitions. Enforces schemas. |
| State Layer | Session Persistence | Persists active navigation stacks, user bindings, input buffers, and session histories to Red Config. |
| Renderer Layer | Layout Constraint Solver | Translates Screen specs into Discord UI components. Manages button grids, dynamic selects, and modals. |
| Adapter Layer | Routing & Execution | Dispatches action handlers and data fetching requests to Local Python functions, MCP (SSE), or ACP (Stdio). |
| Loop Layer | Event Loop & Redraw | Intercepts component interactions, enforces 3s ACKs, schedules deferred updates, and performs message edits. |
π Interaction Lifecycle
When a user triggers a component interaction (e.g., button click or select dropdown):
- Event Dispatch: Discord sends an interaction payload to the bot.
- Loop Interception: The
Loop Layerintercepts the event and schedules an immediate deferral (interaction.response.defer()) within the 3-second limit. - Custom ID Parsing: The
Loop Layerparses the 100-character custom ID format (unidialog:{guild_id}:{channel_id}:{user_id}:{block_id}:{action_hash}) and performs Command Hijacking Prevention (verifying the interacting user matches the session owner). - State Lookup: The
State Layerfetches the active navigation stack and state document from the Red Config database using the parsed session keys. - Spec Mapping: The
Spec Layermatches the interaction block ID with the loaded Screen and Block specs. - Action Dispatch: The
Adapter Layerroutes the action to the defined adapter (Local, MCP, or ACP) and executes it with context/arguments parsed from the event. - Constraint Solving: If state updates occur, the
Renderer Layerresolves UI constraints (e.g., button grids, pagination offsets) and builds the new Discord UI component payload. - Redraw: The
Loop Layerupdates the Discord message with the new content and components, completing the cycle.
5. π Adapter Mappings
UniDialog bridges Discord interactions with different backend architectures using three distinct adapters: Local Python, Model Context Protocol (MCP), and Agent Context Protocol (ACP).
| Capability | Local Backend | MCP Backend | ACP Backend |
|---|---|---|---|
| Source Population | Direct Python calls to registered functions | HTTP/SSE JSON-RPC queries to retrieve dynamic dropdown options | JSON-RPC calls over Stdio to query running agent state |
| Action Handling | Executes local synchronous/asynchronous Python methods | Formulates JSON-RPC method invocations forwarded via SSE | Standardized stdio messages processed by spawning agents |
| Output Streams | Return-based updates or local file/database modifications | Chunked SSE updates from remote server | Stdio stream capturing with a 1.2s coalesce buffer |
| Permissions | Red-DiscordBot role checks & Guild configuration | Remote authorization, token-based checks, confirm dialogs | Subprocess isolation and prompt-based capability approval |
| Transport Layers | In-process Python modules | SSE (Server-Sent Events) and HTTP/JSON-RPC | Subprocess stdio pipelines (stdin/stdout/stderr) |
6. π± UI/UX & Responsive Rendering Rules
To deliver smooth interactive interfaces inside the constraints of Discordβs UI components, UniDialog enforces strict rendering rules:
1. Constraint Solving
- Paging Selects: Dropdown menus containing more than 25 options are automatically split into paginated dropdown components with βNext Pageβ and βPrevious Pageβ buttons.
- Modal Wizards: Forms requiring more than 5 distinct fields are automatically converted into multi-step modal wizards.
- Button Grids: Buttons are automatically arranged into rows of up to 5 buttons, preventing layout overflow (max 5 rows per message).
2. Throttling & Coalescing
- When capturing output streams from an ACP subprocess or MCP tool (e.g. streaming logs or thoughts), UniDialog buffers updates using a 1.2s coalesce buffer.
- Edits to the Discord message are rate-limited to a maximum of 5 edits per 5 seconds to prevent hitting Discord rate limits, while maintaining real-time progress feel.
3. 3-Second ACK Rule
- For all operations, UniDialog intercepts the Discord interaction and calls
interaction.response.defer()within the 3-second API deadline. - While the remote tool or agent subprocess is executing, a temporary loading state is displayed in the Discord UI.
4. Persistence on Reboot
- Every custom component ID is serialized in the format:
unidialog:{guild_id}:{channel_id}:{user_id}:{block_id}:{action_hash} - This format allows the UniDialog engine to route click interactions back to the correct state store and spec definition, even after bot reboots.
7. βοΈ Persistent State Schema (Red Config)
Session states are persisted in the Red-DiscordBot Config database. The nested document structure for a single session is defined as:
{
"session_id": "123e4567-e89b-12d3-a456-426614174000",
"spec_id": "server_deploy_dialog",
"user_id": "123456789012345678",
"guild_id": "987654321098765432",
"channel_id": "555555555555555555",
"message_id": "888888888888888888",
"nav_stack": ["main_menu", "confirm_screen"],
"state": {
"target_env": "production",
"last_action": "execute_deploy"
},
"created_at": 1781258400,
"updated_at": 1781258450
}8. π¦ Data Model & YAML Schema Spec
Dialog engines are defined declaratively in YAML using the following structure:
# Schema definition for unidialog.yml
dialog:
id: "server_deploy_dialog"
title: "Server Deployment Console"
initial_screen: "main_menu"
screens:
main_menu:
title: "Deployment Overview"
description: "Select an environment to proceed with deployment."
blocks:
- id: "env_select"
type: "select"
placeholder: "Choose Target Environment"
options_provider:
adapter: "mcp"
method: "list_environments"
on_change:
action: "set_state"
key: "target_env"
- id: "deploy_btn"
type: "button"
label: "Deploy Now"
style: "success"
on_click:
action: "navigate"
target: "confirm_screen"
confirm_screen:
title: "Confirm Deployment"
description: "Are you sure you want to deploy to {{state.target_env}}?"
blocks:
- id: "confirm_btn"
type: "button"
label: "Confirm & Launch"
style: "danger"
on_click:
adapter: "acp"
action: "execute_deploy"
args:
env: "{{state.target_env}}"
- id: "cancel_btn"
type: "button"
label: "Cancel"
style: "secondary"
on_click:
action: "navigate"
target: "main_menu"9. π Routes & Integration Protocols
1. MCP (Model Context Protocol) JSON-RPC over SSE
For MCP integrations, UniDialog issues requests to remote MCP servers using standard JSON-RPC 2.0.
Request Structure (Data Fetch / Options):
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "list_environments",
"arguments": {}
},
"id": 1
}Response Structure (Success):
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "[\"production\", \"staging\", \"development\"]"
}
]
},
"id": 1
}Response Structure (Error):
{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Internal error: Environment service unreachable",
"data": "Target service timed out after 5000ms"
},
"id": 1
}2. ACP (Agent Context Protocol) over Stdio
When interacting with local agents, UniDialog spawns the agent process and writes JSON commands to stdin, reading responses from stdout.
Command Input:
{
"command": "run_agent",
"agent_id": "deploy_agent",
"parameters": {
"env": "production"
}
}Stream Output:
{
"status": "running",
"log": "Compiling assets...",
"progress": 0.35
}10. π¨ Gotchas, Invariants & Safety
- Rate Limit Exhaustion: Discord rate limits allow max 5 message edits per 5 seconds. The 1.2s coalesce buffer must be strictly enforced.
- State Size Limit: Discord custom ID fields have a hard limit of 100 characters. The custom ID format
unidialog:{guild_id}:{channel_id}:{user_id}:{block_id}:{action_hash}must fit within this constraint (hash the action if arguments are too large). - Zombie Subprocesses: ACP processes spawned over stdio must be aggressively tracked and reaped on dialog cancellation, session timeout, or cog reload.
- Interaction Expiry: Discord interactions expire in 15 minutes. Long-running tasks must shift from edit interactions to direct message edits via webhook or API channel send.
- Command Hijacking: Button clicks must validate
user_idinside the custom ID against the interaction user. Other users clicking must receive an ephemeral βNot your sessionβ error. - Stream Terminations: If an ACP subprocess stdout stream terminates abruptly, the coalesce buffer must flush immediately rather than waiting for the remaining duration of the 1.2s timeout window.
- Select Options Failures: If a dropdown options provider fails (e.g. MCP timeout or bad JSON-RPC response), the engine must not crash or leave a blank UI; it must render a warning option (βFailed to load optionsβ) with an error icon and keep the user on the screen.
- Modal Form Timeout: Since Discord modals expire after approximately 20 minutes if unsubmitted, the engine must handle modal submit errors gracefully, clean up the associated Red Config session state, and release any session locks.
- RPC Parsing Failures: When receiving malformed JSON or invalid JSON-RPC 2.0 structures from an external MCP server, the MCP adapter must catch exceptions, log the error, and display a standard error UI screen to the user.
11. π Verification & Diagnostics Requirements
Before releasing any dialog definition or engine update, the following checks must be verified:
- Custom ID Size Check: Every generated button or dropdown custom ID must be verified to be under 100 characters.
- Zombie Sweeper: Verify that subprocess managers kill all spawned ACP subprocesses upon receiving a cancellation signal or on session timeout.
- State Compaction: Validate that active user session states stored in Red Config do not exceed 10KB to maintain database performance.
- 3-Second ACK Coverage: Confirm that every path leading to external systems (MCP or ACP) defers the interaction response instantly.
- Rate Limit Coalesce Test: Simulate rapid stream output (e.g. 50 chunks per second) and confirm that the coalesce buffer groups edits into max 5 updates per 5 seconds.
- Abrupt Stream Termination: Verify that the coalesce buffer flushes instantly upon EOF or process termination.
- Options Fallback: Inject an MCP options provider failure and verify that the UI falls back to an error option rather than freezing or crashing.
- Malformed JSON-RPC Handler: Test invalid JSON-RPC server responses and verify they render the standard error screen and do not crash the event loop.
12. π Rollout Phases
flowchart LR subgraph Core["Core Development"] P1["Phase 1: Engine Core & YAML Parser"] P2["Phase 2: State Store & Navigation"] end subgraph Integrations["Integrations"] P3["Phase 3: MCP Tool Integration (SSE)"] P4["Phase 4: ACP Runner & Safety Gates"] end subgraph Release["Quality & Release"] P5["Phase 5: Release QA & Diagnostics"] end P1 --> P2 --> P3 --> P4 --> P5
Phase 1: Engine Core & YAML Spec Parser
- Implement YAML parser and validator for Dialog, Screen, and Block specs.
- Build the layout constraint solver (paging, wrapping rows).
- Integrate the Local Python adapter backend.
Phase 2: Persistent State Store & Navigation
- Setup Red Config schemas for persisting active sessions.
- Implement the custom ID serialization for click routing.
- Add back-button/breadcrumb tracking capabilities.
Phase 3: MCP Tool Integration
- Build Server-Sent Events (SSE) client for remote MCP tool invocation.
- Map MCP schemas directly to dynamic components (e.g. generating modals from JSON schemas).
Phase 4: ACP Agent Runner & Safety Gates
- Build subprocess manager for executing ACP agents over stdio.
- Implement the 1.2s coalesce rate-limiting buffer for output streams.
- Add confirmation safety gates for dangerous actions.
Phase 5: Release QA & Diagnostics
- Perform rate-limit stress tests (simulating concurrent button interactions).
- Verify persistent state survival across simulated bot reboots.
- Write suite of diagnostic commands.
13. π Related
- Ompycord β always-on OMPβDiscord bridge.
- SSOTEmbed Β· Red vs OMP Β· Review Β· Cheat Sheet