feat(mcp): Complete Phase 2 - AMS Bridge & Direct Windsurf Connection!

HUGE MILESTONE: AI assistant now has direct real-time access to worldserver!

Phase 1 - Database & Server Tools:
- db_query, db_execute, db_tables, db_describe
- server_info, player_list, gm_command, reload_scripts

Phase 2 - Shared Data Bridge:
- shared_data_read, shared_data_write, shared_data_keys
- ElunaSharedData integration for cross-state communication
- Full client  MCP bidirectional data flow

AMS Bridge Implementation:
- Server: mcp_bridge.lua handles MCP_CHAT, MCP_CLIENT_LOG, MCP_GET_MESSAGES
- Client: MCPBridge.lua with /mcpbridge commands
- Data stored in ElunaSharedData, readable by MCP tools

Key Learnings Documented:
- nlohmann/json: Don't use value() with nullptr
- TrinityCore: Use GetBoolDefault() not GetOption<T>()
- AMS: Use AMS.Send (dot) not AMS:Send (colon) on client
- Smallfolk: Never serialize functions

Windsurf Integration:
- Add to ~/.codeium/windsurf/mcp_config.json
- AI gets direct access to all MCP tools
- No more manual curl commands!

This is SOOO FUCKING COOL!!!
This commit is contained in:
2025-11-30 19:57:29 -05:00
parent ff78ae203b
commit ec14aa5545
4 changed files with 490 additions and 20 deletions

View File

@@ -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<T>()` 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

View File

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

View File

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

View File

@@ -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<std::string> 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