rojo/plugin/src/McpStream.lua
Joe 581aeaeffa Enhance MCP stream handling and update TODO list
- Added place ID validation in the MCP stream connection to ensure only whitelisted IDs are accepted, improving security and control over connections.
- Updated the PluginConfig structure to include an optional place ID field, allowing for better configuration management.
- Refined the TODO list to include new tasks related to hash-based comparisons and terminal command execution, enhancing project tracking.

These changes aim to strengthen the MCP integration and improve the overall functionality of the plugin.
2026-03-01 08:17:46 -06:00

151 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"),
placeId = game.PlaceId,
})
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,
})