@adiholick/nvstudio-mcp 1.0.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ local HttpService = game:GetService("HttpService")
2
+ local LogService = game:GetService("LogService")
3
+
4
+ local TASK_URL = "http://localhost:3055/api/tasks"
5
+ local RESPONSE_URL = "http://localhost:3055/api/response"
6
+
7
+ -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
8
+ local STUDIO_ID = HttpService:GenerateGUID(false)
9
+
10
+ -- Import UI Manager (Procedural Dashboard)
11
+ local uiModule = script.Parent:FindFirstChild("ui_manager")
12
+ local ui = nil
13
+
14
+ -- Membangun Tombol Toolbar (Toggle)
15
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
16
+ local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722")
17
+
18
+ -- Global State & Context AI
19
+ local ctx = {
20
+ scriptHistory = {},
21
+ recentLogs = {},
22
+ HttpService = HttpService,
23
+ studioId = STUDIO_ID
24
+ }
25
+ local MAX_LOGS = 50
26
+ local isConnected = false
27
+ local autoConnect = plugin:GetSetting("AutoConnect") or false
28
+ local isPolling = false
29
+ local isClickDebounce = false -- Mencegah rapid double-click
30
+
31
+ -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
32
+ local startPolling
33
+ local stopPolling
34
+
35
+ -- Inisialisasi UI jika modul ditemukan
36
+ if uiModule then
37
+ local UIManager = require(uiModule)
38
+ ui = UIManager.init(plugin, STUDIO_ID)
39
+
40
+ -- Sync Initial State ke UI
41
+ ui:setAutoConnectState(autoConnect)
42
+ ui:setStatus("disconnected")
43
+
44
+ -- Event Toggle GUI Panel
45
+ toggleBtn.Click:Connect(function()
46
+ ui:toggleVisibility()
47
+ end)
48
+
49
+ -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
50
+ ui.connectBtn.MouseButton1Click:Connect(function()
51
+ if isClickDebounce then return end
52
+ isClickDebounce = true
53
+
54
+ if not isConnected then
55
+ -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
56
+ isManualDisconnect = false
57
+ isConnected = true
58
+ ui:setStatus("waiting")
59
+ ui:addLog("[System] Koneksi manual dimulai. Mencari AI Server (Waiting)...", Color3.fromRGB(255, 220, 100))
60
+ startPolling()
61
+ else
62
+ -- DISCONNECT — hentikan polling dan tandai manual disconnect
63
+ stopPolling()
64
+ ui:setStatus("disconnected")
65
+ ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
66
+ end
67
+
68
+ task.delay(0.3, function()
69
+ isClickDebounce = false
70
+ end)
71
+ end)
72
+
73
+ -- Event Custom Checkbox (Auto-Connect)
74
+ ui.autoConnectBtn.MouseButton1Click:Connect(function()
75
+ autoConnect = not autoConnect
76
+ plugin:SetSetting("AutoConnect", autoConnect)
77
+ ui:setAutoConnectState(autoConnect)
78
+ ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
79
+ end)
80
+ else
81
+ -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
82
+ toggleBtn.Click:Connect(function()
83
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
84
+ end)
85
+ end
86
+
87
+ -- Handler Output Studio (Untuk AI dan GUI)
88
+ local function addSystemLog(msg, msgType)
89
+ local color = Color3.fromRGB(200, 200, 200)
90
+ local prefix = "[INFO] "
91
+ if msgType == Enum.MessageType.MessageError then
92
+ prefix = "[ERROR] "
93
+ color = Color3.fromRGB(255, 100, 100)
94
+ elseif msgType == Enum.MessageType.MessageWarning then
95
+ prefix = "[WARN] "
96
+ color = Color3.fromRGB(255, 200, 100)
97
+ end
98
+
99
+ local fullMsg = prefix .. msg
100
+
101
+ -- 1. Simpan di memori untuk disedot AI
102
+ table.insert(ctx.recentLogs, fullMsg)
103
+ if #ctx.recentLogs > MAX_LOGS then
104
+ table.remove(ctx.recentLogs, 1)
105
+ end
106
+
107
+ -- 2. Cetak ke Terminal GUI Plugin
108
+ if ui then
109
+ ui:addLog(fullMsg, color)
110
+ end
111
+ end
112
+
113
+ -- Ikat listener output
114
+ LogService.MessageOut:Connect(addSystemLog)
115
+
116
+ -- Memuat semua module skills secara dinamis
117
+ local skills = {}
118
+ for _, module in ipairs(script.Parent:GetChildren()) do
119
+ if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
120
+ skills[module.Name] = require(module)
121
+ end
122
+ end
123
+
124
+ -- Fungsi Helper Pathing
125
+ local function getInstanceFromPath(pathString)
126
+ local parts = string.split(pathString, ".")
127
+ local current = game
128
+
129
+ if parts[1] == "game" then table.remove(parts, 1) end
130
+
131
+ -- Handle common Services directly to ensure reliability
132
+ pcall(function()
133
+ local svc = game:GetService(parts[1])
134
+ if svc then
135
+ current = svc
136
+ table.remove(parts, 1)
137
+ end
138
+ end)
139
+
140
+ for _, part in ipairs(parts) do
141
+ local found = current:FindFirstChild(part)
142
+ if not found then return nil end
143
+ current = found
144
+ end
145
+ return current
146
+ end
147
+
148
+ -- Memproses Task Eksekusi dari AI
149
+ local function processTask(taskData)
150
+ if not taskData or not taskData.id or not taskData.command then return nil end
151
+
152
+ if ui then
153
+ ui:addLog("[AI Command] Menerima instruksi: " .. taskData.command, Color3.fromRGB(150, 200, 255))
154
+ end
155
+
156
+ local targetInstance = getInstanceFromPath(taskData.target)
157
+ if not targetInstance then
158
+ return {
159
+ id = taskData.id,
160
+ status = "error",
161
+ error = "Target path tidak ditemukan: " .. taskData.target
162
+ }
163
+ end
164
+
165
+ local skillFunc = skills[taskData.command]
166
+ if not skillFunc then
167
+ return {
168
+ id = taskData.id,
169
+ status = "error",
170
+ error = "Command tidak dikenali oleh Studio: " .. taskData.command
171
+ }
172
+ end
173
+
174
+ -- Eksekusi skill AI
175
+ local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
176
+ result.id = taskData.id
177
+
178
+ -- Cetak hasil eksekusi ke Terminal GUI
179
+ if ui then
180
+ local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
181
+ ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
182
+ end
183
+
184
+ return result
185
+ end
186
+
187
+ -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
188
+ local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
189
+
190
+ function stopPolling()
191
+ isConnected = false
192
+ isManualDisconnect = true
193
+ -- isPolling akan menjadi false secara otomatis saat loop keluar
194
+ end
195
+
196
+ -- Background Polling Loop (Dapat dihentikan oleh user)
197
+ function startPolling()
198
+ if isPolling then return end
199
+ isPolling = true
200
+
201
+ task.spawn(function()
202
+ while isConnected do
203
+ local success, response = pcall(function()
204
+ -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
205
+ local cacheBuster = "?t=" .. tostring(os.clock())
206
+ return HttpService:GetAsync(TASK_URL .. cacheBuster)
207
+ end)
208
+
209
+ -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
210
+ if not isConnected then
211
+ break
212
+ end
213
+
214
+ if success and response then
215
+ -- JIKA PING SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
216
+ if ui then ui:setStatus("connected") end
217
+
218
+ local decodeSuccess, taskData = pcall(function()
219
+ return HttpService:JSONDecode(response)
220
+ end)
221
+
222
+ if decodeSuccess and taskData and taskData.id then
223
+ local resultData = processTask(taskData)
224
+ if resultData then
225
+ pcall(function()
226
+ local jsonResult = HttpService:JSONEncode(resultData)
227
+ HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
228
+ end)
229
+ end
230
+ end
231
+
232
+ -- Fast-polling (long-polling yield di server Node.js)
233
+ -- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
234
+ task.wait(0.05)
235
+ else
236
+ -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
237
+ if ui then ui:setStatus("waiting") end
238
+ task.wait(1)
239
+ end
240
+
241
+ -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
242
+ if not isConnected then
243
+ break
244
+ end
245
+ end
246
+ isPolling = false
247
+ end)
248
+ end
249
+
250
+ -- ══════════════════════════════════════════════════════════════════════
251
+ -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
252
+ -- Mendeteksi apakah server MCP aktif setiap 2 detik.
253
+ -- Jika terdeteksi aktif → otomatis connect (tanpa perlu tekan tombol).
254
+ -- Jika terdeteksi mati → otomatis update status.
255
+ -- ══════════════════════════════════════════════════════════════════════
256
+ task.spawn(function()
257
+ task.wait(1) -- Tunggu sebentar agar UI siap
258
+
259
+ if ui then
260
+ ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
261
+ end
262
+
263
+ while true do
264
+ -- Skip heartbeat jika polling loop sudah aktif (ia sudah menangani semuanya)
265
+ if not isPolling then
266
+ local success, errorMsg = pcall(function()
267
+ local cacheBuster = "?t=" .. tostring(os.clock())
268
+ return HttpService:GetAsync(TASK_URL .. cacheBuster)
269
+ end)
270
+
271
+ if success then
272
+ -- Server terdeteksi aktif!
273
+ if not isConnected and not isManualDisconnect then
274
+ isConnected = true
275
+ if ui then
276
+ ui:setStatus("connected")
277
+ ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
278
+ end
279
+ startPolling()
280
+ end
281
+ else
282
+ -- Server tidak aktif
283
+ if isConnected then
284
+ -- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
285
+ isConnected = false
286
+ if ui then
287
+ ui:setStatus("waiting")
288
+ ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
289
+ end
290
+ elseif not isManualDisconnect then
291
+ if ui then
292
+ ui:setStatus("waiting")
293
+ -- HANYA print error jika belum terkoneksi, untuk debugging
294
+ ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ task.wait(2)
301
+ end
302
+ end)
@@ -0,0 +1,24 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
3
+ if not success or not config.ClassName then
4
+ return { status = "error", error = "Format data JSON tidak valid atau ClassName hilang." }
5
+ end
6
+
7
+ local newSuccess, newInst = pcall(function()
8
+ local inst = Instance.new(config.ClassName)
9
+ inst.Parent = targetInstance
10
+
11
+ if config.Properties then
12
+ for prop, val in pairs(config.Properties) do
13
+ pcall(function() inst[prop] = val end)
14
+ end
15
+ end
16
+ return inst
17
+ end)
18
+
19
+ if newSuccess then
20
+ return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath }
21
+ else
22
+ return { status = "error", error = "Gagal membuat Instance. Pastikan ClassName valid." }
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ -- Guardrail: Cegah penghapusan Service utama
3
+ local protectedServices = {
4
+ "Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
5
+ "ReplicatedFirst", "ReplicatedStorage", "ServerScriptService", "ServerStorage",
6
+ "StarterGui", "StarterPack", "StarterPlayer", "Teams", "SoundService", "Chat",
7
+ "TextChatService", "VoiceChatService"
8
+ }
9
+
10
+ if targetInstance.Parent == game or targetInstance.Parent == nil then
11
+ for _, serviceName in ipairs(protectedServices) do
12
+ if targetInstance.Name == serviceName or targetInstance.ClassName == serviceName then
13
+ return { status = "error", error = "GUARDRAIL BLOCKED: Anda tidak diizinkan untuk menghapus layanan tingkat atas (" .. targetInstance.Name .. ")." }
14
+ end
15
+ end
16
+ end
17
+
18
+ local name = targetInstance.Name
19
+ local className = targetInstance.ClassName
20
+ local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
21
+
22
+ local success, err = pcall(function()
23
+ targetInstance:Destroy()
24
+ end)
25
+
26
+ if success then
27
+ return { status = "success", result = "Instance '" .. name .. "' (" .. className .. ") berhasil dihapus dari " .. parentName .. "." }
28
+ else
29
+ return { status = "error", error = "Gagal menghapus instance: " .. tostring(err) }
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local code = data
3
+ if not code or code == "" then
4
+ return { status = "error", error = "Kode yang dieksekusi tidak boleh kosong." }
5
+ end
6
+
7
+ local HttpService = game:GetService("HttpService")
8
+
9
+ -- Bungkus code ke dalam fungsi return
10
+ local wrappedCode = "return function()\n" .. code .. "\nend"
11
+
12
+ local moduleScript = Instance.new("ModuleScript")
13
+ moduleScript.Name = "AI_Execute_Temp"
14
+ moduleScript.Source = wrappedCode
15
+
16
+ -- Taruh di ReplicatedStorage sementara agar bisa di-require
17
+ moduleScript.Parent = game:GetService("ReplicatedStorage")
18
+
19
+ local resultData = nil
20
+ local execError = nil
21
+ local isFinished = false
22
+
23
+ -- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
24
+ task.spawn(function()
25
+ local reqSuccess, func = pcall(function() return require(moduleScript) end)
26
+ if reqSuccess and type(func) == "function" then
27
+ local execSuccess, res = pcall(func)
28
+ if execSuccess then
29
+ resultData = res
30
+ else
31
+ execError = "Runtime Error: " .. tostring(res)
32
+ end
33
+ else
34
+ execError = "Syntax/Require Error: " .. tostring(func)
35
+ end
36
+ isFinished = true
37
+ end)
38
+
39
+ -- Tunggu hingga 5 detik
40
+ local timeWaited = 0
41
+ while not isFinished and timeWaited < 5 do
42
+ timeWaited = timeWaited + task.wait()
43
+ end
44
+
45
+ -- Pembersihan (Wajib dihancurkan)
46
+ pcall(function() moduleScript:Destroy() end)
47
+
48
+ if not isFinished then
49
+ return { status = "error", error = "Timeout 5 detik terlampaui. Kemungkinan ada infinite loop tanpa task.wait()." }
50
+ end
51
+
52
+ if execError then
53
+ return { status = "error", error = execError }
54
+ end
55
+
56
+ -- Cegah return nil error
57
+ if resultData == nil then
58
+ return { status = "success", result = "Eksekusi berhasil (Tidak ada nilai kembalian)." }
59
+ end
60
+
61
+ -- JSON Encode untuk mencegah raw Instance return crash
62
+ local serializeSuccess, jsonRes = pcall(function()
63
+ return HttpService:JSONEncode(resultData)
64
+ end)
65
+
66
+ if serializeSuccess then
67
+ return { status = "success", result = HttpService:JSONDecode(jsonRes) } -- decode kembali agar format aslinya diteruskan oleh router
68
+ else
69
+ return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
3
+ if not success or not config.Size or not config.Position or not config.Material then
4
+ return { status = "error", error = "Format JSON tidak valid. Membutuhkan Size, Position, dan Material." }
5
+ end
6
+
7
+ local fillSuccess, err = pcall(function()
8
+ local terrain = game.Workspace.Terrain
9
+ local materialEnum = Enum.Material[config.Material] or Enum.Material.Grass
10
+
11
+ local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
12
+ local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
13
+
14
+ local region = Region3.new(position - (size/2), position + (size/2))
15
+ terrain:FillRegion(region:ExpandToGrid(4), 4, materialEnum)
16
+ end)
17
+
18
+ if fillSuccess then
19
+ return { status = "success", result = "Terrain (" .. config.Material .. ") berhasil di-generate." }
20
+ else
21
+ return { status = "error", error = "Gagal men-generate Terrain. Pastikan tipe data array angka valid: " .. tostring(err) }
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local children = {}
3
+ for _, child in ipairs(targetInstance:GetChildren()) do
4
+ table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
5
+ end
6
+ return { status = "success", result = children }
7
+ end
@@ -0,0 +1,7 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ if #ctx.recentLogs == 0 then
3
+ return { status = "success", result = {"Console kosong. Tidak ada error atau log terbaru."} }
4
+ else
5
+ return { status = "success", result = ctx.recentLogs }
6
+ end
7
+ end
@@ -0,0 +1,52 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local CollectionService = game:GetService("CollectionService")
3
+
4
+ local props = {
5
+ Name = targetInstance.Name,
6
+ ClassName = targetInstance.ClassName,
7
+ Parent = targetInstance.Parent and targetInstance.Parent.Name or "nil",
8
+ }
9
+
10
+ -- Ambil Properties Umum secara aman via pcall
11
+ local function tryProp(propName)
12
+ local success, val = pcall(function() return targetInstance[propName] end)
13
+ if success and val ~= nil then
14
+ if typeof(val) == "Vector3" then
15
+ props[propName] = {math.round(val.X*100)/100, math.round(val.Y*100)/100, math.round(val.Z*100)/100}
16
+ elseif typeof(val) == "Color3" then
17
+ props[propName] = {math.round(val.R * 255), math.round(val.G * 255), math.round(val.B * 255)}
18
+ elseif typeof(val) == "CFrame" then
19
+ props[propName] = tostring(val)
20
+ elseif typeof(val) == "EnumItem" then
21
+ props[propName] = val.Name
22
+ elseif typeof(val) == "UDim2" then
23
+ props[propName] = {val.X.Scale, val.X.Offset, val.Y.Scale, val.Y.Offset}
24
+ elseif typeof(val) == "Instance" then
25
+ props[propName] = val:GetFullName()
26
+ else
27
+ props[propName] = val
28
+ end
29
+ end
30
+ end
31
+
32
+ local commonProps = {
33
+ "Anchored", "CanCollide", "Transparency", "Reflectance",
34
+ "Position", "Size", "Color", "Material", "Text", "TextColor3",
35
+ "BackgroundTransparency", "BackgroundColor3", "Visible", "Enabled",
36
+ "Volume", "Playing", "Looped", "Value"
37
+ }
38
+
39
+ for _, prop in ipairs(commonProps) do
40
+ tryProp(prop)
41
+ end
42
+
43
+ -- Attributes & Tags
44
+ pcall(function()
45
+ props.Attributes = targetInstance:GetAttributes()
46
+ end)
47
+ pcall(function()
48
+ props.Tags = CollectionService:GetTags(targetInstance)
49
+ end)
50
+
51
+ return { status = "success", result = props }
52
+ end
@@ -0,0 +1,7 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ if targetInstance:IsA("LuaSourceContainer") then
3
+ return { status = "success", result = targetInstance.Source }
4
+ else
5
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. "). Kamu WAJIB menggunakan get_children terlebih dahulu sebelum menarik source." }
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local InsertService = game:GetService("InsertService")
3
+ local assetId = tonumber(data)
4
+ if not assetId then
5
+ return { status = "error", error = "Asset ID harus berupa angka yang valid." }
6
+ end
7
+
8
+ local success, model = pcall(function()
9
+ return InsertService:LoadAsset(assetId)
10
+ end)
11
+
12
+ if success and model then
13
+ model.Parent = game.Workspace
14
+ return { status = "success", result = "Aset berhasil dimasukkan ke Workspace." }
15
+ else
16
+ return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
17
+ end
18
+ end