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:
2025-11-30 20:11:53 -05:00
parent ec14aa5545
commit e5ee53a1d8
5 changed files with 297 additions and 63 deletions

View 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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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}};
}
}
);