@weppy/roblox-mcp 2.2.2 → 2.3.1
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/.claude-plugin/marketplace.json +2 -2
- package/.github/workflows/install-test.yml +108 -0
- package/CHANGELOG.md +31 -0
- package/README.md +14 -10
- package/docs/en/installation/README.md +4 -4
- package/docs/en/installation/roblox-explorer.md +13 -3
- package/docs/en/installation/roblox-plugin.md +42 -31
- package/docs/en/pro-upgrade.md +26 -12
- package/docs/en/sync/luau-lsp.md +41 -0
- package/docs/en/sync/overview.md +4 -1
- package/docs/es/README.md +13 -7
- package/docs/es/installation/README.md +4 -4
- package/docs/es/pro-upgrade.md +26 -12
- package/docs/es/sync/luau-lsp.md +41 -0
- package/docs/es/sync/overview.md +4 -1
- package/docs/id/README.md +12 -6
- package/docs/id/installation/README.md +4 -4
- package/docs/id/pro-upgrade.md +26 -12
- package/docs/id/sync/luau-lsp.md +41 -0
- package/docs/id/sync/overview.md +4 -1
- package/docs/installer/assets/index-Bz0amd7x.js +63 -0
- package/docs/installer/assets/index-ei4lRUa6.css +1 -0
- package/docs/installer/index.html +14 -0
- package/docs/installer/manifest.webmanifest +15 -0
- package/docs/installer/sw.js +7 -0
- package/docs/installer/wrox-icon.png +0 -0
- package/docs/ja/README.md +14 -10
- package/docs/ja/installation/README.md +3 -3
- package/docs/ja/pro-upgrade.md +26 -12
- package/docs/ja/sync/luau-lsp.md +41 -0
- package/docs/ja/sync/overview.md +4 -1
- package/docs/ko/README.md +14 -10
- package/docs/ko/installation/README.md +4 -4
- package/docs/ko/installation/roblox-explorer.md +13 -3
- package/docs/ko/installation/roblox-plugin.md +42 -31
- package/docs/ko/pro-upgrade.md +26 -12
- package/docs/ko/sync/luau-lsp.md +41 -0
- package/docs/ko/sync/overview.md +4 -1
- package/docs/pt-br/README.md +13 -7
- package/docs/pt-br/installation/README.md +4 -4
- package/docs/pt-br/pro-upgrade.md +26 -12
- package/docs/pt-br/sync/luau-lsp.md +41 -0
- package/docs/pt-br/sync/overview.md +4 -1
- package/docs/troubleshooting.md +1 -1
- package/install.ps1 +468 -98
- package/install.sh +461 -71
- package/llms-full.txt +14 -2
- package/llms.txt +10 -4
- package/package.json +1 -1
- package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogDetailPage-D7eMrarv.js → ChangelogDetailPage-ITTDURna.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogPage-DFCCRyyK.js → ChangelogPage-DjVot-60.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ConfirmModal-BmRJ2JXZ.js → ConfirmModal-B1q8BGeA.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-9bG71eB1.css +1 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-D4y36l03.js +1 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{InfoLabel-CCDWZLC9.js → InfoLabel-COMRAIq0.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{OverviewPage-BHpt3LI2.js → OverviewPage-DojgIxVT.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{PlaytestPage-CNwwI5Ro.js → PlaytestPage-CkEvf6XW.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/PropertyDiff-r9iLxfi8.js +6 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{SettingsPage-CPqQYZPN.js → SettingsPage-CEs6Ob3d.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{StatusBadge-C8VKAPpk.js → StatusBadge-DHhSWmAT.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SyncPage-ifjnfsNg.js +4 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{TierComparison-7ofkPwj3.js → TierComparison-C29caZ6C.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{TierPromoProgress-SnRUjAPh.js → TierPromoProgress-BdEtTxkK.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ToolsPage-CrdNh3D9.js → ToolsPage-BOIC0ngW.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-CQYn3Wfp.js +129 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{useLiveUptime-BnXeLpOw.js → useLiveUptime-Dco8Aiiz.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/index.html +1 -1
- package/plugins/weppy-roblox-mcp/dist/index.js +86 -83
- package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +0 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-CN3LYLAT.css +0 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-CiaCY026.js +0 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/PropertyDiff-DIplDn-J.js +0 -6
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SyncPage-DTSKbpio.js +0 -4
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-DGGmfli1.js +0 -129
package/install.sh
CHANGED
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
# Usage:
|
|
6
6
|
# curl -fsSL https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/install.sh | bash
|
|
7
7
|
#
|
|
8
|
-
# Interactive
|
|
9
|
-
# [1/
|
|
10
|
-
# [2/
|
|
11
|
-
# [3/3] Register MCP with AI apps (user selection)
|
|
8
|
+
# Interactive 2 steps:
|
|
9
|
+
# [1/2] Setup — install Roblox Studio Plugin via npx
|
|
10
|
+
# [2/2] Register MCP with AI apps (user selection)
|
|
12
11
|
#
|
|
13
12
|
|
|
14
13
|
set -euo pipefail
|
|
@@ -74,6 +73,10 @@ trap 'handle_install_error "${LINENO}" "$BASH_COMMAND"' ERR
|
|
|
74
73
|
confirm() {
|
|
75
74
|
local prompt="$1"
|
|
76
75
|
local reply
|
|
76
|
+
if [ "${CI:-}" = "true" ]; then
|
|
77
|
+
printf "%s (Y/n): Y\n" "$prompt"
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
77
80
|
printf "%s (Y/n): " "$prompt"
|
|
78
81
|
read -r reply </dev/tty
|
|
79
82
|
reply="${reply:-Y}"
|
|
@@ -117,7 +120,7 @@ try {
|
|
|
117
120
|
' >/dev/null 2>&1
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
#
|
|
123
|
+
# Antigravity 설정에 canonical mcpServers 래퍼로 MCP 서버를 추가하고 legacy flat key를 정리
|
|
121
124
|
add_antigravity_mcp_config() {
|
|
122
125
|
local config_path="$1"
|
|
123
126
|
local parent_dir
|
|
@@ -128,7 +131,20 @@ const fs = require("fs");
|
|
|
128
131
|
const configPath = process.env.MCP_CONFIG_PATH;
|
|
129
132
|
let config = {};
|
|
130
133
|
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
131
|
-
config
|
|
134
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
135
|
+
config = {};
|
|
136
|
+
}
|
|
137
|
+
const mcpServers = config.mcpServers;
|
|
138
|
+
if (mcpServers !== undefined && (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers))) {
|
|
139
|
+
throw new Error("Antigravity mcpServers must be an object");
|
|
140
|
+
}
|
|
141
|
+
const next = { ...config };
|
|
142
|
+
delete next["weppy-roblox-mcp"];
|
|
143
|
+
next.mcpServers = {
|
|
144
|
+
...(mcpServers || {}),
|
|
145
|
+
"weppy-roblox-mcp": { command: "npx", args: ["-y", "@weppy/roblox-mcp"] }
|
|
146
|
+
};
|
|
147
|
+
config = next;
|
|
132
148
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
133
149
|
'
|
|
134
150
|
}
|
|
@@ -141,9 +157,24 @@ is_antigravity_mcp_configured() {
|
|
|
141
157
|
MCP_CONFIG_PATH="$config_path" node -e '
|
|
142
158
|
const fs = require("fs");
|
|
143
159
|
const configPath = process.env.MCP_CONFIG_PATH;
|
|
160
|
+
function isJsonObject(value) {
|
|
161
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
162
|
+
}
|
|
163
|
+
function hasExpectedCommandShape(value) {
|
|
164
|
+
return (
|
|
165
|
+
isJsonObject(value) &&
|
|
166
|
+
value.command === "npx" &&
|
|
167
|
+
Array.isArray(value.args) &&
|
|
168
|
+
value.args.length === 2 &&
|
|
169
|
+
value.args[0] === "-y" &&
|
|
170
|
+
value.args[1] === "@weppy/roblox-mcp"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
144
173
|
try {
|
|
145
174
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
146
|
-
|
|
175
|
+
const canonical = config?.mcpServers?.["weppy-roblox-mcp"];
|
|
176
|
+
const hasLegacyFlatKey = Object.prototype.hasOwnProperty.call(config, "weppy-roblox-mcp");
|
|
177
|
+
process.exit(hasExpectedCommandShape(canonical) && !hasLegacyFlatKey ? 0 : 1);
|
|
147
178
|
} catch {
|
|
148
179
|
process.exit(1);
|
|
149
180
|
}
|
|
@@ -154,7 +185,414 @@ is_codex_config_configured() {
|
|
|
154
185
|
local config_path="$1"
|
|
155
186
|
|
|
156
187
|
[ -f "$config_path" ] || return 1
|
|
157
|
-
|
|
188
|
+
MCP_CODEX_CONFIG_PATH="$config_path" node <<'NODE' >/dev/null 2>&1
|
|
189
|
+
const fs = require("fs");
|
|
190
|
+
|
|
191
|
+
const configPath = process.env.MCP_CODEX_CONFIG_PATH;
|
|
192
|
+
const serverName = "weppy-roblox-mcp";
|
|
193
|
+
const expectedCommand = "npx";
|
|
194
|
+
const expectedArgs = ["-y", "@weppy/roblox-mcp"];
|
|
195
|
+
const headerPattern = new RegExp(
|
|
196
|
+
"^\\s*\\[\\s*mcp_servers\\." + serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\]\\s*(?:#.*)?$",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
function stripCommentOutsideStrings(line) {
|
|
200
|
+
let inSingle = false;
|
|
201
|
+
let inDouble = false;
|
|
202
|
+
let escaped = false;
|
|
203
|
+
|
|
204
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
205
|
+
const char = line[index];
|
|
206
|
+
|
|
207
|
+
if (char === '"' && !inSingle && !escaped) {
|
|
208
|
+
inDouble = !inDouble;
|
|
209
|
+
} else if (char === "'" && !inDouble && !escaped) {
|
|
210
|
+
inSingle = !inSingle;
|
|
211
|
+
} else if (char === "#" && !inSingle && !inDouble) {
|
|
212
|
+
return line.slice(0, index).trimEnd();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
escaped = char === "\\" && !escaped;
|
|
216
|
+
if (char !== "\\") {
|
|
217
|
+
escaped = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return line.trimEnd();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function countTripleQuoteToggles(line, quote) {
|
|
225
|
+
let count = 0;
|
|
226
|
+
let inSingle = false;
|
|
227
|
+
let inDouble = false;
|
|
228
|
+
let escaped = false;
|
|
229
|
+
|
|
230
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
231
|
+
const char = line[index] ?? "";
|
|
232
|
+
const nextThree = line.slice(index, index + 3);
|
|
233
|
+
const isOutsideStrings = !inSingle && !inDouble;
|
|
234
|
+
|
|
235
|
+
if (isOutsideStrings && nextThree === quote.repeat(3)) {
|
|
236
|
+
count += 1;
|
|
237
|
+
index += 2;
|
|
238
|
+
escaped = false;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (char === '"' && !inSingle && !escaped) {
|
|
243
|
+
inDouble = !inDouble;
|
|
244
|
+
} else if (char === "'" && !inDouble && !escaped) {
|
|
245
|
+
inSingle = !inSingle;
|
|
246
|
+
} else if (char === "#" && !inSingle && !inDouble) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
escaped = char === "\\" && !escaped;
|
|
251
|
+
if (char !== "\\") {
|
|
252
|
+
escaped = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return count;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function advanceTripleQuoteState(line, state) {
|
|
260
|
+
const next = { ...state };
|
|
261
|
+
const tripleDoubleCount = countTripleQuoteToggles(line, '"');
|
|
262
|
+
const tripleSingleCount = countTripleQuoteToggles(line, "'");
|
|
263
|
+
|
|
264
|
+
if (!next.inTripleSingle && tripleDoubleCount % 2 === 1) {
|
|
265
|
+
next.inTripleDouble = !next.inTripleDouble;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!next.inTripleDouble && tripleSingleCount % 2 === 1) {
|
|
269
|
+
next.inTripleSingle = !next.inTripleSingle;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isTomlTableHeaderLine(line) {
|
|
276
|
+
const normalized = stripCommentOutsideStrings(line).trim();
|
|
277
|
+
|
|
278
|
+
if (normalized.length === 0) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return /^\[\[.*\]\]$/.test(normalized) || /^\[.*\]$/.test(normalized);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function findAllCodexBlocks(source) {
|
|
286
|
+
const lines = source.split("\n");
|
|
287
|
+
const blocks = [];
|
|
288
|
+
let activeLines = null;
|
|
289
|
+
let state = {
|
|
290
|
+
inTripleDouble: false,
|
|
291
|
+
inTripleSingle: false,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
for (const line of lines) {
|
|
295
|
+
const isHeaderCandidate = !state.inTripleDouble && !state.inTripleSingle && isTomlTableHeaderLine(line);
|
|
296
|
+
const isCodexHeader = isHeaderCandidate && headerPattern.test(line);
|
|
297
|
+
|
|
298
|
+
if (isCodexHeader) {
|
|
299
|
+
if (activeLines !== null) {
|
|
300
|
+
blocks.push(activeLines.join("\n").trim());
|
|
301
|
+
}
|
|
302
|
+
activeLines = [line];
|
|
303
|
+
} else if (activeLines !== null && isHeaderCandidate) {
|
|
304
|
+
blocks.push(activeLines.join("\n").trim());
|
|
305
|
+
activeLines = null;
|
|
306
|
+
} else if (activeLines !== null) {
|
|
307
|
+
activeLines.push(line);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
state = advanceTripleQuoteState(line, state);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (activeLines !== null) {
|
|
314
|
+
blocks.push(activeLines.join("\n").trim());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return blocks;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parseStringAssignment(value, key) {
|
|
321
|
+
const match = new RegExp("^\\s*" + key + "\\s*=\\s*([\"'])([^\"']+)\\1\\s*$").exec(value);
|
|
322
|
+
return match ? match[2] : null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseTomlStringArray(value) {
|
|
326
|
+
const match = /^\s*args\s*=\s*\[(.*)\]\s*$/ms.exec(value.trim());
|
|
327
|
+
|
|
328
|
+
if (match === null) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const body = match[1] ?? "";
|
|
333
|
+
const values = [];
|
|
334
|
+
let cursor = 0;
|
|
335
|
+
let expectValue = true;
|
|
336
|
+
|
|
337
|
+
while (cursor < body.length) {
|
|
338
|
+
while (cursor < body.length && /\s/.test(body[cursor] ?? "")) {
|
|
339
|
+
cursor += 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (cursor >= body.length) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!expectValue) {
|
|
347
|
+
if (body[cursor] !== ",") {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
cursor += 1;
|
|
351
|
+
expectValue = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const quote = body[cursor];
|
|
356
|
+
if (quote !== '"' && quote !== "'") {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
cursor += 1;
|
|
361
|
+
let token = "";
|
|
362
|
+
let escaped = false;
|
|
363
|
+
|
|
364
|
+
while (cursor < body.length) {
|
|
365
|
+
const char = body[cursor] ?? "";
|
|
366
|
+
|
|
367
|
+
if (char === quote && !escaped) {
|
|
368
|
+
cursor += 1;
|
|
369
|
+
values.push(token);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
token += char;
|
|
374
|
+
escaped = char === "\\" && !escaped;
|
|
375
|
+
if (char !== "\\") {
|
|
376
|
+
escaped = false;
|
|
377
|
+
}
|
|
378
|
+
cursor += 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (cursor > body.length) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
expectValue = false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const leftover = body.slice(cursor).trim();
|
|
389
|
+
if (leftover === ",") {
|
|
390
|
+
return values;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return leftover.length === 0 ? values : null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function collectArrayLines(lines, startIndex) {
|
|
397
|
+
const collected = [stripCommentOutsideStrings(lines[startIndex] ?? "")];
|
|
398
|
+
let bracketDepth = 0;
|
|
399
|
+
let inSingle = false;
|
|
400
|
+
let inDouble = false;
|
|
401
|
+
let escaped = false;
|
|
402
|
+
|
|
403
|
+
for (let lineIndex = startIndex; lineIndex < lines.length; lineIndex += 1) {
|
|
404
|
+
const sanitized = stripCommentOutsideStrings(lines[lineIndex] ?? "");
|
|
405
|
+
if (lineIndex !== startIndex) {
|
|
406
|
+
collected.push(sanitized);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (let index = 0; index < sanitized.length; index += 1) {
|
|
410
|
+
const char = sanitized[index] ?? "";
|
|
411
|
+
|
|
412
|
+
if (char === '"' && !inSingle && !escaped) {
|
|
413
|
+
inDouble = !inDouble;
|
|
414
|
+
} else if (char === "'" && !inDouble && !escaped) {
|
|
415
|
+
inSingle = !inSingle;
|
|
416
|
+
} else if (!inSingle && !inDouble) {
|
|
417
|
+
if (char === "[") {
|
|
418
|
+
bracketDepth += 1;
|
|
419
|
+
} else if (char === "]") {
|
|
420
|
+
bracketDepth -= 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
escaped = char === "\\" && !escaped;
|
|
425
|
+
if (char !== "\\") {
|
|
426
|
+
escaped = false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (bracketDepth <= 0) {
|
|
431
|
+
return {
|
|
432
|
+
nextIndex: lineIndex,
|
|
433
|
+
text: collected.join("\n"),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function parseCodexBlock(blockContent) {
|
|
442
|
+
const lines = blockContent.split("\n");
|
|
443
|
+
let command = null;
|
|
444
|
+
let args = null;
|
|
445
|
+
let hasConflict = false;
|
|
446
|
+
let inTripleDouble = false;
|
|
447
|
+
let inTripleSingle = false;
|
|
448
|
+
|
|
449
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
450
|
+
const line = lines[index] ?? "";
|
|
451
|
+
const sanitized = stripCommentOutsideStrings(line);
|
|
452
|
+
const trimmed = sanitized.trim();
|
|
453
|
+
|
|
454
|
+
if (inTripleDouble) {
|
|
455
|
+
if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
|
|
456
|
+
inTripleDouble = false;
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (inTripleSingle) {
|
|
462
|
+
if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
|
|
463
|
+
inTripleSingle = false;
|
|
464
|
+
}
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
|
|
469
|
+
inTripleDouble = true;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
|
|
474
|
+
inTripleSingle = true;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (trimmed.length === 0) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (/^command\s*=/.test(trimmed)) {
|
|
483
|
+
const parsedCommand = parseStringAssignment(trimmed, "command");
|
|
484
|
+
if (command !== null || parsedCommand === null) {
|
|
485
|
+
hasConflict = true;
|
|
486
|
+
} else {
|
|
487
|
+
command = parsedCommand;
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (/^args\s*=/.test(trimmed)) {
|
|
493
|
+
const collected = collectArrayLines(lines, index);
|
|
494
|
+
const parsedArgs = collected === null ? null : parseTomlStringArray(collected.text);
|
|
495
|
+
|
|
496
|
+
if (args !== null || parsedArgs === null || collected === null) {
|
|
497
|
+
hasConflict = true;
|
|
498
|
+
} else {
|
|
499
|
+
args = parsedArgs;
|
|
500
|
+
index = collected.nextIndex;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
args,
|
|
507
|
+
command,
|
|
508
|
+
hasConflict,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function isStructurallySafe(source) {
|
|
513
|
+
let bracketDepth = 0;
|
|
514
|
+
let braceDepth = 0;
|
|
515
|
+
let inSingle = false;
|
|
516
|
+
let inDouble = false;
|
|
517
|
+
let escaped = false;
|
|
518
|
+
let tripleState = {
|
|
519
|
+
inTripleDouble: false,
|
|
520
|
+
inTripleSingle: false,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
for (const line of source.split("\n")) {
|
|
524
|
+
tripleState = advanceTripleQuoteState(line, tripleState);
|
|
525
|
+
|
|
526
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
527
|
+
const char = line[index] ?? "";
|
|
528
|
+
|
|
529
|
+
if (!inSingle && !inDouble && char === "#") {
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (char === '"' && !inSingle && !escaped) {
|
|
534
|
+
inDouble = !inDouble;
|
|
535
|
+
} else if (char === "'" && !inDouble && !escaped) {
|
|
536
|
+
inSingle = !inSingle;
|
|
537
|
+
} else if (!inSingle && !inDouble) {
|
|
538
|
+
if (char === "[") {
|
|
539
|
+
bracketDepth += 1;
|
|
540
|
+
} else if (char === "]") {
|
|
541
|
+
bracketDepth -= 1;
|
|
542
|
+
if (bracketDepth < 0) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
} else if (char === "{") {
|
|
546
|
+
braceDepth += 1;
|
|
547
|
+
} else if (char === "}") {
|
|
548
|
+
braceDepth -= 1;
|
|
549
|
+
if (braceDepth < 0) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
escaped = char === "\\" && !escaped;
|
|
556
|
+
if (char !== "\\") {
|
|
557
|
+
escaped = false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
!tripleState.inTripleDouble &&
|
|
564
|
+
!tripleState.inTripleSingle &&
|
|
565
|
+
bracketDepth === 0 &&
|
|
566
|
+
braceDepth === 0 &&
|
|
567
|
+
!inSingle &&
|
|
568
|
+
!inDouble
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
const source = fs.readFileSync(configPath, "utf8");
|
|
574
|
+
if (!isStructurallySafe(source)) {
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const blocks = findAllCodexBlocks(source);
|
|
579
|
+
if (blocks.length !== 1) {
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const parsed = parseCodexBlock(blocks[0]);
|
|
584
|
+
const isConfigured =
|
|
585
|
+
!parsed.hasConflict &&
|
|
586
|
+
parsed.command === expectedCommand &&
|
|
587
|
+
Array.isArray(parsed.args) &&
|
|
588
|
+
parsed.args.length === expectedArgs.length &&
|
|
589
|
+
parsed.args.every((entry, index) => entry === expectedArgs[index]);
|
|
590
|
+
|
|
591
|
+
process.exit(isConfigured ? 0 : 1);
|
|
592
|
+
} catch {
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
NODE
|
|
158
596
|
}
|
|
159
597
|
|
|
160
598
|
resolve_optional_cli_command() {
|
|
@@ -175,16 +613,6 @@ resolve_optional_cli_command() {
|
|
|
175
613
|
return 1
|
|
176
614
|
}
|
|
177
615
|
|
|
178
|
-
is_lfs_pointer() {
|
|
179
|
-
local file_path="$1"
|
|
180
|
-
|
|
181
|
-
if [ ! -f "$file_path" ]; then
|
|
182
|
-
return 1
|
|
183
|
-
fi
|
|
184
|
-
|
|
185
|
-
grep -q "git-lfs.github.com/spec/v1" "$file_path"
|
|
186
|
-
}
|
|
187
|
-
|
|
188
616
|
# ── Header ──
|
|
189
617
|
# shellcheck disable=SC2059
|
|
190
618
|
printf "\n${BOLD}WROX Installer${NC}\n"
|
|
@@ -208,67 +636,24 @@ fi
|
|
|
208
636
|
success "Node.js $(node -v) detected"
|
|
209
637
|
|
|
210
638
|
# ═══════════════════════════════════
|
|
211
|
-
# [1/
|
|
639
|
+
# [1/2] Setup — Roblox Studio Plugin
|
|
212
640
|
# ═══════════════════════════════════
|
|
213
|
-
step "1/
|
|
641
|
+
step "1/2" "Setup Roblox Studio Plugin"
|
|
214
642
|
|
|
215
|
-
if confirm " Run
|
|
216
|
-
if
|
|
217
|
-
success "
|
|
643
|
+
if confirm " Run npx -y @weppy/roblox-mcp --setup?"; then
|
|
644
|
+
if npx -y @weppy/roblox-mcp --setup; then
|
|
645
|
+
success "Setup complete"
|
|
218
646
|
else
|
|
219
|
-
|
|
220
|
-
exit 1
|
|
647
|
+
warn "Setup encountered a warning (non-blocking)"
|
|
221
648
|
fi
|
|
222
649
|
else
|
|
223
|
-
warn "
|
|
650
|
+
warn "Setup skipped"
|
|
224
651
|
fi
|
|
225
652
|
|
|
226
653
|
# ═══════════════════════════════════
|
|
227
|
-
# [2/
|
|
654
|
+
# [2/2] Register MCP with AI apps
|
|
228
655
|
# ═══════════════════════════════════
|
|
229
|
-
step "2/
|
|
230
|
-
|
|
231
|
-
PLUGINS_DIR="$HOME/Documents/Roblox/Plugins"
|
|
232
|
-
PLUGIN_NAME="WeppyRobloxMCP.rbxm"
|
|
233
|
-
|
|
234
|
-
# Search for .rbxm in npm global prefix
|
|
235
|
-
NPM_PREFIX=$(npm prefix -g 2>/dev/null)
|
|
236
|
-
BUNDLED_PLUGIN=""
|
|
237
|
-
|
|
238
|
-
# Search for .rbxm in npm global path
|
|
239
|
-
for search_dir in \
|
|
240
|
-
"${NPM_PREFIX}/lib/node_modules/@weppy/roblox-mcp/plugins/weppy-roblox-mcp/roblox-plugin" \
|
|
241
|
-
"${NPM_PREFIX}/lib/node_modules/@weppy/roblox-mcp/roblox-plugin"; do
|
|
242
|
-
if [ -f "${search_dir}/${PLUGIN_NAME}" ]; then
|
|
243
|
-
BUNDLED_PLUGIN="${search_dir}/${PLUGIN_NAME}"
|
|
244
|
-
break
|
|
245
|
-
fi
|
|
246
|
-
done
|
|
247
|
-
|
|
248
|
-
if [ -n "$BUNDLED_PLUGIN" ]; then
|
|
249
|
-
if is_lfs_pointer "$BUNDLED_PLUGIN"; then
|
|
250
|
-
fail "Bundled plugin payload is invalid (Git LFS pointer detected)"
|
|
251
|
-
info "Install the plugin from the GitHub release ZIP instead"
|
|
252
|
-
exit 1
|
|
253
|
-
fi
|
|
254
|
-
|
|
255
|
-
printf " → %s/%s\n" "$PLUGINS_DIR" "$PLUGIN_NAME"
|
|
256
|
-
if confirm " Copy plugin to Roblox Plugins folder?"; then
|
|
257
|
-
mkdir -p "$PLUGINS_DIR"
|
|
258
|
-
cp "$BUNDLED_PLUGIN" "$PLUGINS_DIR/$PLUGIN_NAME"
|
|
259
|
-
success "Plugin installed → $PLUGINS_DIR/$PLUGIN_NAME"
|
|
260
|
-
else
|
|
261
|
-
warn "Plugin install skipped"
|
|
262
|
-
fi
|
|
263
|
-
else
|
|
264
|
-
warn "Bundled plugin file not found"
|
|
265
|
-
info "Will be installed automatically on first MCP server run"
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
|
-
# ═══════════════════════════════════
|
|
269
|
-
# [3/3] Register MCP with AI apps
|
|
270
|
-
# ═══════════════════════════════════
|
|
271
|
-
step "3/3" "Register MCP with AI apps"
|
|
656
|
+
step "2/2" "Register MCP with AI apps"
|
|
272
657
|
printf " Automatic registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity\n"
|
|
273
658
|
|
|
274
659
|
MCP_COMMAND='npx -y @weppy/roblox-mcp'
|
|
@@ -376,7 +761,12 @@ else
|
|
|
376
761
|
# shellcheck disable=SC2059
|
|
377
762
|
printf "\n Select apps to register ${DIM}(comma-separated, 'a' for all, 'n' to skip)${NC}\n"
|
|
378
763
|
printf " → "
|
|
379
|
-
|
|
764
|
+
if [ "${CI:-}" = "true" ]; then
|
|
765
|
+
selection="a"
|
|
766
|
+
printf "a\n"
|
|
767
|
+
else
|
|
768
|
+
read -r selection </dev/tty
|
|
769
|
+
fi
|
|
380
770
|
selection="${selection:-n}"
|
|
381
771
|
|
|
382
772
|
# Parse selection
|
package/llms-full.txt
CHANGED
|
@@ -21,9 +21,21 @@ Roblox Studio Plugin ← Executes commands inside Studio
|
|
|
21
21
|
|
|
22
22
|
When an AI app says "Create a blue part", the MCP server converts this request, and the Roblox Studio plugin actually creates the part.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Web Installer (Recommended)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Open the installer page in Chrome, Edge, or Arc for one-click setup:
|
|
27
|
+
|
|
28
|
+
https://hope1026.github.io/weppy-roblox-mcp/installer/
|
|
29
|
+
|
|
30
|
+
It writes the supported AI app config for `npx -y @weppy/roblox-mcp` first.
|
|
31
|
+
|
|
32
|
+
After you reopen the AI app, the first MCP run fetches the package through `npx` and auto-installs the Roblox Studio plugin.
|
|
33
|
+
|
|
34
|
+
For Safari or Firefox, use the installer page as a guide or continue with the manual install below.
|
|
35
|
+
|
|
36
|
+
## One-Line Install (Fallback)
|
|
37
|
+
|
|
38
|
+
Advanced users can install everything in one command:
|
|
27
39
|
|
|
28
40
|
```bash
|
|
29
41
|
# macOS / Linux
|
package/llms.txt
CHANGED
|
@@ -21,7 +21,11 @@ terrain, and more — inside a live Studio session.
|
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
**Web installer** (recommended one-click setup for Chrome, Edge, and Arc):
|
|
25
|
+
|
|
26
|
+
https://hope1026.github.io/weppy-roblox-mcp/installer/
|
|
27
|
+
|
|
28
|
+
**One-line install** (advanced fallback):
|
|
25
29
|
|
|
26
30
|
```bash
|
|
27
31
|
# macOS / Linux
|
|
@@ -31,8 +35,10 @@ curl -fsSL https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/inst
|
|
|
31
35
|
irm https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/install.ps1 | iex
|
|
32
36
|
```
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
Safari / Firefox users should follow the installer page guide or open the installation docs.
|
|
39
|
+
|
|
40
|
+
Manual alternative:
|
|
41
|
+
1. Install Roblox Studio plugin via the installation guide
|
|
36
42
|
2. Register MCP server: `npx -y @weppy/roblox-mcp`
|
|
37
43
|
|
|
38
44
|
## Tiers
|
|
@@ -49,7 +55,7 @@ Or manually:
|
|
|
49
55
|
- WROX Dashboard guide: https://github.com/hope1026/weppy-roblox-mcp/blob/main/docs/en/dashboard/overview.md
|
|
50
56
|
- Troubleshooting: https://github.com/hope1026/weppy-roblox-mcp/blob/main/docs/troubleshooting.md
|
|
51
57
|
- Compatibility: https://github.com/hope1026/weppy-roblox-mcp/blob/main/docs/compatibility.md
|
|
52
|
-
- Pro upgrade: https://
|
|
58
|
+
- Pro upgrade: https://weppy-web.pages.dev/en/plans
|
|
53
59
|
|
|
54
60
|
## Requirements
|
|
55
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weppy/roblox-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for Roblox Studio integration - enables AI coding agents to interact with Roblox Studio in real-time",
|
|
5
5
|
"main": "plugins/weppy-roblox-mcp/dist/index.js",
|
|
6
6
|
"type": "module",
|