@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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cleanRobloxMarkdown = cleanRobloxMarkdown;
|
|
7
|
+
exports.getRobloxDocs = getRobloxDocs;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
11
|
+
const BASE_URL = 'https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/reference/engine';
|
|
12
|
+
const CACHE_DIR = path_1.default.join(__dirname, '..', '.cache', 'roblox-docs');
|
|
13
|
+
if (!fs_1.default.existsSync(CACHE_DIR)) {
|
|
14
|
+
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
function cleanRobloxMarkdown(text) {
|
|
17
|
+
if (!text)
|
|
18
|
+
return '';
|
|
19
|
+
return text
|
|
20
|
+
// Replace `Type.Target|Label` with `Label`
|
|
21
|
+
.replace(/`(?:Class|Datatype|Enum|Library)\.[^|`]+\|([^`]+)`/g, '`$1`')
|
|
22
|
+
// Replace `Type.Target` with `Target`
|
|
23
|
+
.replace(/`(?:Class|Datatype|Enum|Library)\.([^`]+)`/g, '`$1`')
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
const CATEGORIES = ['classes', 'datatypes', 'enums', 'libraries'];
|
|
27
|
+
function getCategoryFolder(cat) {
|
|
28
|
+
const c = cat.toLowerCase();
|
|
29
|
+
if (c.startsWith('class'))
|
|
30
|
+
return 'classes';
|
|
31
|
+
if (c.startsWith('data') || c.startsWith('type'))
|
|
32
|
+
return 'datatypes';
|
|
33
|
+
if (c.startsWith('enum'))
|
|
34
|
+
return 'enums';
|
|
35
|
+
if (c.startsWith('lib'))
|
|
36
|
+
return 'libraries';
|
|
37
|
+
return 'classes';
|
|
38
|
+
}
|
|
39
|
+
function getCachePath(category, name) {
|
|
40
|
+
const catFolder = path_1.default.join(CACHE_DIR, category);
|
|
41
|
+
if (!fs_1.default.existsSync(catFolder)) {
|
|
42
|
+
fs_1.default.mkdirSync(catFolder, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
return path_1.default.join(catFolder, `${name}.yaml`);
|
|
45
|
+
}
|
|
46
|
+
async function fetchFromNetwork(category, query) {
|
|
47
|
+
const url = `${BASE_URL}/${category}/${query}.yaml`;
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'nvstudio-mcp-roblox-docs' } });
|
|
50
|
+
if (!res.ok)
|
|
51
|
+
return null;
|
|
52
|
+
return await res.text();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function resolveDocYaml(query, categoryFilter = 'auto') {
|
|
59
|
+
// Normalize query casing (capitalized first letter standard)
|
|
60
|
+
const normalizedQuery = query.trim();
|
|
61
|
+
const searchCategories = categoryFilter !== 'auto'
|
|
62
|
+
? [getCategoryFolder(categoryFilter)]
|
|
63
|
+
: ['classes', 'datatypes', 'enums', 'libraries'];
|
|
64
|
+
// 1. Cek disk cache
|
|
65
|
+
for (const cat of searchCategories) {
|
|
66
|
+
const cacheFile = getCachePath(cat, normalizedQuery);
|
|
67
|
+
if (fs_1.default.existsSync(cacheFile)) {
|
|
68
|
+
try {
|
|
69
|
+
const cached = fs_1.default.readFileSync(cacheFile, 'utf-8');
|
|
70
|
+
if (cached && cached.length > 50) {
|
|
71
|
+
return { yamlText: cached, category: cat };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// ignore cache read error
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 2. Fetch dari network
|
|
80
|
+
for (const cat of searchCategories) {
|
|
81
|
+
const rawYaml = await fetchFromNetwork(cat, normalizedQuery);
|
|
82
|
+
if (rawYaml && rawYaml.length > 50) {
|
|
83
|
+
// Simpan ke cache
|
|
84
|
+
try {
|
|
85
|
+
fs_1.default.writeFileSync(getCachePath(cat, normalizedQuery), rawYaml, 'utf-8');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// ignore cache write error
|
|
89
|
+
}
|
|
90
|
+
return { yamlText: rawYaml, category: cat };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function formatSingleMember(parentName, member, kind) {
|
|
96
|
+
const name = member.name || member.Name || 'Unknown';
|
|
97
|
+
const summary = cleanRobloxMarkdown(member.summary || member.description || '');
|
|
98
|
+
const desc = cleanRobloxMarkdown(member.description || '');
|
|
99
|
+
let out = `## \`${name}\` (${kind})\n\n`;
|
|
100
|
+
if (summary)
|
|
101
|
+
out += `> ${summary}\n\n`;
|
|
102
|
+
if (member.type)
|
|
103
|
+
out += `* **Type:** \`${member.type}\`\n`;
|
|
104
|
+
if (member.default !== undefined && member.default !== null)
|
|
105
|
+
out += `* **Default:** \`${member.default}\`\n`;
|
|
106
|
+
const tags = (member.tags || []).join(', ');
|
|
107
|
+
if (tags)
|
|
108
|
+
out += `* **Tags:** ${tags}\n`;
|
|
109
|
+
if (member.deprecation_message) {
|
|
110
|
+
out += `\n> ⚠️ **Deprecated:** ${cleanRobloxMarkdown(member.deprecation_message)}\n\n`;
|
|
111
|
+
}
|
|
112
|
+
// Parameters
|
|
113
|
+
if (Array.isArray(member.parameters) && member.parameters.length > 0) {
|
|
114
|
+
out += `\n### Parameters\n`;
|
|
115
|
+
for (const p of member.parameters) {
|
|
116
|
+
const def = p.default !== undefined && p.default !== null ? ` = \`${p.default}\`` : '';
|
|
117
|
+
out += `* \`${p.name}\`: \`${p.type || 'any'}\`${def} — ${cleanRobloxMarkdown(p.summary || '')}\n`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Returns
|
|
121
|
+
if (Array.isArray(member.returns) && member.returns.length > 0) {
|
|
122
|
+
out += `\n### Returns\n`;
|
|
123
|
+
for (const r of member.returns) {
|
|
124
|
+
out += `* \`${r.type || 'void'}\` — ${cleanRobloxMarkdown(r.summary || '')}\n`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (desc && desc !== summary) {
|
|
128
|
+
out += `\n### Details\n${desc}\n`;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
async function getRobloxDocs(query, member, category = 'auto') {
|
|
133
|
+
const resolved = await resolveDocYaml(query, category);
|
|
134
|
+
if (!resolved) {
|
|
135
|
+
return `❌ Dokumentasi Roblox untuk "${query}" tidak ditemukan di Classes, Data types, Enums, maupun Libraries.\nPastikan ejaan ClassName atau Enum sudah benar (case-sensitive, contoh: 'TweenService', 'CanvasGroup', 'Vector3', 'EasingStyle').`;
|
|
136
|
+
}
|
|
137
|
+
let doc;
|
|
138
|
+
try {
|
|
139
|
+
doc = yaml_1.default.parse(resolved.yamlText);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return `❌ Gagal mem-parse format YAML untuk "${query}": ${err.message}`;
|
|
143
|
+
}
|
|
144
|
+
const name = doc.name || query;
|
|
145
|
+
const docType = doc.type || resolved.category;
|
|
146
|
+
const summary = cleanRobloxMarkdown(doc.summary || '');
|
|
147
|
+
const description = cleanRobloxMarkdown(doc.description || '');
|
|
148
|
+
// JIKA MEMBER SPESIFIK DICARI (Contoh: 'Create', 'GroupTransparency')
|
|
149
|
+
if (member) {
|
|
150
|
+
const memberNorm = member.trim().toLowerCase();
|
|
151
|
+
const allMembers = [];
|
|
152
|
+
const collect = (arr, kind) => {
|
|
153
|
+
if (Array.isArray(arr)) {
|
|
154
|
+
for (const item of arr)
|
|
155
|
+
allMembers.push({ item, kind });
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
collect(doc.constructors, 'Constructor');
|
|
159
|
+
collect(doc.properties, 'Property');
|
|
160
|
+
collect(doc.methods, 'Method');
|
|
161
|
+
collect(doc.functions, 'Function');
|
|
162
|
+
collect(doc.events, 'Event');
|
|
163
|
+
collect(doc.callbacks, 'Callback');
|
|
164
|
+
collect(doc.items, 'Enum Item');
|
|
165
|
+
collect(doc.constants, 'Constant');
|
|
166
|
+
const matched = allMembers.find(({ item }) => {
|
|
167
|
+
const full = String(item.name || '').toLowerCase();
|
|
168
|
+
return full === memberNorm || full.endsWith(`.${memberNorm}`) || full.endsWith(`:${memberNorm}`);
|
|
169
|
+
});
|
|
170
|
+
if (matched) {
|
|
171
|
+
let out = `# ${name} : ${matched.item.name || member}\n\n`;
|
|
172
|
+
out += formatSingleMember(name, matched.item, matched.kind);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
return `⚠️ Member "${member}" tidak ditemukan di dalam ${name}.\nAnggota yang tersedia: ` +
|
|
176
|
+
allMembers.slice(0, 15).map(({ item }) => `\`${item.name}\``).join(', ') +
|
|
177
|
+
(allMembers.length > 15 ? ` ...dan ${allMembers.length - 15} lainnya.` : '');
|
|
178
|
+
}
|
|
179
|
+
// TAMPILAN PENUH OVERVIEW DOKUMENTASI
|
|
180
|
+
let md = `# Roblox Engine: ${name} (${docType})\n\n`;
|
|
181
|
+
if (doc.deprecation_message) {
|
|
182
|
+
md += `> ⚠️ **DEPRECATED:** ${cleanRobloxMarkdown(doc.deprecation_message)}\n\n`;
|
|
183
|
+
}
|
|
184
|
+
if (summary) {
|
|
185
|
+
md += `> ${summary}\n\n`;
|
|
186
|
+
}
|
|
187
|
+
if (doc.inherits) {
|
|
188
|
+
md += `* **Inherits from:** \`${doc.inherits}\`\n`;
|
|
189
|
+
}
|
|
190
|
+
if (doc.memory_category) {
|
|
191
|
+
md += `* **Memory Category:** \`${doc.memory_category}\`\n`;
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(doc.tags) && doc.tags.length > 0) {
|
|
194
|
+
md += `* **Tags:** ${doc.tags.join(', ')}\n`;
|
|
195
|
+
}
|
|
196
|
+
md += `\n---\n\n`;
|
|
197
|
+
if (description && description !== summary) {
|
|
198
|
+
md += `### Description\n${description}\n\n`;
|
|
199
|
+
}
|
|
200
|
+
// CONSTRUCTORS
|
|
201
|
+
if (Array.isArray(doc.constructors) && doc.constructors.length > 0) {
|
|
202
|
+
md += `### Constructors\n`;
|
|
203
|
+
for (const c of doc.constructors) {
|
|
204
|
+
const params = (c.parameters || []).map((p) => `${p.name}: ${p.type || 'any'}`).join(', ');
|
|
205
|
+
md += `* \`${c.name}(${params})\`\n ${cleanRobloxMarkdown(c.summary || '')}\n`;
|
|
206
|
+
}
|
|
207
|
+
md += `\n`;
|
|
208
|
+
}
|
|
209
|
+
// PROPERTIES TABLE
|
|
210
|
+
if (Array.isArray(doc.properties) && doc.properties.length > 0) {
|
|
211
|
+
md += `### Properties\n\n`;
|
|
212
|
+
md += `| Property | Type | Security | Summary |\n`;
|
|
213
|
+
md += `| :--- | :--- | :--- | :--- |\n`;
|
|
214
|
+
for (const p of doc.properties) {
|
|
215
|
+
const pName = p.name ? p.name.replace(/^[^.]+\./, '') : 'Unknown';
|
|
216
|
+
const pType = `\`${p.type || 'any'}\``;
|
|
217
|
+
const sec = p.security?.read ? `${p.security.read}` : 'None';
|
|
218
|
+
const pSummary = cleanRobloxMarkdown(p.summary || '').replace(/[\r\n]+/g, ' ').slice(0, 100);
|
|
219
|
+
md += `| **\`${pName}\`** | ${pType} | ${sec} | ${pSummary} |\n`;
|
|
220
|
+
}
|
|
221
|
+
md += `\n`;
|
|
222
|
+
}
|
|
223
|
+
// METHODS
|
|
224
|
+
if (Array.isArray(doc.methods) && doc.methods.length > 0) {
|
|
225
|
+
md += `### Methods\n`;
|
|
226
|
+
for (const m of doc.methods) {
|
|
227
|
+
const params = (m.parameters || []).map((p) => `${p.name}: ${p.type || 'any'}`).join(', ');
|
|
228
|
+
const returns = (m.returns || []).map((r) => r.type || 'void').join(', ') || 'void';
|
|
229
|
+
md += `* \`${m.name}(${params}) -> ${returns}\`\n ${cleanRobloxMarkdown(m.summary || '')}\n`;
|
|
230
|
+
}
|
|
231
|
+
md += `\n`;
|
|
232
|
+
}
|
|
233
|
+
// FUNCTIONS (Libraries / Static)
|
|
234
|
+
if (Array.isArray(doc.functions) && doc.functions.length > 0) {
|
|
235
|
+
md += `### Functions\n`;
|
|
236
|
+
for (const f of doc.functions) {
|
|
237
|
+
const params = (f.parameters || []).map((p) => `${p.name}: ${p.type || 'any'}`).join(', ');
|
|
238
|
+
const returns = (f.returns || []).map((r) => r.type || 'void').join(', ') || 'void';
|
|
239
|
+
md += `* \`${f.name}(${params}) -> ${returns}\`\n ${cleanRobloxMarkdown(f.summary || '')}\n`;
|
|
240
|
+
}
|
|
241
|
+
md += `\n`;
|
|
242
|
+
}
|
|
243
|
+
// EVENTS
|
|
244
|
+
if (Array.isArray(doc.events) && doc.events.length > 0) {
|
|
245
|
+
md += `### Events\n`;
|
|
246
|
+
for (const e of doc.events) {
|
|
247
|
+
const params = (e.parameters || []).map((p) => `${p.name}: ${p.type || 'any'}`).join(', ');
|
|
248
|
+
md += `* \`${e.name}(${params})\`\n ${cleanRobloxMarkdown(e.summary || '')}\n`;
|
|
249
|
+
}
|
|
250
|
+
md += `\n`;
|
|
251
|
+
}
|
|
252
|
+
// ENUM ITEMS
|
|
253
|
+
if (Array.isArray(doc.items) && doc.items.length > 0) {
|
|
254
|
+
md += `### Enum Items\n\n`;
|
|
255
|
+
md += `| Item | Value | Summary |\n`;
|
|
256
|
+
md += `| :--- | :--- | :--- |\n`;
|
|
257
|
+
for (const it of doc.items) {
|
|
258
|
+
const itSummary = cleanRobloxMarkdown(it.summary || '').replace(/[\r\n]+/g, ' ');
|
|
259
|
+
md += `| **\`${it.name}\`** | \`${it.value}\` | ${itSummary} |\n`;
|
|
260
|
+
}
|
|
261
|
+
md += `\n`;
|
|
262
|
+
}
|
|
263
|
+
return md.trim();
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adiholick/nvstudio-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Local Bridge Server MCP for Roblox Studio",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"public",
|
|
12
|
-
"studio-plugin"
|
|
12
|
+
"studio-plugin",
|
|
13
|
+
"agent-plugin"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsc && npm run build:plugin",
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
"express": "^5.2.1",
|
|
30
31
|
"socket.io": "^4.8.3",
|
|
31
32
|
"uuid": "^14.0.2",
|
|
32
|
-
"ws": "^8.22.0"
|
|
33
|
+
"ws": "^8.22.0",
|
|
34
|
+
"yaml": "^2.9.1"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"@types/express": "^5.0.6",
|