@adiholick/nvstudio-mcp 2.2.3 → 2.3.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.
- package/README.md +43 -47
- package/agent-plugin/mcp_config.json +8 -0
- package/agent-plugin/plugin.json +6 -0
- package/agent-plugin/rules/AGENTS.md +87 -0
- package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
- package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +69 -0
- package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
- package/dist/build-plugin.js +55 -0
- package/dist/index.js +216 -3
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +44 -4
- package/dist/roblox-docs.js +264 -0
- package/package.json +5 -3
- package/public/index.html +619 -108
- package/studio-plugin/Main.server.lua +1 -1
- package/studio-plugin/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/insert_asset.lua +85 -12
- package/studio-plugin/nvstudio_mcp.rbxmx +885 -14
- package/studio-plugin/preview_asset.lua +99 -0
- package/studio-plugin/scan_security.lua +81 -0
- package/studio-plugin/security_scanner.lua +346 -0
- package/studio-plugin/set_properties.lua +10 -0
- package/studio-plugin/ui_manager.lua +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ══════════════════════════════════════════════════════════════════════
|
|
2
|
-
-- NVStudio MCP Plugin v2.
|
|
2
|
+
-- NVStudio MCP Plugin v2.3.0
|
|
3
3
|
-- Arsitektur: Dual-Mode WebSocket Duplex (Port 3055/ws) + Fallback Long-Polling
|
|
4
4
|
-- Plugin auto-connect saat dimuat via WebSocket native, dengan fallback otomatis
|
|
5
5
|
-- ke HTTP stream jika WebSocket tidak tersedia di environment Roblox Studio ini.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- ============================================================================
|
|
3
|
+
-- Skill Module: batch_create_instances
|
|
4
|
+
-- Deskripsi: Pembangun Hierarki Instance Massal & Deklaratif (Tree / Batch List)
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
local HttpService = game:GetService("HttpService")
|
|
8
|
+
|
|
9
|
+
return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
|
|
10
|
+
local success, config = pcall(function()
|
|
11
|
+
if type(data) == "table" then return data end
|
|
12
|
+
return HttpService:JSONDecode(tostring(data))
|
|
13
|
+
end)
|
|
14
|
+
|
|
15
|
+
if not success or type(config) ~= "table" then
|
|
16
|
+
return { status = "error", error = "Format data JSON tidak valid untuk batch_create_instances." }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
local setPropsFunc = require(script.Parent:WaitForChild("set_properties"))
|
|
20
|
+
local defaultParent = targetInstance or game.Workspace
|
|
21
|
+
|
|
22
|
+
if config.target and ctx and ctx.getInstanceFromPath then
|
|
23
|
+
local resolved = ctx.getInstanceFromPath(config.target)
|
|
24
|
+
if resolved then defaultParent = resolved end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
local createdList = {}
|
|
28
|
+
local errors = {}
|
|
29
|
+
local createdCount = 0
|
|
30
|
+
|
|
31
|
+
local function applyProps(inst: Instance, props: any)
|
|
32
|
+
if not props or type(props) ~= "table" or not next(props) then return end
|
|
33
|
+
local propJson = HttpService:JSONEncode(props)
|
|
34
|
+
local res = setPropsFunc(inst, propJson, ctx, inst:GetFullName())
|
|
35
|
+
if res and res.status == "error" then
|
|
36
|
+
table.insert(errors, string.format("[%s] Error properti: %s", inst.Name, tostring(res.result or res.error)))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
-- 1. MODE HIERARCHICAL TREE (Pohon Bersarang)
|
|
41
|
+
local function buildTreeNode(parentInst: Instance, node: any): Instance?
|
|
42
|
+
local className = node.className or node.ClassName
|
|
43
|
+
if not className or type(className) ~= "string" then
|
|
44
|
+
table.insert(errors, "Node pohon tidak memiliki ClassName yang valid.")
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local ok, inst = pcall(function()
|
|
49
|
+
local newObj = Instance.new(className)
|
|
50
|
+
if node.name or node.Name then
|
|
51
|
+
newObj.Name = tostring(node.name or node.Name)
|
|
52
|
+
end
|
|
53
|
+
newObj.Parent = parentInst
|
|
54
|
+
return newObj
|
|
55
|
+
end)
|
|
56
|
+
|
|
57
|
+
if not ok or not inst then
|
|
58
|
+
table.insert(errors, string.format("Gagal membuat instance '%s': %s", className, tostring(inst)))
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
createdCount = createdCount + 1
|
|
63
|
+
table.insert(createdList, inst:GetFullName())
|
|
64
|
+
|
|
65
|
+
-- Terapkan properti
|
|
66
|
+
applyProps(inst, node.properties or node.Properties)
|
|
67
|
+
|
|
68
|
+
-- Rekursif untuk seluruh child
|
|
69
|
+
local children = node.children or node.Children
|
|
70
|
+
if children and type(children) == "table" then
|
|
71
|
+
for _, childNode in ipairs(children) do
|
|
72
|
+
buildTreeNode(inst, childNode)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
return inst
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
-- Eksekusi Mode Tree jika disediakan
|
|
80
|
+
if config.tree and type(config.tree) == "table" then
|
|
81
|
+
buildTreeNode(defaultParent, config.tree)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
-- 2. MODE FLAT ARRAY (Daftar Instance Terpisah)
|
|
85
|
+
local instancesArray = config.instances or config.items
|
|
86
|
+
if instancesArray and type(instancesArray) == "table" then
|
|
87
|
+
for idx, item in ipairs(instancesArray) do
|
|
88
|
+
local parentInst = defaultParent
|
|
89
|
+
if item.parent and ctx and ctx.getInstanceFromPath then
|
|
90
|
+
local resParent = ctx.getInstanceFromPath(item.parent)
|
|
91
|
+
if resParent then parentInst = resParent end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
local className = item.className or item.ClassName
|
|
95
|
+
if className then
|
|
96
|
+
local ok, inst = pcall(function()
|
|
97
|
+
local newObj = Instance.new(className)
|
|
98
|
+
if item.name or item.Name then
|
|
99
|
+
newObj.Name = tostring(item.name or item.Name)
|
|
100
|
+
end
|
|
101
|
+
newObj.Parent = parentInst
|
|
102
|
+
return newObj
|
|
103
|
+
end)
|
|
104
|
+
|
|
105
|
+
if ok and inst then
|
|
106
|
+
createdCount = createdCount + 1
|
|
107
|
+
table.insert(createdList, inst:GetFullName())
|
|
108
|
+
applyProps(inst, item.properties or item.Properties)
|
|
109
|
+
else
|
|
110
|
+
table.insert(errors, string.format("Item #%d (%s) gagal dibuat: %s", idx, tostring(className), tostring(inst)))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
local status = (createdCount > 0 or #errors == 0) and "success" or "error"
|
|
117
|
+
return {
|
|
118
|
+
status = status,
|
|
119
|
+
result = {
|
|
120
|
+
totalCreated = createdCount,
|
|
121
|
+
created = createdList,
|
|
122
|
+
hasErrors = (#errors > 0),
|
|
123
|
+
errors = errors,
|
|
124
|
+
message = string.format("Berhasil membuat %d instance secara batch.", createdCount)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- ============================================================================
|
|
3
|
+
-- Skill Module: batch_set_properties
|
|
4
|
+
-- Deskripsi: Modifikasi Properti Massal untuk Beberapa Instance Sekaligus
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
local HttpService = game:GetService("HttpService")
|
|
8
|
+
|
|
9
|
+
return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
|
|
10
|
+
local success, config = pcall(function()
|
|
11
|
+
if type(data) == "table" then return data end
|
|
12
|
+
return HttpService:JSONDecode(tostring(data))
|
|
13
|
+
end)
|
|
14
|
+
|
|
15
|
+
if not success or type(config) ~= "table" then
|
|
16
|
+
return { status = "error", error = "Format data JSON tidak valid untuk batch_set_properties." }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
local setPropsFunc = require(script.Parent:WaitForChild("set_properties"))
|
|
20
|
+
local updates = config.updates or config
|
|
21
|
+
if type(updates) ~= "table" then
|
|
22
|
+
return { status = "error", error = "Parameter 'updates' harus berupa array pembaruan properti." }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
local updatedCount = 0
|
|
26
|
+
local successfulTargets = {}
|
|
27
|
+
local errors = {}
|
|
28
|
+
|
|
29
|
+
for idx, updateItem in ipairs(updates) do
|
|
30
|
+
local inst = targetInstance
|
|
31
|
+
local instPath = updateItem.target or updateItem.path
|
|
32
|
+
if instPath and ctx and ctx.getInstanceFromPath then
|
|
33
|
+
local resolved = ctx.getInstanceFromPath(instPath)
|
|
34
|
+
if resolved then
|
|
35
|
+
inst = resolved
|
|
36
|
+
else
|
|
37
|
+
table.insert(errors, string.format("Item #%d: Target path '%s' tidak ditemukan.", idx, tostring(instPath)))
|
|
38
|
+
continue
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
local props = updateItem.properties or updateItem.props
|
|
43
|
+
if inst and props and type(props) == "table" then
|
|
44
|
+
local propJson = HttpService:JSONEncode(props)
|
|
45
|
+
local res = setPropsFunc(inst, propJson, ctx, inst:GetFullName())
|
|
46
|
+
if res and res.status == "error" then
|
|
47
|
+
table.insert(errors, string.format("[%s]: %s", inst.Name, tostring(res.result or res.error)))
|
|
48
|
+
else
|
|
49
|
+
updatedCount = updatedCount + 1
|
|
50
|
+
table.insert(successfulTargets, inst:GetFullName())
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
table.insert(errors, string.format("Item #%d: Instance atau properties tidak valid.", idx))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
local status = (updatedCount > 0 or #errors == 0) and "success" or "error"
|
|
58
|
+
return {
|
|
59
|
+
status = status,
|
|
60
|
+
result = {
|
|
61
|
+
totalUpdated = updatedCount,
|
|
62
|
+
updatedInstances = successfulTargets,
|
|
63
|
+
hasErrors = (#errors > 0),
|
|
64
|
+
errors = errors,
|
|
65
|
+
message = string.format("Berhasil memperbarui properti untuk %d instance.", updatedCount)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
@@ -1,23 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
-- ============================================================================
|
|
3
|
+
-- Skill Module: insert_asset (Upgraded with Asset Security Scanner & Script Stripper)
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
|
|
6
|
+
local HttpService = game:GetService("HttpService")
|
|
7
|
+
local InsertService = game:GetService("InsertService")
|
|
8
|
+
|
|
9
|
+
local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
|
|
10
|
+
|
|
11
|
+
return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
|
|
12
|
+
local assetIdStr = ""
|
|
13
|
+
local stripScripts = true -- Default TRUE demi keamanan mutlak
|
|
14
|
+
local scanSecurity = true
|
|
15
|
+
|
|
16
|
+
if typeof(data) == "string" then
|
|
17
|
+
local success, parsed = pcall(function()
|
|
18
|
+
return HttpService:JSONDecode(data)
|
|
19
|
+
end)
|
|
20
|
+
if success and typeof(parsed) == "table" then
|
|
21
|
+
assetIdStr = tostring(parsed.asset_id or parsed.assetId or "")
|
|
22
|
+
if parsed.strip_scripts ~= nil then stripScripts = parsed.strip_scripts end
|
|
23
|
+
if parsed.scan_security ~= nil then scanSecurity = parsed.scan_security end
|
|
24
|
+
else
|
|
25
|
+
assetIdStr = data
|
|
26
|
+
end
|
|
27
|
+
elseif typeof(data) == "table" then
|
|
28
|
+
assetIdStr = tostring(data.asset_id or data.assetId or "")
|
|
29
|
+
if data.strip_scripts ~= nil then stripScripts = data.strip_scripts end
|
|
30
|
+
if data.scan_security ~= nil then scanSecurity = data.scan_security end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
local rawId = string.gsub(assetIdStr, "rbxassetid://", "")
|
|
34
|
+
rawId = string.gsub(rawId, "%s+", "")
|
|
4
35
|
local assetId = tonumber(rawId)
|
|
5
|
-
|
|
36
|
+
|
|
37
|
+
if not assetId or assetId <= 0 then
|
|
6
38
|
return { status = "error", error = "Asset ID harus berupa angka yang valid." }
|
|
7
39
|
end
|
|
8
|
-
|
|
40
|
+
|
|
9
41
|
local success, model = pcall(function()
|
|
10
42
|
return InsertService:LoadAsset(assetId)
|
|
11
43
|
end)
|
|
12
|
-
|
|
13
|
-
if success
|
|
14
|
-
|
|
15
|
-
|
|
44
|
+
|
|
45
|
+
if not success or not model then
|
|
46
|
+
return {
|
|
47
|
+
status = "error",
|
|
48
|
+
error = "Gagal memuat aset. Pastikan Asset ID benar dan model berstatus publik di Creator Store."
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
local securityNotes = {}
|
|
53
|
+
local removedScripts = {}
|
|
54
|
+
|
|
55
|
+
-- 1. Scan keamanan jika diaktifkan
|
|
56
|
+
if scanSecurity then
|
|
57
|
+
local scanResult = SecurityScanner.scanInstance(model, true)
|
|
58
|
+
if scanResult.riskLevel == "DANGEROUS" then
|
|
59
|
+
table.insert(securityNotes, string.format("⚠️ PERINGATAN: Aset ini memiliki %d ancaman BERBAHAYA (Backdoor/Obfuscation/Webhook)!", scanResult.highThreats))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
-- 2. Sanitasi: Hapus skrip jika strip_scripts = true
|
|
64
|
+
if stripScripts then
|
|
65
|
+
removedScripts = SecurityScanner.stripScripts(model)
|
|
66
|
+
if #removedScripts > 0 then
|
|
67
|
+
table.insert(securityNotes, string.format("🛡️ Keamanan: %d skrip bawaan telah dihapus secara otomatis demi keamanan game Anda.", #removedScripts))
|
|
16
68
|
else
|
|
17
|
-
|
|
69
|
+
table.insert(securityNotes, "🛡️ Keamanan: Aset bersih, tidak ditemukan skrip berbahaya.")
|
|
18
70
|
end
|
|
19
|
-
return { status = "success", result = "Aset berhasil dimasukkan ke " .. model.Parent.Name .. "." }
|
|
20
71
|
else
|
|
21
|
-
|
|
72
|
+
table.insert(securityNotes, "ℹ️ Catatan: strip_scripts = false, skrip bawaan model dipertahankan.")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
-- 3. Masukkan model ke target
|
|
76
|
+
local parentTarget = targetInstance
|
|
77
|
+
if not parentTarget or parentTarget == game or parentTarget.ClassName == "DataModel" then
|
|
78
|
+
parentTarget = game.Workspace
|
|
22
79
|
end
|
|
80
|
+
|
|
81
|
+
model.Parent = parentTarget
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
status = "success",
|
|
85
|
+
result = {
|
|
86
|
+
message = string.format("Aset '%s' (ID: %d) berhasil dimasukkan ke %s.", model.Name, assetId, parentTarget.Name),
|
|
87
|
+
assetName = model.Name,
|
|
88
|
+
assetId = assetId,
|
|
89
|
+
parent = parentTarget:GetFullName(),
|
|
90
|
+
stripScriptsApplied = stripScripts,
|
|
91
|
+
removedScriptsCount = #removedScripts,
|
|
92
|
+
removedScripts = removedScripts,
|
|
93
|
+
securityNotes = table.concat(securityNotes, "\n"),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
23
96
|
end
|