diff --git a/araxiaonline/araxia_docs/MCP_SERVER.md b/araxiaonline/araxia_docs/MCP_SERVER.md index 3599989e86..0ae682c072 100644 --- a/araxiaonline/araxia_docs/MCP_SERVER.md +++ b/araxiaonline/araxia_docs/MCP_SERVER.md @@ -4,15 +4,25 @@ The Araxia MCP Server embeds a Model Context Protocol server directly into the worldserver, enabling AI assistants (like Claude/Cascade) to interact with the game server in real-time. -**Status:** ✅ Phase 1 Complete (Nov 30, 2025) +**Status:** ✅ Phase 1 & 2 Complete (Nov 30, 2025) + +## 🎉 What This Enables + +With this integration, the AI assistant can: +- **Query databases directly** - No more asking you to run SQL commands +- **See online players** - Know who's logged in and where they are +- **Read client messages** - Receive data from the WoW client via AMS bridge +- **Write to client** - Send messages that display in the WoW client +- **Debug in real-time** - Direct access to server state while you're playing ## Features - **Database Access**: ✅ Direct SQL queries to world, characters, and auth databases - **Server Status**: ✅ Real-time server info, player lists, uptime +- **Shared Data Bridge**: ✅ Read/write ElunaSharedData (client ↔ MCP communication) - **GM Commands**: ✅ Stub (needs ChatHandler integration) -- **Eluna Integration**: ⏳ (Phase 2) Execute Lua, inspect state, hot-reload -- **AMS Bridge**: ⏳ (Phase 4) Communicate with client addons +- **Eluna Integration**: ⏳ (Phase 3) Execute Lua, inspect state, hot-reload +- **World Object Tools**: ⏳ (Phase 4) Creature/GO manipulation ## Configuration @@ -167,12 +177,83 @@ src/araxiaonline/mcp/ - `World.cpp` - `sMCPServer->Shutdown()` in destructor - `CMakeLists.txt` - Auto-collected via `CollectSourceFiles` +## Windsurf MCP Configuration + +To connect Cascade/Claude directly to the worldserver, add to `~/.codeium/windsurf/mcp_config.json`: + +```json +{ + "mcpServers": { + "araxia-worldserver": { + "url": "http://localhost:8765/mcp", + "transport": "http" + } + } +} +``` + +After adding, restart Windsurf. The AI will have direct access to all MCP tools. + +## AMS Bridge (Client ↔ MCP) + +The AMS bridge enables bidirectional communication between the WoW client and the MCP server. + +### Data Flow +``` +Client Addon → AMS.Send() → Server Lua → ElunaSharedData → MCP Tool +MCP Tool → ElunaSharedData → Server Lua → AMS.Send() → Client Addon +``` + +### Client Side (`AraxiaTrinityAdmin/MCPBridge.lua`) +- Captures chat messages and sends to server +- Provides `/mcpbridge` commands for control +- Polls for messages from MCP (disabled by default) + +### Server Side (`lua_scripts/mcp_bridge.lua`) +- Receives client messages via AMS handlers +- Stores in ElunaSharedData for MCP to read +- Reads MCP messages and sends to client + +### Shared Data Keys +| Key | Purpose | +|-----|---------| +| `mcp_client_chat` | Chat messages from client | +| `mcp_client_logs` | Log/error messages from client | +| `mcp_to_client` | Messages from MCP to display on client | + +### Client Commands +``` +/mcpbridge status - Show bridge status +/mcpbridge test - Send test message to MCP +/mcpbridge on/off - Enable/disable bridge +/mcpbridge poll - Start polling for MCP messages +``` + ## Roadmap | Phase | Feature | Status | |-------|---------|--------| | 1 | Database tools, server info | ✅ Complete | -| 2 | Eluna integration (lua_eval, shared_data) | ⏳ Planned | -| 3 | World object tools (creatures, GOs) | ⏳ Planned | -| 4 | AMS bridge (client addon communication) | ⏳ Planned | +| 2 | Shared data bridge (ElunaSharedData) | ✅ Complete | +| 3 | Eluna integration (lua_eval, hot-reload) | ⏳ Planned | +| 4 | World object tools (creatures, GOs) | ⏳ Planned | | 5 | Event streaming (logs, world events) | ⏳ Planned | + +## Key Learnings + +### nlohmann/json Gotchas +- **Never use `request.value("id", nullptr)`** - causes type errors +- Use `request.contains("id") ? request["id"] : json(nullptr)` instead + +### TrinityCore ConfigMgr API +- Use `sConfigMgr->GetBoolDefault()`, `GetIntDefault()`, `GetStringDefault()` +- NOT `GetOption()` which doesn't exist + +### ElunaSharedData API +- Singleton pattern: `sElunaSharedData->Get()`, `Set()`, `GetKeys()` +- Lua API: `SetSharedData()`, `GetSharedData()`, `HasSharedData()` + +### AMS Client/Server Pattern +- Client: `AMS.Send("HANDLER_NAME", data)` (dot notation, NOT colon!) +- Server: `AMS.Send(player, "HANDLER_NAME", data)` +- Never serialize functions - Smallfolk will error diff --git a/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua b/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua new file mode 100644 index 0000000000..f27a2f55c3 --- /dev/null +++ b/araxiaonline/client_addons/AraxiaTrinityAdmin/MCPBridge.lua @@ -0,0 +1,181 @@ +--[[ + MCP Bridge - Client Side + + Captures client-side events and sends them to the server via AMS + for the MCP server to read. Also displays messages from MCP. +]] + +local addonName, addon = ... + +-- Create the MCP Bridge module +addon.MCPBridge = addon.MCPBridge or {} +local MCPBridge = addon.MCPBridge + +-- Configuration +MCPBridge.enabled = true +MCPBridge.captureChat = true +MCPBridge.captureErrors = true +MCPBridge.pollInterval = 5 -- seconds between MCP message polls + +-- Chat types to capture +MCPBridge.chatTypes = { + ["CHAT_MSG_SAY"] = "SAY", + ["CHAT_MSG_YELL"] = "YELL", + ["CHAT_MSG_PARTY"] = "PARTY", + ["CHAT_MSG_GUILD"] = "GUILD", + ["CHAT_MSG_WHISPER"] = "WHISPER", + ["CHAT_MSG_CHANNEL"] = "CHANNEL", + ["CHAT_MSG_SYSTEM"] = "SYSTEM", +} + +-- Send chat message to server for MCP +function MCPBridge:SendChatToMCP(message, chatType) + if not self.enabled or not self.captureChat then return end + if not AMS then return end + + AMS.Send("MCP_CHAT", { + message = message, + chatType = chatType or "CHAT" + }) +end + +-- Send client log/error to server for MCP +function MCPBridge:SendLogToMCP(message, level, source) + if not self.enabled or not self.captureErrors then return end + if not AMS then return end + + AMS.Send("MCP_CLIENT_LOG", { + message = message, + level = level or "INFO", + source = source or "AraxiaTrinityAdmin" + }) +end + +-- Send structured data to server for MCP +function MCPBridge:SendDataToMCP(dataType, payload) + if not self.enabled then return end + if not AMS then return end + + AMS.Send("MCP_CLIENT_DATA", { + dataType = dataType, + payload = payload + }) +end + +-- Request pending messages from MCP +function MCPBridge:PollMCPMessages() + if not self.enabled then return end + if not AMS then return end + + AMS.Send("MCP_GET_MESSAGES", {}) +end + +-- Handle messages received from MCP +function MCPBridge:OnMCPMessagesReceived(data) + local messages = data.messages or {} + + for _, msg in ipairs(messages) do + -- Display MCP messages in chat + print("|cFF00FF00[MCP]|r " .. (msg.message or tostring(msg))) + end +end + +-- Register AMS handler for MCP messages +local function RegisterAMSHandler() + if not AMS then + C_Timer.After(1, RegisterAMSHandler) -- Retry + return + end + + AMS:RegisterHandler("MCP_MESSAGES_RESPONSE", function(data) + MCPBridge:OnMCPMessagesReceived(data) + end) + + print("|cFF00FF00[MCP Bridge]|r Client-side bridge ready") +end + +-- Chat event handler +local chatFrame = CreateFrame("Frame") +chatFrame:RegisterEvent("CHAT_MSG_SAY") +chatFrame:RegisterEvent("CHAT_MSG_YELL") +chatFrame:RegisterEvent("CHAT_MSG_PARTY") +chatFrame:RegisterEvent("CHAT_MSG_GUILD") +chatFrame:RegisterEvent("CHAT_MSG_SYSTEM") + +chatFrame:SetScript("OnEvent", function(self, event, message, sender, ...) + local chatType = MCPBridge.chatTypes[event] + if chatType and MCPBridge.captureChat then + -- Only capture our own messages or system messages + local playerName = UnitName("player") + if sender == playerName or event == "CHAT_MSG_SYSTEM" then + MCPBridge:SendChatToMCP(message, chatType) + end + end +end) + +-- Error handler - capture Lua errors (disabled to prevent recursion) +-- The error handler can cause infinite loops if AMS serialization fails +-- We'll rely on explicit error logging instead +MCPBridge.captureErrors = false -- Disabled by default + +-- Poll timer for MCP messages +local pollTimer +local function StartPolling() + if pollTimer then return end + pollTimer = C_Timer.NewTicker(MCPBridge.pollInterval, function() + MCPBridge:PollMCPMessages() + end) +end + +-- Slash command to toggle MCP bridge +SLASH_MCPBRIDGE1 = "/mcpbridge" +SlashCmdList["MCPBRIDGE"] = function(msg) + local cmd = msg:lower():trim() + + if cmd == "on" then + MCPBridge.enabled = true + print("|cFF00FF00[MCP Bridge]|r Enabled") + elseif cmd == "off" then + MCPBridge.enabled = false + print("|cFFFF0000[MCP Bridge]|r Disabled") + elseif cmd == "chat on" then + MCPBridge.captureChat = true + print("|cFF00FF00[MCP Bridge]|r Chat capture enabled") + elseif cmd == "chat off" then + MCPBridge.captureChat = false + print("|cFFFF0000[MCP Bridge]|r Chat capture disabled") + elseif cmd == "status" then + print("|cFF00FFFF[MCP Bridge Status]|r") + print(" Enabled: " .. (MCPBridge.enabled and "Yes" or "No")) + print(" Chat Capture: " .. (MCPBridge.captureChat and "Yes" or "No")) + print(" Error Capture: " .. (MCPBridge.captureErrors and "Yes" or "No")) + elseif cmd == "test" then + -- Send directly, bypassing captureErrors check + if AMS then + AMS.Send("MCP_CLIENT_LOG", { + message = "Test message from client @ " .. date("%H:%M:%S"), + level = "INFO", + source = "MCPBridge Test" + }) + print("|cFF00FF00[MCP Bridge]|r Test message sent to server") + else + print("|cFFFF0000[MCP Bridge]|r AMS not available") + end + elseif cmd == "poll" then + StartPolling() + print("|cFF00FF00[MCP Bridge]|r Polling started") + else + print("|cFF00FFFF[MCP Bridge Commands]|r") + print(" /mcpbridge on|off - Enable/disable bridge") + print(" /mcpbridge chat on|off - Toggle chat capture") + print(" /mcpbridge status - Show status") + print(" /mcpbridge test - Send test message") + end +end + +-- Initialize +C_Timer.After(2, function() + RegisterAMSHandler() + -- Polling disabled by default - use /mcpbridge poll to enable + -- StartPolling() +end) diff --git a/araxiaonline/lua_scripts/mcp_bridge.lua b/araxiaonline/lua_scripts/mcp_bridge.lua new file mode 100644 index 0000000000..f10c2faa96 --- /dev/null +++ b/araxiaonline/lua_scripts/mcp_bridge.lua @@ -0,0 +1,126 @@ +--[[ + MCP Bridge - Server Side + + Provides a communication bridge between the WoW client and the MCP server. + Client sends messages via AMS, server stores them in ElunaSharedData, + MCP tools can read/write to ElunaSharedData. + + Keys used: + - mcp_client_chat: Last N chat messages from client + - mcp_client_logs: Client-side addon errors/logs + - mcp_to_client: Messages from MCP to display on client +]] + +local Smallfolk = require("Smallfolk") + +-- Configuration +local MAX_CHAT_MESSAGES = 50 +local MAX_LOG_MESSAGES = 100 + +-- Initialize storage keys if not present +local function InitSharedData() + if not HasSharedData("mcp_client_chat") then + SetSharedData("mcp_client_chat", Smallfolk.dumps({})) + end + if not HasSharedData("mcp_client_logs") then + SetSharedData("mcp_client_logs", Smallfolk.dumps({})) + end + if not HasSharedData("mcp_to_client") then + SetSharedData("mcp_to_client", Smallfolk.dumps({})) + end +end + +-- Add a message to a shared data list (FIFO) +local function AddToSharedList(key, message, maxItems) + local data = GetSharedData(key) + local list = data and Smallfolk.loads(data) or {} + + table.insert(list, { + timestamp = os.time(), + message = message + }) + + -- Trim to max size + while #list > maxItems do + table.remove(list, 1) + end + + SetSharedData(key, Smallfolk.dumps(list)) +end + +-- AMS Handler: Client sends chat message for MCP to see +local function HandleMCPChatMessage(player, data) + local playerName = player:GetName() + local message = data.message or "" + local chatType = data.chatType or "CHAT" + + local formatted = string.format("[%s] %s: %s", chatType, playerName, message) + AddToSharedList("mcp_client_chat", formatted, MAX_CHAT_MESSAGES) + + print("[MCP Bridge] Chat captured: " .. formatted) +end + +-- AMS Handler: Client sends log/error for MCP to see +local function HandleMCPClientLog(player, data) + local playerName = player:GetName() + local logLevel = data.level or "INFO" + local message = data.message or "" + local source = data.source or "unknown" + + local formatted = string.format("[%s] [%s] %s: %s", logLevel, source, playerName, message) + AddToSharedList("mcp_client_logs", formatted, MAX_LOG_MESSAGES) + + print("[MCP Bridge] Client log: " .. formatted) +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 {} + + -- Clear after reading + SetSharedData("mcp_to_client", Smallfolk.dumps({})) + + AMS.Send(player, "MCP_MESSAGES_RESPONSE", { + messages = messages + }) +end + +-- AMS Handler: Client sends DB query result or other structured data +local function HandleMCPClientData(player, data) + local playerName = player:GetName() + local dataType = data.dataType or "generic" + local payload = data.payload or {} + + -- Store in a type-specific key + local key = "mcp_client_" .. dataType + SetSharedData(key, Smallfolk.dumps({ + player = playerName, + timestamp = os.time(), + data = payload + })) + + print("[MCP Bridge] Client data received: " .. dataType) +end + +-- Register AMS handlers +local function RegisterAMSHandlers() + -- Check if AMS is available + if not AMS or not AMS.RegisterHandler then + print("[MCP Bridge] Warning: AMS not available, skipping handler registration") + return + end + + AMS.RegisterHandler("MCP_CHAT", HandleMCPChatMessage) + AMS.RegisterHandler("MCP_CLIENT_LOG", HandleMCPClientLog) + AMS.RegisterHandler("MCP_GET_MESSAGES", HandleMCPGetMessages) + AMS.RegisterHandler("MCP_CLIENT_DATA", HandleMCPClientData) + + print("[MCP Bridge] AMS handlers registered") +end + +-- Initialize on load +InitSharedData() +RegisterAMSHandlers() + +print("[MCP Bridge] Server-side bridge loaded") diff --git a/src/araxiaonline/mcp/ServerTools.cpp b/src/araxiaonline/mcp/ServerTools.cpp index 23d4b65bc3..203f4e30a1 100644 --- a/src/araxiaonline/mcp/ServerTools.cpp +++ b/src/araxiaonline/mcp/ServerTools.cpp @@ -13,6 +13,7 @@ #include "Log.h" #include "GitRevision.h" #include "GameTime.h" +#include "LuaEngine/ElunaSharedData.h" namespace Araxia { @@ -209,27 +210,108 @@ void RegisterServerTools() }, [](const json& params) -> json { std::string pattern = params.value("pattern", ""); - int maxLines = params.value("lines", 50); - std::string logType = params.value("logType", "server"); - // For now, return info about where logs are + // This is a placeholder - actual log search would read ElunaSharedData + // where the client/server Lua code writes log messages return { {"success", true}, - {"message", "Log search requires file access. Logs are typically in /opt/trinitycore/logs/"}, - {"logFiles", { - {"server", "Server.log"}, - {"eluna", "Eluna.log"}, - {"dberrors", "DBErrors.log"}, - {"gm", "GM.log"} - }}, - {"pattern", pattern}, - {"maxLines", maxLines}, - {"note", "Use db_query on characters.gm_command_log for GM command history"} + {"message", "Use shared_data tool with key 'mcp_logs' to read logs pushed by client/server"}, + {"pattern", pattern} }; } ); - TC_LOG_INFO("araxia.mcp", "[MCP] Server tools registered (server_info, player_list, gm_command, reload_scripts, log_search)"); + // shared_data_read - Read from ElunaSharedData (AMS bridge) + sMCPServer->RegisterTool( + "shared_data_read", + "Read data from ElunaSharedData. This is the bridge for client/server Lua communication.", + { + {"type", "object"}, + {"properties", { + {"key", { + {"type", "string"}, + {"description", "The shared data key to read (e.g., 'mcp_logs', 'mcp_client_chat')"} + }} + }}, + {"required", {"key"}} + }, + [](const json& params) -> json { + std::string key = params.value("key", ""); + + if (key.empty()) + return {{"success", false}, {"error", "Key is required"}}; + + std::string value; + bool exists = sElunaSharedData->Get(key, value); + + return { + {"success", true}, + {"key", key}, + {"exists", exists}, + {"value", exists ? value : ""} + }; + } + ); + + // shared_data_write - Write to ElunaSharedData + sMCPServer->RegisterTool( + "shared_data_write", + "Write data to ElunaSharedData. Lua scripts can read this.", + { + {"type", "object"}, + {"properties", { + {"key", { + {"type", "string"}, + {"description", "The shared data key to write"} + }}, + {"value", { + {"type", "string"}, + {"description", "The value to store (use JSON string for complex data)"} + }} + }}, + {"required", {"key", "value"}} + }, + [](const json& params) -> json { + std::string key = params.value("key", ""); + std::string value = params.value("value", ""); + + if (key.empty()) + return {{"success", false}, {"error", "Key is required"}}; + + sElunaSharedData->Set(key, value); + + return { + {"success", true}, + {"key", key}, + {"message", "Data written successfully"} + }; + } + ); + + // shared_data_keys - List all shared data keys + sMCPServer->RegisterTool( + "shared_data_keys", + "List all keys in ElunaSharedData.", + { + {"type", "object"}, + {"properties", json::object()} + }, + [](const json& /*params*/) -> json { + std::vector keys = sElunaSharedData->GetKeys(); + + json keysJson = json::array(); + for (const auto& k : keys) + keysJson.push_back(k); + + return { + {"success", true}, + {"keys", keysJson}, + {"count", keys.size()} + }; + } + ); + + TC_LOG_INFO("araxia.mcp", "[MCP] Server tools registered (server_info, player_list, gm_command, reload_scripts, log_search, shared_data_*)"); } } // namespace Araxia