rojo/plugin/src/McpStream.lua
Joe 3fd50df0b2 Update logging and enhance .gitignore for Atlas directory
- Introduced a logging system using `tracing` and related crates to improve log management and output formatting.
- Updated `.gitignore` to include the `.atlas/` directory, ensuring that log files and other generated content are not tracked by Git.
- Enhanced project templates to include the new `.gitignore` settings for Atlas in various project types.

These changes aim to improve logging capabilities and maintain a cleaner repository by ignoring unnecessary files.
2026-03-01 13:16:00 -06:00

237 lines
5.7 KiB
Lua

local HttpService = game:GetService("HttpService")
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Log = require(Packages.Log)
local Config = require(script.Parent.Config)
local ConsoleOutput = require(script.Parent.McpTools.Utils.ConsoleOutput)
local strict = require(script.Parent.strict)
local RECONNECT_INTERVAL = 5
local PASSTHROUGH_TOOLS = {
run_code = true,
insert_model = true,
get_console_output = true,
get_studio_mode = true,
start_stop_play = true,
run_script_in_play_mode = true,
}
local McpStream = {}
McpStream.__index = McpStream
function McpStream.new(options)
local self = setmetatable({}, McpStream)
self._onSyncCommand = options.onSyncCommand
self._onGetScriptCommand = options.onGetScriptCommand
self._onToolCommand = options.onToolCommand
self._getPluginConfig = options.getPluginConfig
self._wsClient = nil
self._running = false
self._loopThread = nil
return self
end
function McpStream:start()
if self._running then
return
end
self._running = true
self._loopThread = task.spawn(function()
while self._running do
self:_tryConnect()
if self._running then
task.wait(RECONNECT_INTERVAL)
end
end
end)
end
function McpStream:stop()
self._running = false
if self._loopThread then
task.cancel(self._loopThread)
self._loopThread = nil
end
self:_disconnect()
end
function McpStream:_disconnect()
ConsoleOutput.onLogMessage = nil
if self._wsClient then
pcall(function()
self._wsClient:Destroy()
end)
self._wsClient = nil
end
end
function McpStream:_tryConnect()
local host = Config.defaultHost
local port = Config.defaultPort
local url = ("ws://%s:%s/api/mcp/stream"):format(host, port)
local success, wsClient =
pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, { Url = url })
if not success then
return
end
self._wsClient = wsClient
Log.trace("MCP stream connected to {}", url)
local greeting = HttpService:JSONEncode({
type = "hello",
twoWaySync = self._getPluginConfig("twoWaySync"),
oneShotSync = self._getPluginConfig("oneShotSync"),
confirmationBehavior = self._getPluginConfig("confirmationBehavior"),
placeId = game.PlaceId,
})
pcall(function()
wsClient:Send(greeting)
end)
ConsoleOutput.onLogMessage = function(message: string, messageType: Enum.MessageType)
if not self._wsClient then
return
end
pcall(function()
self._wsClient:Send(HttpService:JSONEncode({
type = "roblox_log",
message = message,
level = messageType.Value,
}))
end)
end
local done = false
local closed, errored, received
received = wsClient.MessageReceived:Connect(function(msg)
local ok, data = pcall(HttpService.JSONDecode, HttpService, msg)
if not ok then
return
end
if data.type == "sync" and data.requestId then
Log.info(
"MCP stream: received sync command (requestId={}, mode={})",
data.requestId,
data.mode or "standard"
)
local resultPromise = self._onSyncCommand(data.requestId, data.mode or "standard", data.overrides or {})
resultPromise
:andThen(function(result)
if self._wsClient then
local json = HttpService:JSONEncode(result)
pcall(function()
self._wsClient:Send(json)
end)
end
end)
:catch(function(err)
Log.warn("MCP stream: sync command failed: {}", tostring(err))
if self._wsClient then
local errorResult = HttpService:JSONEncode({
requestId = data.requestId,
status = "error",
changes = {},
message = tostring(err),
})
pcall(function()
self._wsClient:Send(errorResult)
end)
end
end)
elseif data.type == "getScript" and data.requestId then
Log.info("MCP stream: received getScript command (requestId={})", data.requestId)
local resultPromise = self._onGetScriptCommand(data.requestId, data)
resultPromise
:andThen(function(result)
if self._wsClient then
local json = HttpService:JSONEncode(result)
pcall(function()
self._wsClient:Send(json)
end)
end
end)
:catch(function(err)
Log.warn("MCP stream: getScript command failed: {}", tostring(err))
if self._wsClient then
local errorResult = HttpService:JSONEncode({
requestId = data.requestId,
status = "error",
message = tostring(err),
})
pcall(function()
self._wsClient:Send(errorResult)
end)
end
end)
elseif data.type and data.requestId and PASSTHROUGH_TOOLS[data.type] then
Log.info("MCP stream: received {} command (requestId={})", data.type, data.requestId)
local resultPromise = self._onToolCommand(data.requestId, data.type, data.args or {})
resultPromise
:andThen(function(result)
if self._wsClient then
local json = HttpService:JSONEncode(result)
pcall(function()
self._wsClient:Send(json)
end)
end
end)
:catch(function(err)
Log.warn("MCP stream: {} command failed: {}", data.type, tostring(err))
if self._wsClient then
local errorResult = HttpService:JSONEncode({
requestId = data.requestId,
status = "error",
response = tostring(err),
})
pcall(function()
self._wsClient:Send(errorResult)
end)
end
end)
end
end)
closed = wsClient.Closed:Connect(function()
done = true
ConsoleOutput.onLogMessage = nil
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
Log.trace("MCP stream disconnected")
end)
errored = wsClient.Error:Connect(function(_code, _msg)
done = true
ConsoleOutput.onLogMessage = nil
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
end)
while not done and self._running do
task.wait(1)
end
self:_disconnect()
end
return strict("McpStream", {
new = McpStream.new,
})