Skip to main content
Glama

Vibe Blocks MCP

Plugin.server.lua77.5 kB
--[[ Plugin Main Script ]] local HttpService = game:GetService("HttpService") local RunService = game:GetService("RunService") local LogService = game:GetService("LogService") -- Added LogService local SERVER_URL = "http://localhost:8000/plugin_command" -- TODO: Make configurable local POLLING_INTERVAL = 2 -- Seconds -- --- NEW: Result Reporting Configuration --- -- local SERVER_RESULT_ENDPOINT = "http://localhost:8000/plugin_report_result" -- Endpoint for sending results back -- --- END: Result Reporting Configuration --- -- -- --- NEW: Logging Configuration --- -- local SERVER_LOG_ENDPOINT = "http://localhost:8000/receive_studio_logs" -- Endpoint for sending logs local SEND_INTERVAL = 1.5 -- Minimum seconds between log sends to avoid spam local MAX_LOG_BATCH_SIZE = 50 -- Max logs to send in one batch local logsToSend = {} -- Buffer for logs waiting to be sent local isSendingLogs = false -- Flag to prevent concurrent sends local lastLogSendTime = 0 -- --- END: Logging Configuration --- -- local lastPollTime = 0 print("Vibe Blocks MCP Companion Plugin Loaded") -- --- Helper: Send Result Back to Server --- -- local function sendResultToServer(requestId, resultData) if not requestId then print("Vibe Blocks MCP Plugin: Error - Cannot send result without a request ID.") return end local payload = { request_id = requestId, result = resultData -- This should be a table (will be JSON encoded) } local success, encodedPayload = pcall(function() return HttpService:JSONEncode(payload) end) if not success then print("Vibe Blocks MCP Plugin: Error - Failed to JSON encode result payload for request ID " .. requestId .. ": " .. tostring(encodedPayload)) -- encodedPayload is error message here return end print("Vibe Blocks MCP Plugin: Sending result for request ID " .. requestId .. " to " .. SERVER_RESULT_ENDPOINT) local postSuccess, postError = pcall(function() -- Use PostAsync for non-blocking request HttpService:PostAsync(SERVER_RESULT_ENDPOINT, encodedPayload, Enum.HttpContentType.ApplicationJson) end) if not postSuccess then print("Vibe Blocks MCP Plugin: Error - Failed to POST result to server for request ID " .. requestId .. ": " .. tostring(postError)) -- Maybe implement retry logic later if needed else print("Vibe Blocks MCP Plugin: Successfully posted result for request ID " .. requestId) end end -- --- End Helper: Send Result --- -- local function findObjectFromPath(pathString) -- Simple path traversal (game, workspace, or starts with game/workspace) local parts = pathString:split(".") local currentObject local firstPartLower = string.lower(parts[1]) -- Check if the path starts explicitly with game or workspace if firstPartLower == "game" then currentObject = game table.remove(parts, 1) -- Remove 'game' from parts to traverse elseif firstPartLower == "workspace" then currentObject = workspace table.remove(parts, 1) -- Remove 'workspace' from parts to traverse else -- Default to starting search from 'game' for other services (ServerStorage, etc.) currentObject = game end -- Traverse the remaining parts for _, partName in ipairs(parts) do if currentObject then currentObject = currentObject:FindFirstChild(partName) else return nil -- Path became invalid end end return currentObject end -- --- NEW: Helper to Convert Python/JSON values to Roblox Types --- -- Returns: robloxValue, errorMessage (errorMessage is nil on success) local function convertToRobloxType(propertyName, valueFromPython) local propNameLower = string.lower(propertyName or "") -- Safe lowercasing -- 1. Determine Expected Type based on Property Name local expectedType = "unknown" if propNameLower == "position" or propNameLower == "size" or propNameLower == "velocity" or propNameLower == "rotvelocity" or propNameLower == "orientation" then expectedType = "Vector3" elseif propNameLower == "color" then expectedType = "Color3" elseif propNameLower == "brickcolor" then expectedType = "BrickColor" elseif propNameLower == "cframe" then expectedType = "CFrame" elseif propNameLower == "material" then expectedType = "Enum.Material" elseif propNameLower == "shape" or propNameLower == "parttype" or propNameLower == "formfactor" then expectedType = "Enum.PartType" -- FormFactor maps to PartType Enum -- Add more specific property -> type mappings here (e.g., UDim2, NumberSequence) end -- 2. Handle Conversion based on Input Type and Expected Type local inputType = typeof(valueFromPython) if expectedType == "Vector3" then if inputType == "table" then if type(valueFromPython.x) == "number" and type(valueFromPython.y) == "number" and type(valueFromPython.z) == "number" then return Vector3.new(valueFromPython.x, valueFromPython.y, valueFromPython.z), nil elseif type(valueFromPython[1]) == "number" and type(valueFromPython[2]) == "number" and type(valueFromPython[3]) == "number" and #valueFromPython == 3 then return Vector3.new(valueFromPython[1], valueFromPython[2], valueFromPython[3]), nil else return nil, "Invalid table format for Vector3. Expected {x,y,z} or array [1,2,3]." end else return nil, "Incorrect input type for Vector3. Expected table, got " .. inputType end elseif expectedType == "Color3" then if inputType == "table" then -- Prefer {r,g,b} format (assume 0-1 range from JSON) if type(valueFromPython.r) == "number" and type(valueFromPython.g) == "number" and type(valueFromPython.b) == "number" then return Color3.new(valueFromPython.r, valueFromPython.g, valueFromPython.b), nil -- Accept array [r,g,b] format (assume 0-1 range from JSON) elseif type(valueFromPython[1]) == "number" and type(valueFromPython[2]) == "number" and type(valueFromPython[3]) == "number" and #valueFromPython == 3 then -- Check if values seem to be in 0-255 range (common mistake) if valueFromPython[1] > 1 or valueFromPython[2] > 1 or valueFromPython[3] > 1 then print("Vibe Blocks MCP Plugin: Warning - Color3 array values > 1 detected for '"..propertyName.."'. Assuming 0-255 range and using Color3.fromRGB.") return Color3.fromRGB(math.floor(valueFromPython[1]), math.floor(valueFromPython[2]), math.floor(valueFromPython[3])), nil else return Color3.new(valueFromPython[1], valueFromPython[2], valueFromPython[3]), nil end else return nil, "Invalid table format for Color3. Expected {r,g,b} or array [r,g,b] (0-1 range preferred)." end else return nil, "Incorrect input type for Color3. Expected table, got " .. inputType end elseif expectedType == "BrickColor" then if inputType == "string" or inputType == "number" then -- BrickColor.new handles invalid names/numbers gracefully by returning grey return BrickColor.new(valueFromPython), nil else return nil, "Incorrect input type for BrickColor. Expected string or number, got " .. inputType end elseif expectedType == "CFrame" then if inputType == "table" then -- Support 12-number array format: [x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22] local allNumbers = true if #valueFromPython == 12 then for i = 1, 12 do if type(valueFromPython[i]) ~= "number" then allNumbers = false break end end if allNumbers then return CFrame.new( valueFromPython[1], valueFromPython[2], valueFromPython[3], valueFromPython[4], valueFromPython[5], valueFromPython[6], valueFromPython[7], valueFromPython[8], valueFromPython[9], valueFromPython[10], valueFromPython[11], valueFromPython[12] ), nil else return nil, "Invalid CFrame array. Expected 12 numbers." end -- Add support for other CFrame formats here if needed (e.g., Position+LookVector dict) else return nil, "Invalid table format for CFrame. Expected array of 12 numbers." end else return nil, "Incorrect input type for CFrame. Expected table, got " .. inputType end elseif string.sub(expectedType, 1, 5) == "Enum." then -- Handle Enums local enumTypeName = string.sub(expectedType, 6) -- Get "Material", "PartType", etc. local enumType = Enum[enumTypeName] if not enumType then return nil, "Internal Error: Unknown Enum type '" .. enumTypeName .. "'" end if inputType == "string" then -- If it already starts with "Enum.", try direct lookup if string.sub(valueFromPython, 1, 5) == "Enum." then local parts = valueFromPython:split(".") if #parts == 3 and parts[2] == enumTypeName then local enumItem = enumType[parts[3]] if enumItem then return enumItem, nil else return nil, "Invalid Enum item name '" .. parts[3] .. "' in full Enum path." end else return nil, "Invalid full Enum path format: " .. valueFromPython end else -- Try lookup by string name directly using index local enumItem = enumType[valueFromPython] -- Use direct indexing -- Alternative: iterate through enumType:GetEnumItems() and compare names (case-insensitive?) if enumItem then return enumItem, nil else return nil, "Could not find Enum item '" .. valueFromPython .. "' in Enum." .. enumTypeName end end elseif inputType == "number" then -- Try lookup by enum value/number for _, item in ipairs(enumType:GetEnumItems()) do if item.Value == valueFromPython then return item, nil end end return nil, "Could not find Enum item with value " .. tostring(valueFromPython) .. " in Enum." .. enumTypeName else return nil, "Incorrect input type for Enum." .. enumTypeName .. ". Expected string or number, got " .. inputType end end -- 3. If no specific type matched or conversion wasn't needed, return the original value -- This handles basic types: string, number, boolean, nil, and tables for non-special properties if inputType == "string" or inputType == "number" or inputType == "boolean" or inputType == "nil" or inputType == "table" then return valueFromPython, nil else -- Should not happen for standard JSON types, but catch anyway return nil, "Unsupported input value type: " .. inputType end end -- --- END Helper: Convert Roblox Value --- -- local function handleSetEnvironment(data) local targetName = data.target local properties = data.properties local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not targetName or not properties or type(properties) ~= "table" then resultPayload.error = "Missing/invalid 'target' or 'properties' in set_environment data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local targetService local serviceSuccess, serviceOrError = pcall(function() if string.lower(targetName) == "lighting" then return game:GetService("Lighting") elseif string.lower(targetName) == "terrain" then return workspace:FindFirstChildOfClass("Terrain") else error("Unsupported target for set_environment: " .. targetName) end end) if not serviceSuccess then resultPayload.error = "Error finding target service: " .. tostring(serviceOrError) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end targetService = serviceOrError -- Assign the found service if not targetService then resultPayload.error = "Could not find target service instance: " .. targetName print("Vibe Blocks MCP Plugin: " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end print("Vibe Blocks MCP Plugin: Setting properties on " .. targetService.Name .. ":") local allPropertiesSuccess = true local propertyErrors = {} for propName, propValue in pairs(properties) do local success, err = pcall(function() -- Basic type handling - TODO: Expand this like handleSetProperty if type(propValue) == "number" then targetService[propName] = propValue print(" - Set " .. propName .. " to " .. tostring(propValue)) elseif type(propValue) == "boolean" then targetService[propName] = propValue print(" - Set " .. propName .. " to " .. tostring(propValue)) elseif type(propValue) == "string" then targetService[propName] = propValue print(" - Set " .. propName .. " to '" .. propValue .. "'") elseif type(propValue) == "table" then -- Placeholder for complex types error("Table values not fully supported yet in set_environment") else error("Unsupported value type: " .. type(propValue)) end end) if not success then allPropertiesSuccess = false propertyErrors[propName] = tostring(err) print(" - Error setting property " .. propName .. ": " .. tostring(err)) end end if allPropertiesSuccess then resultPayload.success = true print("Vibe Blocks MCP Plugin: Finished setting environment properties successfully.") else -- Still technically a success for the operation, but report errors resultPayload.success = false -- Mark as partial failure if any prop failed resultPayload.errors = propertyErrors resultPayload.error = "Failed to set one or more properties." -- General error message print("Vibe Blocks MCP Plugin: Finished setting environment properties with errors.") end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleCreateInstance(data) local className = data.class_name local parentName = data.parent_name or "Workspace" -- Default to Workspace local properties = data.properties or {} -- Default to empty table local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not className then resultPayload.error = "Missing 'class_name' in create_instance data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end -- Find the parent local parentObject = findObjectFromPath(parentName) if not parentObject then resultPayload.error = "Could not find parent object for create_instance: " .. parentName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end print(string.format("Vibe Blocks MCP Plugin: Creating instance of '%s' under '%s'", className, parentObject:GetFullName())) local success, newInstanceOrError = pcall(function() local inst = Instance.new(className) inst.Parent = parentObject -- Parent first -- Apply properties using the new converter for propName, propValue in pairs(properties) do print(string.format(" - Applying property '%s' with value type: %s", propName, type(propValue))) local robloxValue, convertError = convertToRobloxType(propName, propValue) if convertError then -- If conversion fails, wrap the error (will be caught by outer pcall) error(string.format("Error converting value for property '%s': %s", propName, convertError)) else -- Assign the converted value local setSuccess, setError = pcall(function() inst[propName] = robloxValue end) if not setSuccess then -- If setting the converted value fails, wrap the error error(string.format("Error setting property '%s' after conversion: %s", propName, tostring(setError))) end end end return inst -- Return the instance if all properties were set successfully end) if success then local newInstance = newInstanceOrError resultPayload.success = true resultPayload.name = newInstance.Name resultPayload.path = newInstance:GetFullName() print("Vibe Blocks MCP Plugin: Finished creating instance " .. resultPayload.path) else resultPayload.error = "Failed to create instance: " .. tostring(newInstanceOrError) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleDeleteInstance(data) local objectName = data.object_name if not objectName then print("Vibe Blocks MCP Plugin: Error - Missing 'object_name' in delete_instance data.") return end local target = findObjectFromPath(objectName) if not target then print("Vibe Blocks MCP Plugin: Error - Could not find object to delete: " .. objectName) return end local fullName = target:GetFullName() print("Vibe Blocks MCP Plugin: Deleting instance " .. fullName) local success, err = pcall(function() target:Destroy() end) if success then print("Vibe Blocks MCP Plugin: Successfully deleted " .. fullName) else print("Vibe Blocks MCP Plugin: Error deleting " .. fullName .. ": " .. tostring(err)) end end local function handleSetProperty(data) local objectName = data.object_name local propertyName = data.property_name local propertyValue = data.value local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not objectName or not propertyName then resultPayload.error = "Missing 'object_name' or 'property_name' in set_property data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local target = findObjectFromPath(objectName) if not target then resultPayload.error = "Could not find object to set property on: " .. objectName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local fullName = target:GetFullName() print(string.format("Vibe Blocks MCP Plugin: Setting property '%s' on '%s'", propertyName, fullName)) local success, err = pcall(function() -- <<< Debug logging remains >>> print(" - Debug: typeof(propertyValue):", typeof(propertyValue)) local debugSuccess, debugEncoded = pcall(function() return HttpService:JSONEncode(propertyValue) end) if debugSuccess then print(" - Debug: propertyValue JSON:", debugEncoded) else print(" - Debug: propertyValue raw:", tostring(propertyValue)) end -- <<< NEW: Attempt to decode if value is string >>> local valueToConvert = propertyValue if typeof(valueToConvert) == "string" then -- Only attempt decode if it looks like an array or object string if string.sub(valueToConvert, 1, 1) == "[" or string.sub(valueToConvert, 1, 1) == "{" then local decodeSuccess, decodedTable = pcall(function() return HttpService:JSONDecode(valueToConvert) end) if decodeSuccess and typeof(decodedTable) == "table" then print(" - Info: Successfully JSONDecoded string value to table.") valueToConvert = decodedTable -- Use the decoded table instead else -- Log if decoding failed but maybe shouldn't have print(" - Warning: Value is string resembling table/array, but failed to decode or wasn't table type. Error:", tostring(decodedTable)) end else print(" - Info: Value is string, but doesn't start with [ or {. Proceeding with raw string.") end end -- <<< END NEW >>> -- Convert the incoming value using the helper local robloxValue, convertError = convertToRobloxType(propertyName, valueToConvert) if convertError then -- Raise an error if conversion fails error("Value conversion failed: " .. convertError) end -- Assign the converted Roblox value target[propertyName] = robloxValue print(string.format(" - Successfully set '%s' to value of type %s", propertyName, typeof(robloxValue))) end) if success then resultPayload.success = true print("Vibe Blocks MCP Plugin: Finished setting property successfully.") else resultPayload.error = "Failed to set property: " .. tostring(err) print(" - Error setting property: " .. tostring(err)) end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleMoveInstance(data) local objectName = data.object_name local positionTable = data.position local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not objectName or not positionTable or type(positionTable) ~= "table" then resultPayload.error = "Missing/invalid 'object_name' or 'position' in move_instance data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end if type(positionTable.x) ~= "number" or type(positionTable.y) ~= "number" or type(positionTable.z) ~= "number" then resultPayload.error = "Invalid 'position' table format (expected {x=num, y=num, z=num})." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local target = findObjectFromPath(objectName) if not target then resultPayload.error = "Could not find object to move: " .. objectName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local newPosition = Vector3.new(positionTable.x, positionTable.y, positionTable.z) local fullName = target:GetFullName() print(string.format("Vibe Blocks MCP Plugin: Moving '%s' to %s", fullName, tostring(newPosition))) local success, err = pcall(function() if target:IsA("Model") and target.PrimaryPart then local currentCFrame = target:GetPrimaryPartCFrame() target:SetPrimaryPartCFrame(CFrame.new(newPosition) * (currentCFrame - currentCFrame.Position)) elseif target:IsA("BasePart") then target.Position = newPosition else error("Target is not a Model with PrimaryPart or a BasePart.") end end) if success then resultPayload.success = true print("Vibe Blocks MCP Plugin: Successfully moved " .. fullName) else resultPayload.error = "Failed to move object: " .. tostring(err) print("Vibe Blocks MCP Plugin: Error moving " .. fullName .. ": " .. tostring(err)) end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleCloneInstance(data) local objectName = data.object_name local newName = data.new_name -- Optional local parentName = data.parent_name -- Optional local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not objectName then resultPayload.error = "Missing 'object_name' in clone_instance data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local original = findObjectFromPath(objectName) if not original then resultPayload.error = "Could not find original object to clone: " .. objectName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local originalFullName = original:GetFullName() print("Vibe Blocks MCP Plugin: Cloning " .. originalFullName) local cloneSuccess, clone = pcall(function() return original:Clone() end) if not cloneSuccess then resultPayload.error = "Failed to clone object: " .. tostring(clone) -- clone is error message here print("Vibe Blocks MCP Plugin: Error cloning " .. originalFullName .. ": " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end print(" - Clone successful.") -- Handle parenting local parentObject = original.Parent -- Default local parentError = nil if parentName then local specifiedParent = findObjectFromPath(parentName) if specifiedParent then parentObject = specifiedParent else parentError = "Specified parent not found, using original parent." print(" - Warning: " .. parentError) end end local setParentSuccess, setParentErr = pcall(function() clone.Parent = parentObject end) if not setParentSuccess then resultPayload.error = "Failed to set parent on clone: " .. tostring(setParentErr) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end pcall(function() clone:Destroy() end) -- Clean up clone return end print(" - Parent set to: " .. (parentObject and parentObject:GetFullName() or "nil")) -- Handle naming if newName then local setNameSuccess, setNameErr = pcall(function() clone.Name = newName end) if not setNameSuccess then resultPayload.error = "Failed to set name on clone: " .. tostring(setNameErr) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) -- Don't destroy the clone here, parent was set successfully else print(" - Name set to: " .. newName) end else print(" - Using default clone name: " .. clone.Name) end -- If we reached here without a major error, report success if not resultPayload.error then resultPayload.success = true resultPayload.clone_name = clone.Name resultPayload.clone_path = clone:GetFullName() if parentError then resultPayload.parent_error = parentError end -- Include the parent warning print("Vibe Blocks MCP Plugin: Finished cloning. New instance at " .. resultPayload.clone_path) end -- Send final result (success or naming error) if requestId then sendResultToServer(requestId, resultPayload) end end local function handleCreateScript(data) local scriptName = data.script_name local scriptCode = data.script_code local scriptType = data.script_type or "Script" -- Default to Script local parentName = data.parent_name or "Workspace" -- Default to Workspace local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not scriptName or not scriptCode then resultPayload.error = "Missing 'script_name' or 'script_code' in create_script data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end if scriptType ~= "Script" and scriptType ~= "LocalScript" then resultPayload.error = "Invalid 'script_type': " .. scriptType .. ". Must be 'Script' or 'LocalScript'." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local parentObject = findObjectFromPath(parentName) if not parentObject then resultPayload.error = "Could not find parent object for create_script: " .. parentName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end print(string.format("Vibe Blocks MCP Plugin: Creating %s named '%s' under '%s'", scriptType, scriptName, parentObject:GetFullName())) local success, newScriptOrError = pcall(function() local newScript = Instance.new(scriptType) newScript.Name = scriptName newScript.Source = scriptCode newScript.Parent = parentObject -- Parent last after setting properties return newScript end) if success then local newScript = newScriptOrError resultPayload.success = true resultPayload.name = newScript.Name resultPayload.path = newScript:GetFullName() print("Vibe Blocks MCP Plugin: Successfully created script " .. resultPayload.path) else resultPayload.error = string.format("Failed creating %s '%s': %s", scriptType, scriptName, tostring(newScriptOrError)) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleSpawnNpc(data) local modelAssetId = data.model_asset_id local templateModelName = data.template_model_name local positionTable = data.position -- Optional, will be validated later local parentName = data.parent_name or "Workspace" local newName = data.new_name -- Optional local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not modelAssetId and not templateModelName then resultPayload.error = "Missing 'model_asset_id' or 'template_model_name' in spawn_npc data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local parentObject = findObjectFromPath(parentName) if not parentObject then resultPayload.error = "Could not find parent object for spawn_npc: " .. parentName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end -- Validate position table if provided local newPosition = nil if positionTable then if type(positionTable) ~= "table" or type(positionTable.x) ~= "number" or type(positionTable.y) ~= "number" or type(positionTable.z) ~= "number" then resultPayload.error = "Invalid 'position' table format (expected {x=num, y=num, z=num})." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return else newPosition = Vector3.new(positionTable.x, positionTable.y, positionTable.z) end end local npcModel = nil local loadSuccess, loadResultOrError -- Load or Clone NPC Model if modelAssetId then print("Vibe Blocks MCP Plugin: Spawning NPC from Asset ID: " .. tostring(modelAssetId)) loadSuccess, loadResultOrError = pcall(function() local insertService = game:GetService("InsertService") local asset = insertService:LoadAsset(modelAssetId) if asset:IsA('Model') and #asset:GetChildren() == 1 then return asset:GetChildren()[1] else return asset end end) if not loadSuccess then resultPayload.error = "Error loading asset ID " .. tostring(modelAssetId) .. ": " .. tostring(loadResultOrError) end elseif templateModelName then print("Vibe Blocks MCP Plugin: Spawning NPC by cloning template: " .. templateModelName) local template = findObjectFromPath(templateModelName) if not template then resultPayload.error = "Template model not found: " .. templateModelName else loadSuccess, loadResultOrError = pcall(function() return template:Clone() end) if not loadSuccess then resultPayload.error = "Error cloning template " .. templateModelName .. ": " .. tostring(loadResultOrError) end end end -- Check if loading/cloning failed if not loadSuccess or not loadResultOrError or not loadResultOrError:IsA("Instance") then if not resultPayload.error then resultPayload.error = "Failed to obtain a valid instance for the NPC." end print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end npcModel = loadResultOrError -- Set Name, Parent, and Position local setupSuccess, setupError = pcall(function() if newName then npcModel.Name = newName end npcModel.Parent = parentObject print(" - NPC Parent set to: " .. parentObject:GetFullName()) -- Attempt to position if newPosition then if npcModel:IsA("Model") and npcModel.PrimaryPart then local currentCFrame = npcModel:GetPrimaryPartCFrame() npcModel:SetPrimaryPartCFrame(CFrame.new(newPosition) * (currentCFrame - currentCFrame.Position)) print(" - Positioned using SetPrimaryPartCFrame") elseif npcModel:IsA("BasePart") then npcModel.Position = newPosition print(" - Positioned using Position property") else resultPayload.warning = "Could not automatically set position - NPC is not a Model with PrimaryPart or a BasePart." print(" - Warning: " .. resultPayload.warning) end else print(" - No position specified, skipping positioning.") end end) if not setupSuccess then resultPayload.error = "Error setting up NPC (Name/Parent/Position): " .. tostring(setupError) print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) pcall(function() npcModel:Destroy() end) -- Clean up the partially set up NPC else resultPayload.success = true resultPayload.name = npcModel.Name resultPayload.path = npcModel:GetFullName() print("Vibe Blocks MCP Plugin: Finished spawning NPC " .. resultPayload.path) if resultPayload.warning then -- Include position warning if it exists print("Vibe Blocks MCP Plugin: Spawn finished with warnings.") end end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleScaleModel(data) local objectName = data.object_name local scaleFactor = data.scale_factor local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not objectName or type(scaleFactor) ~= "number" or scaleFactor <= 0 then resultPayload.error = "Missing/invalid 'object_name' or 'scale_factor' in scale_model data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local targetModel = findObjectFromPath(objectName) if not targetModel or not targetModel:IsA("Model") then resultPayload.error = "Could not find Model to scale: " .. objectName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local fullName = targetModel:GetFullName() print(string.format("Vibe Blocks MCP Plugin: Scaling model '%s' by factor %.2f", fullName, scaleFactor)) local success, err = pcall(function() local currentSize = targetModel:GetExtentsSize() local targetSize = currentSize * scaleFactor targetModel:ScaleTo(targetSize.X) end) if success then resultPayload.success = true print("Vibe Blocks MCP Plugin: Successfully scaled " .. fullName) else resultPayload.error = "Failed to scale model: " .. tostring(err) print("Vibe Blocks MCP Plugin: Error scaling " .. fullName .. ": " .. tostring(err)) end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handlePlayAnimation(data) local targetName = data.target_name local animationId = data.animation_id local requestId = data.request_id -- Extract request ID local resultPayload = {} -- Initialize result payload if not targetName or not animationId or type(animationId) ~= "number" then resultPayload.error = "Missing/invalid 'target_name' or 'animation_id' in play_animation data." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local target = findObjectFromPath(targetName) if not target then resultPayload.error = "Could not find target object for play_animation: " .. targetName print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local animator = target:FindFirstChildOfClass("Humanoid") or target:FindFirstChildOfClass("AnimationController") if not animator then resultPayload.error = "Target object " .. targetName .. " does not contain a Humanoid or AnimationController." print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error) if requestId then sendResultToServer(requestId, resultPayload) end return end local animationAssetId = "rbxassetid://" .. tostring(animationId) print(string.format("Vibe Blocks MCP Plugin: Playing animation %s on %s", animationAssetId, target:GetFullName())) local animInstance = Instance.new("Animation") animInstance.Name = "MCP_TempAnimation" animInstance.AnimationId = animationAssetId local loadSuccess, trackOrError = pcall(function() return animator:LoadAnimation(animInstance) end) pcall(function() animInstance:Destroy() end) -- Clean up temp instance if not loadSuccess then resultPayload.error = "Error loading animation " .. animationAssetId .. ": " .. tostring(trackOrError) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) else local animationTrack = trackOrError local playSuccess, playError = pcall(function() animationTrack:Play() end) if playSuccess then resultPayload.success = true resultPayload.message = "Animation track loaded and played." print("Vibe Blocks MCP Plugin: Successfully started animation track.") else resultPayload.error = "Error playing animation track: " .. tostring(playError) print("Vibe Blocks MCP Plugin: " .. resultPayload.error) end end -- Send final result if requestId then sendResultToServer(requestId, resultPayload) end end local function handleSendChat(data) local message = data.message -- local senderName = data.sender_name -- Placeholder for future use if needed if not message or type(message) ~= "string" then print("Vibe Blocks MCP Plugin: Error - Missing/invalid 'message' in send_chat data.") return end local textChatService = game:GetService("TextChatService") if not textChatService then print("Vibe Blocks MCP Plugin: Error - TextChatService not found.") return end -- Use RBXSystem channel for system messages local systemChannel = textChatService:FindFirstChild("RBXSystem") if not systemChannel or not systemChannel:IsA("TextChannel") then print("Vibe Blocks MCP Plugin: Error - Could not find RBXSystem TextChannel.") return end print("Vibe Blocks MCP Plugin: Sending system chat message: " .. message) local success, err = pcall(function() -- Use SendAsync for more general message sending systemChannel:SendAsync(message) -- Alternative: systemChannel:DisplaySystemMessage(message) end) if success then print("Vibe Blocks MCP Plugin: Successfully sent chat message.") else print("Vibe Blocks MCP Plugin: Error sending chat message: " .. tostring(err)) end end local function handleTeleportPlayer(data) local playerName = data.player_name local destinationPlaceId = data.destination_place_id local teleportOptions = data.teleport_options -- Optional table local customLoadingScriptName = data.custom_loading_script -- Optional string name if not playerName or not destinationPlaceId or type(destinationPlaceId) ~= "number" then print("Vibe Blocks MCP Plugin: Error - Missing/invalid 'player_name' or 'destination_place_id' in teleport_player data.") return end local teleportService = game:GetService("TeleportService") local playersService = game:GetService("Players") local replicatedFirst = game:GetService("ReplicatedFirst") if not teleportService then print("Vibe Blocks MCP Plugin: Error - TeleportService not found.") return end -- Player finding only works in a running game instance local playerToTeleport = playersService:FindFirstChild(playerName) if not playerToTeleport then print("Vibe Blocks MCP Plugin: Warning - Player \"" .. playerName .. "\" not found (or command run outside active game).") -- Don't return error, as this might be run in edit mode intentionally return end -- Construct TeleportOptions if provided local finalTeleportOptions if teleportOptions and type(teleportOptions) == "table" then local success, optionsInstance = pcall(function() return Instance.new("TeleportOptions") end) if success and optionsInstance then finalTeleportOptions = optionsInstance for key, value in pairs(teleportOptions) do local setSuccess, setError = pcall(function() -- Basic assignment, might need type checks for complex option values finalTeleportOptions[key] = value end) if not setSuccess then print(" - Warning: Failed to set TeleportOption '" .. key .. "': " .. tostring(setError)) end end else print("Vibe Blocks MCP Plugin: Warning - Could not create TeleportOptions instance.") end end -- Find custom loading screen if provided local loadingScreenGui if customLoadingScriptName and type(customLoadingScriptName) == "string" then loadingScreenGui = replicatedFirst:FindFirstChild(customLoadingScriptName) if not loadingScreenGui or not loadingScreenGui:IsA("LocalScript") then print("Vibe Blocks MCP Plugin: Warning - Custom loading script '" .. customLoadingScriptName .. "' not found or not a LocalScript in ReplicatedFirst.") loadingScreenGui = nil -- Reset if not valid end end print(string.format("Vibe Blocks MCP Plugin: Attempting to teleport player %s to place %d", playerName, destinationPlaceId)) local success, err = pcall(function() teleportService:TeleportAsync(destinationPlaceId, {playerToTeleport}, finalTeleportOptions, loadingScreenGui) end) if success then print("Vibe Blocks MCP Plugin: Teleport initiated successfully for " .. playerName) else print("Vibe Blocks MCP Plugin: Error initiating teleport for " .. playerName .. ": " .. tostring(err)) end end local function handleSetPlayerPosition(data) local playerName = data.player_name local positionTable = data.position if not playerName or not positionTable or type(positionTable) ~= "table" then print("Vibe Blocks MCP Plugin: Error - Missing/invalid 'player_name' or 'position' in set_player_position data.") return end -- Validate position table if type(positionTable.x) ~= "number" or type(positionTable.y) ~= "number" or type(positionTable.z) ~= "number" then print("Vibe Blocks MCP Plugin: Error - Invalid 'position' table format (expected {x=num, y=num, z=num}).") return end -- Find the player's character in the Workspace local character = workspace:FindFirstChild(playerName) if not character or not character:IsA("Model") then print("Vibe Blocks MCP Plugin: Error - Could not find character Model in Workspace named: " .. playerName) -- It might also be in game.Players[playerName].Character, but workspace is usually safer for positioning return end -- Find the HumanoidRootPart local hrp = character:FindFirstChild("HumanoidRootPart") if not hrp or not hrp:IsA("BasePart") then print("Vibe Blocks MCP Plugin: Error - Could not find HumanoidRootPart in character: " .. playerName) return end local newPosition = Vector3.new(positionTable.x, positionTable.y, positionTable.z) print(string.format("Vibe Blocks MCP Plugin: Setting position of %s's HumanoidRootPart to %s", playerName, tostring(newPosition))) local success, err = pcall(function() -- Set CFrame directly, preserves orientation if possible hrp.CFrame = CFrame.new(newPosition) -- Alternative if anchoring issues: hrp.Position = newPosition end) if success then print("Vibe Blocks MCP Plugin: Successfully set player position.") else print("Vibe Blocks MCP Plugin: Error setting player position: " .. tostring(err)) end end -- --- NEW: Find Instances Handler --- -- local function handleFindInstances(data) local classNameFilter = data.class_name -- Can be nil local nameContainsFilter = data.name_contains -- Can be nil local searchRootName = data.search_root or "Workspace" local requestId = data.request_id local resultPayload = { instances = {} } -- Initialize with empty list local errorResult = nil local root = findObjectFromPath(searchRootName) if not root then errorResult = { error = "Search root not found: " .. searchRootName } else print(string.format("Vibe Blocks MCP Plugin: Finding instances under %s (%s) for request ID %s", root.Name, root.ClassName, requestId or "N/A")) print(string.format(" - Filters: ClassName='%s', NameContains='%s'", classNameFilter or "any", nameContainsFilter or "any")) local nameFilterLower = nameContainsFilter and nameContainsFilter:lower() or nil local findSuccess, findError = pcall(function() for _, descendant in ipairs(root:GetDescendants()) do -- ClassName check local classMatch = (classNameFilter == nil) or (descendant.ClassName == classNameFilter) -- Name check (case-insensitive) local nameMatch = (nameFilterLower == nil) or (string.find(descendant.Name:lower(), nameFilterLower) ~= nil) if classMatch and nameMatch then -- No need to serialize here, just basic info table.insert(resultPayload.instances, { name = descendant.Name, className = descendant.ClassName, path = descendant:GetFullName() }) end end end) if not findSuccess then errorResult = { error = "Error during search: " .. tostring(findError) } print(" - Error during search: " .. tostring(findError)) else print(string.format(" - Found %d matching instances.", #resultPayload.instances)) end end -- Send results or error back if requestId then sendResultToServer(requestId, errorResult or resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in find_instances data. Cannot report result back.") end end -- --- END: Find Instances Handler --- -- -- --- NEW: List Children Handler --- -- local function handleListChildren(data) local parentName = data.parent_name or "Workspace" -- Use provided name or default local requestId = data.request_id -- Get the request ID sent by the server local parentObject = findObjectFromPath(parentName) local results = {} local errorResult = nil if not parentObject then local errMsg = "Error - Could not find parent object: " .. parentName print("Vibe Blocks MCP Plugin: " .. errMsg) errorResult = { error = errMsg } else print(string.format("Vibe Blocks MCP Plugin: Listing children of %s (%s) for request ID %s", parentObject.Name, parentObject.ClassName, requestId or "N/A")) local success, childrenOrError = pcall(function() return parentObject:GetChildren() end) if success then local children = childrenOrError for i, child in ipairs(children) do table.insert(results, { name = child.Name, className = child.ClassName, path = child:GetFullName() }) print(string.format(" - Found: %s (%s) Path: %s", child.Name, child.ClassName, child:GetFullName())) end print("Vibe Blocks MCP Plugin: Finished listing children for " .. parentName) else local errMsg = "Error getting children for " .. parentName .. ": " .. tostring(childrenOrError) print("Vibe Blocks MCP Plugin: " .. errMsg) errorResult = { error = errMsg } end end -- Send the result (or error) back to the server if requestId then sendResultToServer(requestId, errorResult or results) -- Use the previously added helper else print("Vibe Blocks MCP Plugin: Warning - No request_id found in list_children data. Cannot report result back.") end end -- --- END: List Children Handler --- -- -- --- Helper: Serialize Roblox Value to JSON-compatible Table/Primitive --- -- -- NOTE: Defined globally before handlers that might use it. local function serializeValue(value) local valueType = typeof(value) if valueType == 'Vector3' then return {type='Vector3', x=value.X, y=value.Y, z=value.Z} elseif valueType == 'CFrame' then -- Simplified CFrame representation (Position + LookVector for basic orientation) -- Or return full components if needed local pos = value.Position local look = value.LookVector return {type='CFrame', position={x=pos.X, y=pos.Y, z=pos.Z}, lookVector={x=look.X, y=look.Y, z=look.Z}} elseif valueType == 'Color3' then return {type='Color3', r=value.R, g=value.G, b=value.B} elseif valueType == 'BrickColor' then return {type='BrickColor', name=value.Name, number=value.Number} elseif valueType == 'boolean' or valueType == 'number' or valueType == 'string' or valueType == 'nil' then return value -- These types are directly JSON compatible elseif string.find(valueType, "Enum.") then -- Check if it's an EnumItem return {type='EnumItem', fullValue=tostring(value), name=value.Name, value=value.Value} elseif valueType == 'Instance' then return {type='Instance', name=value.Name, className=value.ClassName, path=value:GetFullName()} elseif valueType == 'RBXScriptConnection' then return {type='Connection', status=value.Connected and 'Connected' or 'Disconnected'} -- Basic info -- Add more types as needed: Vector2, UDim2, Rect, Ray, Region3, PhysicalProperties, etc. else -- Fallback: represent unknown types as a string return {type=valueType, value=tostring(value)} end end -- --- END Helper: Serialize Roblox Value --- -- -- --- NEW: Get Property Handler --- -- local function handleGetProperty(data) local objectName = data.object_name local propertyName = data.property_name local requestId = data.request_id local resultPayload = {} if not objectName or not propertyName then resultPayload.error = "Missing object_name or property_name in get_property data." else local target = findObjectFromPath(objectName) if not target then resultPayload.error = "Object not found: " .. objectName else print(string.format("Vibe Blocks MCP Plugin: Getting property '%s' on %s (%s) for request ID %s", propertyName, target.Name, target.ClassName, requestId or "N/A")) local success, value = pcall(function() return target[propertyName] end) if success then print(string.format(" - Raw value type: %s", typeof(value))) -- Serialize the value for JSON transport local serializeSuccess, serializedResult = pcall(serializeValue, value) if serializeSuccess then resultPayload.value = serializedResult print(" - Serialized value sent.") else resultPayload.error = "Failed to serialize property value: " .. tostring(serializedResult) -- serializedResult is error message here print(" - Error serializing: " .. tostring(serializedResult)) end else resultPayload.error = "Error accessing property: " .. tostring(value) -- value is error message here print(" - Error accessing property: " .. tostring(value)) end end end -- Send the result/error back to the server if requestId then sendResultToServer(requestId, resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in get_property data. Cannot report result back.") end end -- --- END: Get Property Handler --- -- -- --- NEW: Edit Script Handler --- -- local function handleEditScript(data) local scriptPath = data.script_path local newScriptCode = data.script_code local requestId = data.request_id local resultPayload = {} if not scriptPath or not newScriptCode then resultPayload.error = "Missing script_path or script_code in edit_script data." else local targetScript = findObjectFromPath(scriptPath) if not targetScript then resultPayload.error = "Script not found at path: " .. scriptPath elseif not (targetScript:IsA("Script") or targetScript:IsA("LocalScript")) then resultPayload.error = "Target object is not a Script or LocalScript: " .. targetScript.ClassName else print(string.format("Vibe Blocks MCP Plugin: Editing script '%s' for request ID %s", targetScript:GetFullName(), requestId or "N/A")) local success, err = pcall(function() targetScript.Source = newScriptCode end) if success then resultPayload.success = true print(" - Script source updated successfully.") else resultPayload.error = "Error setting script source: " .. tostring(err) print(" - Error setting script source: " .. tostring(err)) end end end -- Send the result/error back to the server if requestId then sendResultToServer(requestId, resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in edit_script data. Cannot report result back.") end end -- --- END: Edit Script Handler --- -- -- --- NEW: Delete Script Handler --- -- local function handleDeleteScript(data) local scriptPath = data.script_path local requestId = data.request_id local resultPayload = {} if not scriptPath then resultPayload.error = "Missing script_path in delete_script data." else local targetScript = findObjectFromPath(scriptPath) if not targetScript then resultPayload.error = "Script not found at path: " .. scriptPath elseif not (targetScript:IsA("Script") or targetScript:IsA("LocalScript")) then resultPayload.error = "Target object is not a Script or LocalScript: " .. targetScript.ClassName else local fullName = targetScript:GetFullName() print(string.format("Vibe Blocks MCP Plugin: Deleting script '%s' for request ID %s", fullName, requestId or "N/A")) local success, err = pcall(function() targetScript:Destroy() end) if success then resultPayload.success = true print(" - Script deleted successfully.") else resultPayload.error = "Error deleting script: " .. tostring(err) print(" - Error deleting script: " .. tostring(err)) end end end -- Send the result/error back to the server if requestId then sendResultToServer(requestId, resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in delete_script data. Cannot report result back.") end end -- --- END: Delete Script Handler --- -- -- --- NEW: Set Primary Part Handler --- -- local function handleSetPrimaryPart(data) local modelPath = data.model_path local partPath = data.part_path local requestId = data.request_id local resultPayload = {} if not modelPath or not partPath then resultPayload.error = "Missing model_path or part_path in set_primary_part data." else local model = findObjectFromPath(modelPath) local part = findObjectFromPath(partPath) if not model then resultPayload.error = "Model not found at path: " .. modelPath elseif not model:IsA("Model") then resultPayload.error = "Object at model_path is not a Model: " .. model.ClassName elseif not part then resultPayload.error = "Part not found at path: " .. partPath elseif not part:IsA("BasePart") then resultPayload.error = "Object at part_path is not a BasePart: " .. part.ClassName elseif not part:IsDescendantOf(model) then resultPayload.error = "Part at " .. partPath .. " is not a descendant of Model at " .. modelPath else print(string.format("Vibe Blocks MCP Plugin: Setting PrimaryPart of '%s' to '%s' for request ID %s", model:GetFullName(), part:GetFullName(), requestId or "N/A")) local success, err = pcall(function() model.PrimaryPart = part end) if success then resultPayload.success = true print(" - PrimaryPart set successfully.") else resultPayload.error = "Error setting PrimaryPart property: " .. tostring(err) print(" - Error setting PrimaryPart: " .. tostring(err)) end end end -- Send the result/error back to the server if requestId then sendResultToServer(requestId, resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in set_primary_part data. Cannot report result back.") end end -- --- END: Set Primary Part Handler --- -- -- --- NEW: Execute Script in Studio Handler --- -- local function handleExecuteScriptInStudio(data) local scriptCode = data.script_code local requestId = data.request_id local resultPayload = { output_lines = {}, return_values = nil, -- Explicitly nil initially error_message = nil } if not scriptCode or type(scriptCode) ~= "string" then resultPayload.error_message = "Missing or invalid 'script_code' (must be a string)." else print(string.format("Vibe Blocks MCP Plugin: Executing script in Studio for request ID %s (Code: %s...)", requestId or "N/A", string.sub(scriptCode, 1, 50))) -- <<< NEW: Prepend standard globals to script code >>> local scriptToExecute = string.format("local game = game\nlocal Workspace = game:GetService(\"Workspace\")\n%s", scriptCode) print(" - Info: Prepended locals game/Workspace to script.") -- 1. Compile the MODIFIED script string local compiledFunc, compileError = loadstring(scriptToExecute) if not compiledFunc then resultPayload.error_message = "Compile Error: " .. tostring(compileError) print(" - Error during script compilation:", compileError) else -- 2. Prepare environment for execution (capture print) local capturedOutput = {} -- local originalPrint = print -- No longer need to save/restore global print -- <<< RE-INTRODUCE: Explicitly populate tempEnv with globals AND custom print >>> local tempEnv = {} -- Copy essential globals tempEnv.game = game tempEnv.workspace = workspace tempEnv.script = script tempEnv.Instance = Instance tempEnv.Vector3 = Vector3 tempEnv.Color3 = Color3 tempEnv.BrickColor = BrickColor tempEnv.CFrame = CFrame tempEnv.Enum = Enum tempEnv.ipairs = ipairs tempEnv.pairs = pairs tempEnv.tostring = tostring tempEnv.tonumber = tonumber tempEnv.pcall = pcall -- Allow script to use pcall itself tempEnv.type = type tempEnv.select = select tempEnv.assert = assert tempEnv.warn = warn -- Capture warn? tempEnv.error = error -- Capture error? tempEnv.math = math tempEnv.table = table tempEnv.string = string tempEnv.os = os tempEnv.debug = debug -- Add our custom print DIRECTLY to the environment tempEnv.print = function(...) local args = {...} local lineParts = {} for i = 1, #args do table.insert(lineParts, tostring(args[i])) end local line = table.concat(lineParts, "\t") table.insert(capturedOutput, line) -- Optional: print(" [Captured Print]:", line) end -- Set the environment for the function setfenv(compiledFunc, tempEnv) -- 3. Execute using pcall (should use print from tempEnv now) local executionSuccess, results = pcall(compiledFunc) -- 5. Process results resultPayload.output_lines = capturedOutput if not executionSuccess then resultPayload.error_message = "Runtime Error: " .. tostring(results) -- results is the error message here print(" - Error during script execution:", tostring(results)) else -- Execution succeeded, results contains return values print(" - Script execution successful.") local returnVals = {select("#", results), results} -- Get all return values if select("#", results) > 0 then local serializedReturns = {} for i = 1, select("#", results) do local success, serialized = pcall(serializeValue, select(i, results)) if success then table.insert(serializedReturns, serialized) else table.insert(serializedReturns, {type="SerializationError", error=tostring(serialized)}) end end resultPayload.return_values = serializedReturns print(" - Captured", #serializedReturns, "return value(s).") else print(" - Script returned no values.") end end end end -- Send the result/error back to the server if requestId then sendResultToServer(requestId, resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in execute_script_in_studio data. Cannot report result back.") end end -- --- END: Execute Script in Studio Handler --- -- -- --- NEW: Modify Children Handler --- -- local function handleModifyChildren(data) local parentPath = data.parent_path local propertyName = data.property_name local propertyValue = data.property_value -- This is already parsed Python object -> Lua table/primitive local nameFilter = data.child_name_filter local classFilter = data.child_class_filter local requestId = data.request_id local resultPayload = { affected_count = 0, errors = {}, error_message = nil -- For fatal errors } -- 1. Find Parent local parentObject = findObjectFromPath(parentPath) if not parentObject then resultPayload.error_message = "Parent object not found at path: " .. parentPath print("Vibe Blocks MCP Plugin: Error - " .. resultPayload.error_message) if requestId then sendResultToServer(requestId, resultPayload) end return end print(string.format("Vibe Blocks MCP Plugin: Modifying children under '%s' for request ID %s", parentObject:GetFullName(), requestId or "N/A")) print(string.format(" - Filters: Name='%s', Class='%s'", nameFilter or "Any", classFilter or "Any")) print(string.format(" - Action: Set '%s'", propertyName)) -- 2. Iterate and Modify Children local children = parentObject:GetChildren() for i, child in ipairs(children) do local childMatches = true -- Apply filters if nameFilter and child.Name ~= nameFilter then childMatches = false end if classFilter and child.ClassName ~= classFilter then childMatches = false end -- If filters pass, attempt modification if childMatches then local childFullName = child:GetFullName() print(string.format(" - Processing child: %s", childFullName)) local success, err = pcall(function() -- Reuse value processing logic from handleSetProperty local valueToConvert = propertyValue if typeof(valueToConvert) == "string" then if string.sub(valueToConvert, 1, 1) == "[" or string.sub(valueToConvert, 1, 1) == "{" then local decodeSuccess, decodedTable = pcall(function() return HttpService:JSONDecode(valueToConvert) end) if decodeSuccess and typeof(decodedTable) == "table" then print(" - Info: Decoded string value for child.") valueToConvert = decodedTable else print(" - Warning: String value for child looked like table/array but failed to decode.") end end end -- Convert value local robloxValue, convertError = convertToRobloxType(propertyName, valueToConvert) if convertError then error("Value conversion failed: " .. convertError) end -- Assign value child[propertyName] = robloxValue print(string.format(" - Successfully set '%s' to type %s", propertyName, typeof(robloxValue))) end) if success then resultPayload.affected_count = resultPayload.affected_count + 1 else local errorMsg = string.format("Failed on '%s': %s", childFullName, tostring(err)) print(" - ERROR: " .. errorMsg) table.insert(resultPayload.errors, errorMsg) end end end print(string.format("Vibe Blocks MCP Plugin: Finished modifying children. Affected: %d, Errors: %d", resultPayload.affected_count, #resultPayload.errors)) -- 3. Send Result if requestId then sendResultToServer(requestId, resultPayload) else print("Vibe Blocks MCP Plugin: Warning - No request_id found in modify_children data. Cannot report result back.") end end -- --- END: Modify Children Handler --- -- local function executeCommand(commandData) local action = commandData.action print("Vibe Blocks MCP Plugin: Executing action - " .. (action or "nil")) if action == "get_property_studio" then local objPath = commandData.object_path local propName = commandData.property_name if not objPath or not propName then print("Vibe Blocks MCP Plugin: Invalid get_property_studio command - missing object_path or property_name") return end local success, result = pcall(function() -- Attempt to find the object using the path -- NOTE: This simple FindFirstChild approach won't work for nested paths like game.Workspace.Part -- We need a helper to traverse the path. -- Let's add a simple path finder local target = findObjectFromPath(objPath) if target then local value = target[propName] print(string.format("Vibe Blocks MCP Plugin: Property [%s.%s] = %s", objPath, propName, tostring(value))) else print(string.format("Vibe Blocks MCP Plugin: Target object not found for path: %s", objPath)) end end) if not success then print(string.format("Vibe Blocks MCP Plugin: Error executing get_property_studio [%s.%s]: %s", objPath, propName, tostring(result))) end elseif action == "print_message" then -- Add a simple test action local message = commandData.message or "No message provided." print("Vibe Blocks MCP Plugin: Message from server -> " .. message) elseif action == "set_environment" then handleSetEnvironment(commandData.data) -- Pass the 'data' part of the command elseif action == "create_instance" then handleCreateInstance(commandData.data) -- Pass the 'data' part elseif action == "delete_instance" then handleDeleteInstance(commandData.data) elseif action == "set_property" then handleSetProperty(commandData.data) elseif action == "move_instance" then handleMoveInstance(commandData.data) elseif action == "clone_instance" then handleCloneInstance(commandData.data) elseif action == "create_script" then handleCreateScript(commandData.data) elseif action == "spawn_npc" then handleSpawnNpc(commandData.data) elseif action == "scale_model" then handleScaleModel(commandData.data) elseif action == "play_animation" then handlePlayAnimation(commandData.data) elseif action == "send_chat" then handleSendChat(commandData.data) elseif action == "teleport_player" then handleTeleportPlayer(commandData.data) elseif action == "set_player_position" then handleSetPlayerPosition(commandData.data) elseif action == "list_children" then handleListChildren(commandData.data) elseif action == "get_property" then handleGetProperty(commandData.data) elseif action == "find_instances" then handleFindInstances(commandData.data) elseif action == "edit_script" then handleEditScript(commandData.data) elseif action == "delete_script" then handleDeleteScript(commandData.data) elseif action == "set_primary_part" then handleSetPrimaryPart(commandData.data) elseif action == "execute_script_in_studio" then handleExecuteScriptInStudio(commandData.data) -- <<< ADD: New action routing >>> elseif action == "modify_children" then handleModifyChildren(commandData.data) else print("Vibe Blocks MCP Plugin: Unknown command action received: " .. tostring(action)) end end local COMMAND_HANDLERS = { set_environment = handleSetEnvironment, create_instance = handleCreateInstance, delete_instance = handleDeleteInstance, set_property = handleSetProperty, get_property = handleGetProperty, list_children = handleListChildren, move_instance = handleMoveInstance, clone_instance = handleCloneInstance, create_script = handleCreateScript, spawn_npc = handleSpawnNpc, scale_model = handleScaleModel, play_animation = handlePlayAnimation, send_chat = handleSendChat, teleport_player = handleTeleportPlayer, set_player_position = handleSetPlayerPosition, find_instances = handleFindInstances, edit_script = handleEditScript, delete_script = handleDeleteScript, set_primary_part = handleSetPrimaryPart, execute_script_in_studio = handleExecuteScriptInStudio, modify_children = handleModifyChildren, -- <<< REGISTER: New handler >>> } local function pollServer() -- Check if running in Studio environment before proceeding if not RunService:IsStudio() then -- print("Vibe Blocks MCP Companion Plugin: Not in Studio environment. Polling disabled.") -- Reduce noise return end local currentTime = os.clock() if currentTime - lastPollTime < POLLING_INTERVAL then return -- Don't poll too frequently end lastPollTime = currentTime local success, responseBody = pcall(function() -- Attempt HTTP GET request -- IMPORTANT: This will fail in Play Solo client, handled by pcall return HttpService:GetAsync(SERVER_URL) end) if not success then -- Ignore specific HTTP errors expected when running in Play Solo client local errStr = tostring(responseBody) -- responseBody contains error object/string if string.find(errStr, "Http requests can only be executed by game server") then -- Silently ignore, this is expected in Play Solo client -- print("Vibe Blocks MCP Plugin: Http GetAsync skipped (Play Solo Client context).") else -- Log other unexpected errors print("Vibe Blocks MCP Plugin: Error polling server - " .. errStr) end return -- Stop processing if the GET failed for any reason end -- Process successful response local decodedSuccess, commandData = pcall(function() return HttpService:JSONDecode(responseBody) end) if not decodedSuccess then print("Vibe Blocks MCP Plugin: Error decoding JSON response from server - " .. tostring(commandData)) -- commandData is error msg return end -- Check if the decoded data is a table and not empty if type(commandData) == "table" and next(commandData) ~= nil then -- Use the structure expected from the Python server: -- {"action": "action_name", "data": { ... }, "request_id": "..."} local action = commandData.action local data = commandData.data local requestId = commandData.request_id -- Extract request_id print("Vibe Blocks MCP Plugin: Received command from server.") print(" - Action: " .. tostring(action)) print(" - Request ID: " .. tostring(requestId)) -- Log the ID -- print(" - Data: " .. HttpService:JSONEncode(data)) -- Optional: Log data if needed, can be verbose if action and COMMAND_HANDLERS[action] then print("Vibe Blocks MCP Plugin: Executing action - " .. action) -- Prepare data for the handler, ensuring request_id is included local handlerData = data if type(handlerData) ~= "table" then -- If original data wasn't a table, create one handlerData = {} end -- Add/overwrite request_id in the data passed to the handler handlerData.request_id = requestId -- Execute the handler local handlerSuccess, handlerError = pcall(COMMAND_HANDLERS[action], handlerData) if not handlerSuccess then print("Vibe Blocks MCP Plugin: Error executing action '" .. action .. "': " .. tostring(handlerError)) -- If the handler failed, send an error result back if there's a request ID if requestId then sendResultToServer(requestId, { error = "Plugin error during action '" .. action .. "' execution: " .. tostring(handlerError) }) end else -- Success! The handler itself (like list_children) is responsible for sending back results if needed. print("Vibe Blocks MCP Plugin: Action '" .. action .. "' execution finished.") end else print("Vibe Blocks MCP Plugin: Warning - Unknown or missing action in command: " .. tostring(action)) -- Send an error result back for unknown actions if there's a request ID if requestId then sendResultToServer(requestId, { error = "Unknown action requested by server: " .. tostring(action) }) end end else -- Empty response or non-table data, likely just no command queued -- print("Vibe Blocks MCP Plugin: No command received from server.") -- Reduce noise end end -- --- NEW: Log Handling Functions --- -- -- Function to send buffered logs to the Python server local function sendLogsToServer() if isSendingLogs or #logsToSend == 0 then return -- Already sending or nothing to send end local currentTime = os.clock() -- Enforce send interval if currentTime - lastLogSendTime < SEND_INTERVAL then -- Optional: Could schedule a deferred send here instead of just dropping return end isSendingLogs = true lastLogSendTime = currentTime -- Update time *before* sending -- Take a batch (up to MAX_LOG_BATCH_SIZE) local batch = {} local count = math.min(#logsToSend, MAX_LOG_BATCH_SIZE) for i = 1, count do table.insert(batch, table.remove(logsToSend, 1)) -- Move from buffer to batch end -- Print only if actually sending (avoids client spam) if RunService:IsServer() then print("Vibe Blocks MCP Plugin: Sending", #batch, "logs to server...") end local success, response = pcall(function() local jsonData = HttpService:JSONEncode(batch) return HttpService:PostAsync(SERVER_LOG_ENDPOINT, jsonData, Enum.HttpContentType.ApplicationJson) end) if success then -- Log successful send only on server -- if RunService:IsServer() then print("Log send success.") end else local errorString = tostring(response) -- Check if it's the expected client-side error if string.find(errorString, "Http requests can only be executed by game server") then -- This error is expected on the client during Play mode, do nothing or minimal log -- print("MCP Debug: Client log send blocked as expected.") else -- Log other unexpected errors warn("Vibe Blocks MCP Plugin: Failed to send logs to server:", errorString) -- Retry logic (keep this part) for i = #batch, 1, -1 do table.insert(logsToSend, 1, batch[i]) end end end isSendingLogs = false -- If there are still logs left, immediately try sending another batch -- This handles cases where logs accumulate faster than the send interval allows clearing if #logsToSend > 0 then task.defer(sendLogsToServer) end end -- Function called by LogService event local function onMessageOut(message, messageType) -- Avoid logging our own log sending messages if string.find(message, "Vibe Blocks MCP Plugin: Sending") then return end local logEntry = { message = message, log_type = tostring(messageType), -- Convert Enum::MessageType to string timestamp = os.clock() -- Use os.clock() for high-resolution timestamp } table.insert(logsToSend, logEntry) -- Trigger send mechanism (non-blocking) -- Use task.defer to ensure it runs after the current event processing -- The IsServer check is inside sendLogsToServer, so it's safe to defer always task.defer(sendLogsToServer) end -- --- END: Log Handling Functions --- -- -- Only run polling loop and connect log service in Studio if RunService:IsStudio() then -- TODO: Add plugin button/UI later if needed RunService.Heartbeat:Connect(function() pollServer() end) print("Vibe Blocks MCP Companion Plugin: Started polling loop.") -- --- NEW: Connect to Log Service --- -- LogService.MessageOut:Connect(onMessageOut) print("Vibe Blocks MCP Companion Plugin: Connected to LogService for log forwarding.") -- --- END: Connect to Log Service --- -- else print("Vibe Blocks MCP Companion Plugin: Not running polling loop or log forwarding (not in Studio).") end -- --- NEW: List Children Handler --- -- -- <<< Function definition moved above COMMAND_HANDLERS >>> -- --- END: List Children Handler --- --

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/majidmanzarpour/vibe-blocks-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server