@adiholick/nvstudio-mcp 2.1.8 → 2.2.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.
@@ -31,6 +31,7 @@ local ctx = {
31
31
  recentLogs = {},
32
32
  HttpService = HttpService,
33
33
  studioId = STUDIO_ID,
34
+ getInstanceFromPath = nil -- akan di-bind di bawah
34
35
  }
35
36
  local MAX_LOGS = 50
36
37
  local isRunning = false -- apakah loop aktif
@@ -43,6 +44,7 @@ local serverReachable = false -- apakah server Node.js reachable
43
44
  local RECONNECT_DELAY_INITIAL = 1.5
44
45
  local RECONNECT_DELAY_MAX = 1.5
45
46
  local reconnectDelay = RECONNECT_DELAY_INITIAL
47
+ local currentLoopId = nil
46
48
 
47
49
  -- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
48
50
  local skills = {}
@@ -122,21 +124,41 @@ LogService.MessageOut:Connect(addSystemLog)
122
124
 
123
125
  -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
124
126
  local function getInstanceFromPath(pathString)
127
+ if type(pathString) ~= "string" or pathString == "" then return nil end
125
128
  local parts = string.split(pathString, ".")
129
+ if #parts == 0 then return nil end
126
130
  local current = game
127
131
  if parts[1] == "game" then table.remove(parts, 1) end
128
132
  pcall(function()
129
133
  local svc = game:GetService(parts[1])
130
134
  if svc then current = svc; table.remove(parts, 1) end
131
135
  end)
132
- for _, part in ipairs(parts) do
136
+ local i = 1
137
+ while i <= #parts do
138
+ local part = parts[i]
133
139
  local found = current:FindFirstChild(part)
140
+
141
+ -- Jika tidak ketemu, coba gabungkan dengan part berikutnya (untuk nama yang mengandung titik)
142
+ local combinedPart = part
143
+ local lookAhead = i + 1
144
+ while not found and lookAhead <= #parts do
145
+ combinedPart = combinedPart .. "." .. parts[lookAhead]
146
+ found = current:FindFirstChild(combinedPart)
147
+ if found then
148
+ i = lookAhead
149
+ end
150
+ lookAhead = lookAhead + 1
151
+ end
152
+
134
153
  if not found then return nil end
135
154
  current = found
155
+ i = i + 1
136
156
  end
137
157
  return current
138
158
  end
139
159
 
160
+ ctx.getInstanceFromPath = getInstanceFromPath
161
+
140
162
  -- ── Proses Task dari Server ───────────────────────────────────────────────────
141
163
  local function processTask(taskData)
142
164
  if not taskData or not taskData.id or not taskData.command then return nil end
@@ -147,7 +169,11 @@ local function processTask(taskData)
147
169
 
148
170
  local targetInstance = getInstanceFromPath(taskData.target)
149
171
  if not targetInstance then
150
- return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
172
+ if taskData.target == "" or taskData.target == "nil" then
173
+ targetInstance = game
174
+ else
175
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
176
+ end
151
177
  end
152
178
 
153
179
  local skillFunc = skills[taskData.command]
@@ -155,8 +181,16 @@ local function processTask(taskData)
155
181
  return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
156
182
  end
157
183
 
158
- local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
159
- result.id = taskData.id
184
+ local ok, result = pcall(function()
185
+ return skillFunc(targetInstance, taskData.data, ctx, taskData.target)
186
+ end)
187
+
188
+ if not ok then
189
+ result = { status = "error", error = "Internal Plugin Error: " .. tostring(result) }
190
+ end
191
+
192
+ result = result or { status = "success", result = "Eksekusi selesai (Tidak ada return value)" }
193
+ result.id = taskData.id
160
194
 
161
195
  if ui then
162
196
  local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
@@ -188,24 +222,11 @@ local function sendResponse(taskId, resultData)
188
222
  end)
189
223
  end
190
224
 
191
- -- ── Update UI berdasarkan state ───────────────────────────────────────────────
192
- local function updateUIStatus()
193
- if not ui then return end
194
- if not serverReachable then
195
- ui:setStatus("waiting")
196
- elseif mcpAgentActive then
197
- ui:setStatus("connected")
198
- else
199
- -- Server reachable tapi tidak ada AI agent
200
- -- Tampilkan sebagai connected (karena server sse bridge aktif)
201
- ui:setStatus("server_connected")
202
- end
203
- end
204
-
205
225
  -- ── Proses satu event dari SSE stream ────────────────────────────────────────
206
226
  local function handleSSELine(line)
207
227
  -- Format SSE: "data: {...json...}"
208
228
  local payload = line
229
+ if line:sub(1, 6) == "event:" then return end
209
230
  if line:sub(1, 5) == "data:" then
210
231
  payload = line:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
211
232
  end
@@ -227,8 +248,12 @@ local function handleSSELine(line)
227
248
  local changed = (mcpAgentActive ~= newActive)
228
249
  mcpAgentActive = newActive
229
250
  if ui then
230
- -- Jaga status tetap Connected selama bridge server hidup
231
- ui:setStatus("connected")
251
+ if mcpAgentActive then
252
+ ui:setStatus("connected")
253
+ else
254
+ ui:setStatus("server_connected")
255
+ end
256
+
232
257
  if newActive and changed then
233
258
  ui:addLog("[System] ✅ IDE Agent (Antigravity) aktif.", Color3.fromRGB(100, 255, 100))
234
259
  elseif not newActive and changed then
@@ -273,13 +298,16 @@ function startConnectionLoop()
273
298
  isManualDisconnect = false
274
299
  reconnectDelay = RECONNECT_DELAY_INITIAL
275
300
 
301
+ local loopId = HttpService:GenerateGUID(false)
302
+ currentLoopId = loopId
303
+
276
304
  if ui then
277
305
  ui:addLog("[System] NVStudio MCP v2.1.8 — Menghubungkan ke server...", Color3.fromRGB(200, 200, 200))
278
306
  end
279
307
 
280
308
  local consecutiveErrors = 0
281
309
 
282
- while isRunning and not isManualDisconnect do
310
+ while isRunning and not isManualDisconnect and currentLoopId == loopId do
283
311
  -- Stream / polling snapshot dari server
284
312
  local streamOk, streamRaw = pcall(function()
285
313
  return HttpService:GetAsync(
@@ -295,7 +323,7 @@ function startConnectionLoop()
295
323
  if not serverReachable then
296
324
  serverReachable = true
297
325
  if ui then
298
- ui:setStatus("connected")
326
+ ui:setStatus(mcpAgentActive and "connected" or "server_connected")
299
327
  ui:addLog("[System] ✅ Server MCP terhubung & siap.", Color3.fromRGB(100, 255, 150))
300
328
  end
301
329
  end
@@ -7,18 +7,28 @@ return function(targetInstance, data, ctx, targetPath)
7
7
  local newSuccess, newInst = pcall(function()
8
8
  local inst = Instance.new(config.ClassName)
9
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
10
  return inst
17
11
  end)
18
12
 
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." }
13
+ if not newSuccess then
14
+ return { status = "error", error = "Gagal membuat Instance: " .. tostring(newInst) }
15
+ end
16
+
17
+ local propResultMsg = ""
18
+ if config.Properties and next(config.Properties) then
19
+ local setPropsFunc = require(script.Parent:FindFirstChild("set_properties"))
20
+ if setPropsFunc then
21
+ local propData = ctx.HttpService:JSONEncode(config.Properties)
22
+ local propRes = setPropsFunc(newInst, propData, ctx, "")
23
+ if propRes.status == "error" then
24
+ propResultMsg = " (Namun ada error properti: " .. propRes.result .. ")"
25
+ elseif propRes.result then
26
+ propResultMsg = " (" .. propRes.result .. ")"
27
+ end
28
+ else
29
+ propResultMsg = " (Gagal meload set_properties)"
30
+ end
23
31
  end
32
+
33
+ return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath .. propResultMsg }
24
34
  end
@@ -1,4 +1,8 @@
1
1
  return function(targetInstance, data, ctx, targetPath)
2
+ if targetInstance == game or targetInstance.Parent == game then
3
+ return { status = "error", error = "Tidak dapat menghapus root DataModel atau Service utama!" }
4
+ end
5
+
2
6
  -- Guardrail: Cegah penghapusan Service utama
3
7
  local protectedServices = {
4
8
  "Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
@@ -19,6 +23,11 @@ return function(targetInstance, data, ctx, targetPath)
19
23
  local className = targetInstance.ClassName
20
24
  local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
21
25
 
26
+ local childrenCount = #targetInstance:GetDescendants()
27
+ if childrenCount > 10 and data ~= "force" then
28
+ return { status = "error", error = "GUARDRAIL: Instance '" .. name .. "' memiliki " .. childrenCount .. " descendants. Jika Anda yakin ingin menghapus seluruhnya beserta isinya, sertakan data='force'." }
29
+ end
30
+
22
31
  local success, err = pcall(function()
23
32
  targetInstance:Destroy()
24
33
  end)
@@ -10,7 +10,7 @@ return function(targetInstance, data, ctx, targetPath)
10
10
  local wrappedCode = "return function()\n" .. code .. "\nend"
11
11
 
12
12
  local moduleScript = Instance.new("ModuleScript")
13
- moduleScript.Name = "AI_Execute_Temp"
13
+ moduleScript.Name = "AI_Execute_" .. HttpService:GenerateGUID(false):gsub("-", "")
14
14
  moduleScript.Source = wrappedCode
15
15
 
16
16
  -- Taruh di ReplicatedStorage sementara agar bisa di-require
@@ -21,10 +21,17 @@ return function(targetInstance, data, ctx, targetPath)
21
21
  local isFinished = false
22
22
 
23
23
  -- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
24
- task.spawn(function()
24
+ local logs = {}
25
+
26
+ local execThread = task.spawn(function()
25
27
  local reqSuccess, func = pcall(function() return require(moduleScript) end)
26
28
  if reqSuccess and type(func) == "function" then
29
+ local LogService = game:GetService("LogService")
30
+ local connection = LogService.MessageOut:Connect(function(msg, msgType)
31
+ table.insert(logs, msg)
32
+ end)
27
33
  local execSuccess, res = pcall(func)
34
+ pcall(function() connection:Disconnect() end)
28
35
  if execSuccess then
29
36
  resultData = res
30
37
  else
@@ -42,20 +49,31 @@ return function(targetInstance, data, ctx, targetPath)
42
49
  timeWaited = timeWaited + task.wait()
43
50
  end
44
51
 
52
+
53
+ if not isFinished then
54
+ pcall(function() task.cancel(execThread) end)
55
+ end
56
+
45
57
  -- Pembersihan (Wajib dihancurkan)
46
58
  pcall(function() moduleScript:Destroy() end)
47
59
 
48
60
  if not isFinished then
49
- return { status = "error", error = "Timeout 5 detik terlampaui. Kemungkinan ada infinite loop tanpa task.wait()." }
61
+ return { status = "error", error = "Timeout 5 detik terlampaui. Thread mungkin tetap berjalan di background jika terjebak dalam infinite loop tanpa task.wait(). PASTIKAN menambahkan task.wait() di dalam loop komputasi berat." }
50
62
  end
51
63
 
52
64
  if execError then
53
- return { status = "error", error = execError }
65
+ return { status = "error", error = execError, logs = #logs > 0 and logs or nil }
54
66
  end
55
67
 
68
+ local returnPayload = { status = "success" }
69
+ if #logs > 0 then
70
+ returnPayload.logs = logs
71
+ end
72
+
56
73
  -- Cegah return nil error
57
74
  if resultData == nil then
58
- return { status = "success", result = "Eksekusi berhasil (Tidak ada nilai kembalian)." }
75
+ returnPayload.result = "Eksekusi berhasil (Tidak ada nilai kembalian)."
76
+ return returnPayload
59
77
  end
60
78
 
61
79
  -- JSON Encode untuk mencegah raw Instance return crash
@@ -64,7 +82,8 @@ return function(targetInstance, data, ctx, targetPath)
64
82
  end)
65
83
 
66
84
  if serializeSuccess then
67
- return { status = "success", result = HttpService:JSONDecode(jsonRes) } -- decode kembali agar format aslinya diteruskan oleh router
85
+ returnPayload.result = HttpService:JSONDecode(jsonRes) -- decode kembali agar format aslinya diteruskan oleh router
86
+ return returnPayload
68
87
  else
69
88
  return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
70
89
  end
@@ -6,7 +6,8 @@ return function(targetInstance, data, ctx, targetPath)
6
6
 
7
7
  local fillSuccess, err = pcall(function()
8
8
  local terrain = game.Workspace.Terrain
9
- local materialEnum = Enum.Material[config.Material] or Enum.Material.Grass
9
+ local matOk, materialEnum = pcall(function() return Enum.Material[config.Material] end)
10
+ if not matOk then materialEnum = Enum.Material.Grass end
10
11
 
11
12
  local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
12
13
  local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
@@ -1,6 +1,12 @@
1
1
  return function(targetInstance, data, ctx, targetPath)
2
2
  local children = {}
3
- for _, child in ipairs(targetInstance:GetChildren()) do
3
+ local allChildren = targetInstance:GetChildren()
4
+ local MAX_LIMIT = 200
5
+ for i, child in ipairs(allChildren) do
6
+ if i > MAX_LIMIT then
7
+ table.insert(children, "... (Terpotong, " .. tostring(#allChildren - MAX_LIMIT) .. " children lainnya tidak ditampilkan)")
8
+ break
9
+ end
4
10
  table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
5
11
  end
6
12
  return { status = "success", result = children }
@@ -0,0 +1,46 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local maxDepth = 2
3
+ if data and data ~= "" then
4
+ pcall(function() maxDepth = tonumber(data) or 2 end)
5
+ end
6
+
7
+ local totalNodes = 0
8
+ local HARD_LIMIT = 2000 -- safeguard
9
+
10
+ local hardLimitReached = false
11
+
12
+ local function traverse(inst, currentDepth)
13
+ if hardLimitReached then return nil end
14
+ totalNodes = totalNodes + 1
15
+ local node = { name = inst.Name, className = inst.ClassName }
16
+
17
+ if totalNodes >= HARD_LIMIT then
18
+ hardLimitReached = true
19
+ return { name = "... (terpotong, melebihi limit " .. HARD_LIMIT .. " nodes)", className = "Warning" }
20
+ end
21
+
22
+ if currentDepth >= maxDepth then
23
+ local childCount = #inst:GetChildren()
24
+ if childCount > 0 then
25
+ node.children = "... (" .. childCount .. " children tersembunyi)"
26
+ end
27
+ return node
28
+ end
29
+
30
+ local children = inst:GetChildren()
31
+ if #children > 0 then
32
+ node.children = {}
33
+ for _, child in ipairs(children) do
34
+ local childNode = traverse(child, currentDepth + 1)
35
+ if childNode then
36
+ table.insert(node.children, childNode)
37
+ end
38
+ end
39
+ end
40
+
41
+ return node
42
+ end
43
+
44
+ local tree = traverse(targetInstance, 0)
45
+ return { status = "success", result = tree }
46
+ end
@@ -1,6 +1,7 @@
1
1
  return function(targetInstance, data, ctx, targetPath)
2
2
  local InsertService = game:GetService("InsertService")
3
- local assetId = tonumber(data)
3
+ local rawId = tostring(data or ""):gsub("rbxassetid://", ""):gsub("%s+", "")
4
+ local assetId = tonumber(rawId)
4
5
  if not assetId then
5
6
  return { status = "error", error = "Asset ID harus berupa angka yang valid." }
6
7
  end
@@ -10,8 +11,12 @@ return function(targetInstance, data, ctx, targetPath)
10
11
  end)
11
12
 
12
13
  if success and model then
13
- model.Parent = game.Workspace
14
- return { status = "success", result = "Aset berhasil dimasukkan ke Workspace." }
14
+ if targetInstance == game or targetInstance.ClassName == "DataModel" then
15
+ model.Parent = game.Workspace
16
+ else
17
+ model.Parent = targetInstance
18
+ end
19
+ return { status = "success", result = "Aset berhasil dimasukkan ke " .. model.Parent.Name .. "." }
15
20
  else
16
21
  return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
17
22
  end