rojo/plugin/src/McpStream.lua
Joe 0f5cabb22a Implement MCP integration for AI agents and enhance sync functionality
- Added a new section in the README to document the integration of MCP (Model Context Protocol) for AI agents, detailing how to set up and use the `atlas_sync` tool.
- Introduced `McpStream` to manage WebSocket connections for real-time sync commands between the plugin and the MCP server.
- Enhanced the `App` component to initialize and manage the MCP stream, including handling sync commands and user confirmations.
- Updated the API to support WebSocket upgrades for the MCP stream, allowing for efficient communication with connected plugins.
- Improved the handling of sync commands, ensuring proper responses based on the current connection state and user selections.

These changes aim to facilitate programmatic syncs to Roblox Studio, improving the workflow for teams using AI agents.
2026-03-01 08:11:36 -06:00

150 lines
3.3 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 strict = require(script.Parent.strict)
local RECONNECT_INTERVAL = 5
local McpStream = {}
McpStream.__index = McpStream
function McpStream.new(options)
local self = setmetatable({}, McpStream)
self._onSyncCommand = options.onSyncCommand
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()
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"),
})
pcall(function()
wsClient:Send(greeting)
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={})", data.requestId)
local resultPromise = self._onSyncCommand(data.requestId)
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)
end
end)
closed = wsClient.Closed:Connect(function()
done = true
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
Log.trace("MCP stream disconnected")
end)
errored = wsClient.Error:Connect(function(_code, _msg)
done = true
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,
})