mirror of
https://github.com/araxiaonline/TrinityCore.git
synced 2026-06-13 03:32:28 -04:00
feat(mcp): Add error handling & Event Bus design
Database Tools: - Wrapped all queries in try/catch to prevent server crashes - Errors returned as JSON instead of crashing - Logged to araxia.mcp for debugging AMS Bridge Fixes: - Fixed AMS.RegisterHandler (dot not colon) - Fixed JSON parsing for MCP messages - Disabled auto-polling by default Event Bus Design (EVENT_BUS_DESIGN.md): - Unified pub/sub for C++ Core, Eluna, MCP, AMS - Real-time event streaming (no polling) - Enables MCP to see player targets, spawns, errors - Phased implementation plan Roadmap Updated: - Phase 3: Content Creator Commands (non-GM) - Phase 6: Event Bus implementation
This commit is contained in:
143
araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md
Normal file
143
araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Araxia Event Bus Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
A unified internal event bus that connects all major subsystems:
|
||||
- **C++ Core** (TrinityCore)
|
||||
- **Eluna** (Lua scripting)
|
||||
- **MCP Server** (AI assistant)
|
||||
- **AMS** (client addon communication)
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Any component can publish events** - DB changes, player actions, errors, etc.
|
||||
2. **Any component can subscribe** - MCP sees player targets, Eluna sees MCP commands
|
||||
3. **Decoupled architecture** - Components don't need to know about each other
|
||||
4. **Audit trail** - All events logged for debugging
|
||||
5. **Real-time** - Low latency for interactive debugging
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ EVENT BUS (C++) │
|
||||
│ │
|
||||
│ EventBus::Publish("player.target.changed", {guid, name, entry}) │
|
||||
│ EventBus::Subscribe("player.*", callback) │
|
||||
│ │
|
||||
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────┤
|
||||
│ C++ Core │ Eluna │ MCP │ AMS │ Logging │
|
||||
│ │ │ │ │ │
|
||||
│ Publishes: │ Publishes: │ Publishes: │ Publishes: │ Subscribes: │
|
||||
│ - DB errors │ - Script │ - AI cmds │ - Client │ - All events│
|
||||
│ - Spawns │ events │ - Queries │ messages │ │
|
||||
│ - Combat │ - Handlers │ │ - Target │ │
|
||||
│ │ │ │ changes │ │
|
||||
│ Subscribes: │ Subscribes: │ Subscribes: │ Subscribes: │ │
|
||||
│ - MCP cmds │ - All │ - All │ - MCP msgs │ │
|
||||
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
## Event Categories
|
||||
|
||||
### Player Events
|
||||
- `player.login` / `player.logout`
|
||||
- `player.target.changed` - **Key for MCP to see your target!**
|
||||
- `player.position.changed`
|
||||
- `player.command` - GM commands executed
|
||||
|
||||
### Creature Events
|
||||
- `creature.spawn` / `creature.despawn`
|
||||
- `creature.property.changed` - Wander distance, movement type, etc.
|
||||
- `creature.combat.start` / `creature.combat.end`
|
||||
|
||||
### Database Events
|
||||
- `db.query.start` / `db.query.complete` / `db.query.error`
|
||||
- `db.execute.start` / `db.execute.complete`
|
||||
|
||||
### MCP Events
|
||||
- `mcp.tool.called` - AI called a tool
|
||||
- `mcp.message.sent` - AI sent message to client
|
||||
- `mcp.error` - MCP operation failed
|
||||
|
||||
### AMS Events
|
||||
- `ams.message.received` - Client sent message
|
||||
- `ams.message.sent` - Server sent to client
|
||||
- `ams.handler.error` - Handler failed
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
```cpp
|
||||
// EventBus.h
|
||||
class EventBus {
|
||||
public:
|
||||
static void Publish(const std::string& event, const json& data);
|
||||
static void Subscribe(const std::string& pattern, EventCallback callback);
|
||||
static void Unsubscribe(const std::string& pattern);
|
||||
|
||||
// Store recent events for MCP to query
|
||||
static std::vector<Event> GetRecentEvents(size_t count = 100);
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 2: Eluna Integration
|
||||
```lua
|
||||
-- Lua API
|
||||
EventBus.Publish("custom.event", {data = "value"})
|
||||
EventBus.Subscribe("player.*", function(event, data)
|
||||
print("Player event:", event, data.name)
|
||||
end)
|
||||
```
|
||||
|
||||
### Phase 3: MCP Integration
|
||||
New MCP tools:
|
||||
- `event_subscribe` - Subscribe to event patterns
|
||||
- `event_history` - Get recent events
|
||||
- `event_publish` - Publish an event
|
||||
|
||||
### Phase 4: Auto-Publishing
|
||||
Hook into TrinityCore to auto-publish events:
|
||||
- Player target changes → `player.target.changed`
|
||||
- Creature spawns → `creature.spawn`
|
||||
- DB errors → `db.error`
|
||||
|
||||
## Example: MCP Sees Player Target
|
||||
|
||||
With the event bus:
|
||||
|
||||
```
|
||||
Player targets Scarlet Sentry
|
||||
↓
|
||||
C++ Core publishes "player.target.changed"
|
||||
↓
|
||||
Event Bus routes to all subscribers
|
||||
↓
|
||||
MCP subscriber stores in ElunaSharedData
|
||||
↓
|
||||
AI calls shared_data_read("mcp_current_target")
|
||||
↓
|
||||
AI sees: {name: "Scarlet Sentry", entry: 4283, guid: "..."}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **No polling** - Events pushed in real-time
|
||||
2. **Flexible** - Add new event types without changing core
|
||||
3. **Debuggable** - Full event history for troubleshooting
|
||||
4. **Extensible** - Easy to add new subscribers/publishers
|
||||
5. **Unified** - One system instead of multiple ad-hoc bridges
|
||||
|
||||
## Storage
|
||||
|
||||
Events stored in `ElunaSharedData` for cross-component access:
|
||||
- `event_bus_history` - Recent events (ring buffer, JSON array)
|
||||
- `event_bus_subscriptions` - Active subscriptions
|
||||
- `mcp_current_target` - Special: current player target for MCP
|
||||
|
||||
## Security
|
||||
|
||||
- Event data sanitized before storage
|
||||
- Subscription patterns validated
|
||||
- Rate limiting on publish
|
||||
- Audit log of all events
|
||||
@@ -235,9 +235,41 @@ MCP Tool → ElunaSharedData → Server Lua → AMS.Send() → Client Addon
|
||||
|-------|---------|--------|
|
||||
| 1 | Database tools, server info | ✅ Complete |
|
||||
| 2 | Shared data bridge (ElunaSharedData) | ✅ Complete |
|
||||
| 3 | Eluna integration (lua_eval, hot-reload) | ⏳ Planned |
|
||||
| 3 | Content Creator Commands (via AMS, not GM-only) | ⏳ Next |
|
||||
| 4 | World object tools (creatures, GOs) | ⏳ Planned |
|
||||
| 5 | Event streaming (logs, world events) | ⏳ Planned |
|
||||
| 5 | Eluna integration (lua_eval, hot-reload) | ⏳ Planned |
|
||||
| 6 | **Event Bus** (unified pub/sub for all systems) | ⏳ Planned |
|
||||
|
||||
### Event Bus (See EVENT_BUS_DESIGN.md)
|
||||
|
||||
Unified internal event system connecting C++ Core, Eluna, MCP, and AMS:
|
||||
- Any component can publish/subscribe to events
|
||||
- Real-time event streaming (no polling)
|
||||
- Full event history for debugging
|
||||
- Enables MCP to see player target changes, spawns, errors, etc.
|
||||
|
||||
### Phase 3: Content Creator Commands
|
||||
|
||||
**Goal:** Allow any player with AraxiaTrinityAdmin addon to execute server commands without GM status.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
Client Addon → AMS → Server Lua → Eluna API (bypasses GM check)
|
||||
```
|
||||
|
||||
**Planned Commands:**
|
||||
- `target <name>` - Target creature by name/entry
|
||||
- `teleport <x,y,z>` - Teleport player
|
||||
- `spawn <entry>` - Spawn creature/GO
|
||||
- `modify <property>` - Modify targeted creature
|
||||
- `waypoint` - Waypoint management
|
||||
- `respawn` - Force respawn targeted creature
|
||||
|
||||
**Security:**
|
||||
- Server-side validation of addon presence
|
||||
- Configurable permission system (whitelist accounts/players)
|
||||
- All actions logged for audit
|
||||
- No access to actual GM commands (ban, kick, etc.)
|
||||
|
||||
## Key Learnings
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ local function RegisterAMSHandler()
|
||||
return
|
||||
end
|
||||
|
||||
AMS:RegisterHandler("MCP_MESSAGES_RESPONSE", function(data)
|
||||
AMS.RegisterHandler("MCP_MESSAGES_RESPONSE", function(data)
|
||||
MCPBridge:OnMCPMessagesReceived(data)
|
||||
end)
|
||||
|
||||
|
||||
@@ -76,14 +76,28 @@ end
|
||||
-- AMS Handler: Client requests any pending MCP messages
|
||||
local function HandleMCPGetMessages(player, data)
|
||||
local messagesData = GetSharedData("mcp_to_client")
|
||||
local messages = messagesData and Smallfolk.loads(messagesData) or {}
|
||||
|
||||
-- MCP writes plain JSON, not Smallfolk format
|
||||
-- Just send the raw string - client can parse or display it
|
||||
local messages = {}
|
||||
|
||||
if messagesData and messagesData ~= "" and messagesData ~= "{}" and messagesData ~= "[]" then
|
||||
-- Try to extract simple messages from JSON-like format
|
||||
-- Format: [{"message":"text"},{"message":"text2"}]
|
||||
for msg in messagesData:gmatch('"message":"([^"]*)"') do
|
||||
table.insert(messages, {message = msg})
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear after reading
|
||||
SetSharedData("mcp_to_client", Smallfolk.dumps({}))
|
||||
SetSharedData("mcp_to_client", "")
|
||||
|
||||
AMS.Send(player, "MCP_MESSAGES_RESPONSE", {
|
||||
messages = messages
|
||||
})
|
||||
if #messages > 0 then
|
||||
AMS.Send(player, "MCP_MESSAGES_RESPONSE", {
|
||||
messages = messages
|
||||
})
|
||||
print("[MCP Bridge] Sent " .. #messages .. " messages to client")
|
||||
end
|
||||
end
|
||||
|
||||
-- AMS Handler: Client sends DB query result or other structured data
|
||||
|
||||
@@ -112,22 +112,45 @@ void RegisterDatabaseTools()
|
||||
|
||||
TC_LOG_DEBUG("araxia.mcp", "[MCP] db_query on %s: %s", database.c_str(), query.c_str());
|
||||
|
||||
QueryResult result;
|
||||
if (database == "world")
|
||||
result = WorldDatabase.Query(query.c_str());
|
||||
else if (database == "characters")
|
||||
result = CharacterDatabase.Query(query.c_str());
|
||||
else if (database == "auth")
|
||||
result = LoginDatabase.Query(query.c_str());
|
||||
else
|
||||
return {{"success", false}, {"error", "Unknown database: " + database}};
|
||||
|
||||
json data = QueryResultToJson(result);
|
||||
data["success"] = true;
|
||||
data["database"] = database;
|
||||
data["query"] = query;
|
||||
|
||||
return data;
|
||||
try
|
||||
{
|
||||
QueryResult result;
|
||||
if (database == "world")
|
||||
result = WorldDatabase.Query(query.c_str());
|
||||
else if (database == "characters")
|
||||
result = CharacterDatabase.Query(query.c_str());
|
||||
else if (database == "auth")
|
||||
result = LoginDatabase.Query(query.c_str());
|
||||
else
|
||||
return {{"success", false}, {"error", "Unknown database: " + database}};
|
||||
|
||||
json data = QueryResultToJson(result);
|
||||
data["success"] = true;
|
||||
data["database"] = database;
|
||||
data["query"] = query;
|
||||
|
||||
return data;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
TC_LOG_ERROR("araxia.mcp", "[MCP] db_query EXCEPTION: %s", e.what());
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", std::string("Query exception: ") + e.what()},
|
||||
{"database", database},
|
||||
{"query", query}
|
||||
};
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
TC_LOG_ERROR("araxia.mcp", "[MCP] db_query UNKNOWN EXCEPTION");
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "Unknown query exception - check server logs"},
|
||||
{"database", database},
|
||||
{"query", query}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -226,33 +249,44 @@ void RegisterDatabaseTools()
|
||||
[](const json& params) -> json {
|
||||
std::string database = params.value("database", "world");
|
||||
|
||||
QueryResult result;
|
||||
if (database == "world")
|
||||
result = WorldDatabase.Query("SHOW TABLES");
|
||||
else if (database == "characters")
|
||||
result = CharacterDatabase.Query("SHOW TABLES");
|
||||
else if (database == "auth")
|
||||
result = LoginDatabase.Query("SHOW TABLES");
|
||||
else
|
||||
return {{"success", false}, {"error", "Unknown database: " + database}};
|
||||
|
||||
json tables = json::array();
|
||||
if (result)
|
||||
try
|
||||
{
|
||||
do
|
||||
QueryResult result;
|
||||
if (database == "world")
|
||||
result = WorldDatabase.Query("SHOW TABLES");
|
||||
else if (database == "characters")
|
||||
result = CharacterDatabase.Query("SHOW TABLES");
|
||||
else if (database == "auth")
|
||||
result = LoginDatabase.Query("SHOW TABLES");
|
||||
else
|
||||
return {{"success", false}, {"error", "Unknown database: " + database}};
|
||||
|
||||
json tables = json::array();
|
||||
if (result)
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
tables.push_back(fields[0].GetString());
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
tables.push_back(fields[0].GetString());
|
||||
}
|
||||
while (result->NextRow());
|
||||
}
|
||||
while (result->NextRow());
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"database", database},
|
||||
{"tables", tables},
|
||||
{"count", tables.size()}
|
||||
};
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
return {{"success", false}, {"error", std::string("Exception: ") + e.what()}};
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {{"success", false}, {"error", "Unknown exception in db_tables"}};
|
||||
}
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"database", database},
|
||||
{"tables", tables},
|
||||
{"count", tables.size()}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -288,22 +322,33 @@ void RegisterDatabaseTools()
|
||||
|
||||
std::string query = "DESCRIBE " + table;
|
||||
|
||||
QueryResult result;
|
||||
if (database == "world")
|
||||
result = WorldDatabase.Query(query.c_str());
|
||||
else if (database == "characters")
|
||||
result = CharacterDatabase.Query(query.c_str());
|
||||
else if (database == "auth")
|
||||
result = LoginDatabase.Query(query.c_str());
|
||||
else
|
||||
return {{"success", false}, {"error", "Unknown database: " + database}};
|
||||
|
||||
json data = QueryResultToJson(result);
|
||||
data["success"] = true;
|
||||
data["database"] = database;
|
||||
data["table"] = table;
|
||||
|
||||
return data;
|
||||
try
|
||||
{
|
||||
QueryResult result;
|
||||
if (database == "world")
|
||||
result = WorldDatabase.Query(query.c_str());
|
||||
else if (database == "characters")
|
||||
result = CharacterDatabase.Query(query.c_str());
|
||||
else if (database == "auth")
|
||||
result = LoginDatabase.Query(query.c_str());
|
||||
else
|
||||
return {{"success", false}, {"error", "Unknown database: " + database}};
|
||||
|
||||
json data = QueryResultToJson(result);
|
||||
data["success"] = true;
|
||||
data["database"] = database;
|
||||
data["table"] = table;
|
||||
|
||||
return data;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
return {{"success", false}, {"error", std::string("Exception: ") + e.what()}, {"table", table}};
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {{"success", false}, {"error", "Unknown exception in db_describe"}, {"table", table}};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user