From e5ee53a1d8d501d14ffca6260b5837cd1db74524 Mon Sep 17 00:00:00 2001 From: James Huston Date: Sun, 30 Nov 2025 20:11:53 -0500 Subject: [PATCH] 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 --- araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md | 143 ++++++++++++++++ araxiaonline/araxia_docs/MCP_SERVER.md | 36 +++- .../AraxiaTrinityAdmin/MCPBridge.lua | 2 +- araxiaonline/lua_scripts/mcp_bridge.lua | 24 ++- src/araxiaonline/mcp/DatabaseTools.cpp | 155 +++++++++++------- 5 files changed, 297 insertions(+), 63 deletions(-) create mode 100644 araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md diff --git a/araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md b/araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md new file mode 100644 index 0000000000..dc75c5c870 --- /dev/null +++ b/araxiaonline/araxia_docs/EVENT_BUS_DESIGN.md @@ -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 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 diff --git a/araxiaonline/araxia_docs/MCP_SERVER.md b/araxiaonline/araxia_docs/MCP_SERVER.md index 0ae682c072..93eb640bdf 100644 --- a/araxiaonline/araxia_docs/MCP_SERVER.md +++ b/araxiaonline/araxia_docs/MCP_SERVER.md @@ -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 ` - Target creature by name/entry +- `teleport ` - Teleport player +- `spawn ` - Spawn creature/GO +- `modify ` - 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 diff --git a/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua b/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua index f27a2f55c3..76264b2396 100644 --- a/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua +++ b/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua @@ -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) diff --git a/araxiaonline/lua_scripts/mcp_bridge.lua b/araxiaonline/lua_scripts/mcp_bridge.lua index f10c2faa96..64d6e3227d 100644 --- a/araxiaonline/lua_scripts/mcp_bridge.lua +++ b/araxiaonline/lua_scripts/mcp_bridge.lua @@ -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 diff --git a/src/araxiaonline/mcp/DatabaseTools.cpp b/src/araxiaonline/mcp/DatabaseTools.cpp index 3e95c4f1e6..62d5ab9c08 100644 --- a/src/araxiaonline/mcp/DatabaseTools.cpp +++ b/src/araxiaonline/mcp/DatabaseTools.cpp @@ -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}}; + } } );