@chrrxs/robloxstudio-mcp-inspector 2.19.1 → 2.21.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/dist/index.js CHANGED
@@ -196,6 +196,9 @@ var init_bridge_service = __esm({
196
196
  }
197
197
  return removed;
198
198
  }
199
+ async unregisterInstanceIdEverywhere(instanceId) {
200
+ return this.unregisterInstanceId(instanceId);
201
+ }
199
202
  getInstances() {
200
203
  return Array.from(this.instances.values());
201
204
  }
@@ -451,29 +454,256 @@ var init_bridge_service = __esm({
451
454
  }
452
455
  });
453
456
 
457
+ // ../core/dist/roblox-docs.js
458
+ function isDocCategory(value) {
459
+ return DOC_CATEGORIES.includes(value);
460
+ }
461
+ function cacheGet(key) {
462
+ const entry = cache.get(key);
463
+ if (!entry)
464
+ return void 0;
465
+ const ttl = entry.notFound ? NEGATIVE_CACHE_TTL_MS : CACHE_TTL_MS;
466
+ if (Date.now() - entry.fetchedAt > ttl) {
467
+ cache.delete(key);
468
+ return void 0;
469
+ }
470
+ return entry;
471
+ }
472
+ function cacheSet(key, entry) {
473
+ if (cache.size >= MAX_CACHE_ENTRIES) {
474
+ const oldest = cache.keys().next().value;
475
+ if (oldest !== void 0)
476
+ cache.delete(oldest);
477
+ }
478
+ cache.set(key, entry);
479
+ }
480
+ function docUrl(category, name) {
481
+ return `${DOCS_BASE_URL}/${category}/${encodeURIComponent(name)}.md`;
482
+ }
483
+ async function fetchRobloxDoc(category, name) {
484
+ const key = `${category}/${name}`;
485
+ const cached = cacheGet(key);
486
+ if (cached) {
487
+ if (cached.notFound)
488
+ throw new DocNotFoundError(category, name);
489
+ return cached.content;
490
+ }
491
+ const controller = new AbortController();
492
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
493
+ let response;
494
+ try {
495
+ response = await fetch(docUrl(category, name), {
496
+ signal: controller.signal,
497
+ headers: { Accept: "text/markdown, text/plain" }
498
+ });
499
+ } catch (error) {
500
+ throw new Error(`Failed to fetch Roblox docs for ${key}: ${error instanceof Error ? error.message : String(error)}`);
501
+ } finally {
502
+ clearTimeout(timer);
503
+ }
504
+ if (response.status === 404) {
505
+ cacheSet(key, { fetchedAt: Date.now(), notFound: true });
506
+ throw new DocNotFoundError(category, name);
507
+ }
508
+ if (!response.ok) {
509
+ throw new Error(`Failed to fetch Roblox docs for ${key}: HTTP ${response.status}`);
510
+ }
511
+ const content = await response.text();
512
+ cacheSet(key, { fetchedAt: Date.now(), content });
513
+ return content;
514
+ }
515
+ function listSections(markdown) {
516
+ const sections = [];
517
+ for (const line of markdown.split("\n")) {
518
+ const match = line.match(/^##\s+(.+?)\s*$/);
519
+ if (match)
520
+ sections.push(match[1]);
521
+ }
522
+ return sections;
523
+ }
524
+ function extractSection(markdown, section) {
525
+ const lines = markdown.split("\n");
526
+ const wanted = section.trim().toLowerCase();
527
+ let start = -1;
528
+ for (let i = 0; i < lines.length; i++) {
529
+ const match = lines[i].match(/^##\s+(.+?)\s*$/);
530
+ if (match && match[1].toLowerCase() === wanted) {
531
+ start = i;
532
+ break;
533
+ }
534
+ }
535
+ if (start === -1)
536
+ return void 0;
537
+ let end = lines.length;
538
+ for (let i = start + 1; i < lines.length; i++) {
539
+ if (/^##?\s+/.test(lines[i])) {
540
+ end = i;
541
+ break;
542
+ }
543
+ }
544
+ return lines.slice(start, end).join("\n").trimEnd();
545
+ }
546
+ async function getRobloxDoc(category, name, section) {
547
+ const markdown = await fetchRobloxDoc(category, name);
548
+ const sections = listSections(markdown);
549
+ if (section) {
550
+ const extracted = extractSection(markdown, section);
551
+ if (extracted === void 0) {
552
+ throw new Error(`Section "${section}" not found in ${category}/${name}. Available sections: ${sections.join(", ") || "(none)"}`);
553
+ }
554
+ return { content: extracted, truncated: false, sections };
555
+ }
556
+ if (markdown.length > MAX_DOC_CHARS) {
557
+ const head = markdown.slice(0, MAX_DOC_CHARS);
558
+ const note = `
559
+
560
+ ---
561
+ [Truncated at ${MAX_DOC_CHARS} of ${markdown.length} characters. Re-request with the "section" parameter to read one section in full. Available sections: ${sections.join(", ")}]`;
562
+ return { content: head + note, truncated: true, sections };
563
+ }
564
+ return { content: markdown, truncated: false, sections };
565
+ }
566
+ var DOC_CATEGORIES, DOCS_BASE_URL, FETCH_TIMEOUT_MS, CACHE_TTL_MS, NEGATIVE_CACHE_TTL_MS, MAX_CACHE_ENTRIES, MAX_DOC_CHARS, cache, DocNotFoundError;
567
+ var init_roblox_docs = __esm({
568
+ "../core/dist/roblox-docs.js"() {
569
+ "use strict";
570
+ DOC_CATEGORIES = ["classes", "enums", "datatypes", "libraries", "globals"];
571
+ DOCS_BASE_URL = "https://create.roblox.com/docs/reference/engine";
572
+ FETCH_TIMEOUT_MS = 15e3;
573
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
574
+ NEGATIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
575
+ MAX_CACHE_ENTRIES = 200;
576
+ MAX_DOC_CHARS = 5e4;
577
+ cache = /* @__PURE__ */ new Map();
578
+ DocNotFoundError = class extends Error {
579
+ constructor(category, name) {
580
+ super(`No Roblox documentation found for ${category}/${name}. Names are case-sensitive PascalCase (e.g. "ProximityPrompt", "TweenService"). Valid categories: ${DOC_CATEGORIES.join(", ")}.`);
581
+ this.name = "DocNotFoundError";
582
+ }
583
+ };
584
+ }
585
+ });
586
+
454
587
  // ../core/dist/mcp-compat.js
455
588
  import { ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
456
- function registerEmptyResourceShim(server) {
589
+ function registerResourceHandlers(server) {
457
590
  server.registerCapabilities({ resources: {} });
458
591
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
459
592
  resources: []
460
593
  }));
461
594
  server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
462
- resourceTemplates: []
595
+ resourceTemplates: [
596
+ {
597
+ uriTemplate: "robloxdocs://classes/{className}",
598
+ name: "Roblox class documentation",
599
+ description: "Official engine reference for a class (e.g. robloxdocs://classes/ProximityPrompt) \u2014 properties, methods, events, and usage guidance as markdown.",
600
+ mimeType: "text/markdown"
601
+ },
602
+ {
603
+ uriTemplate: "robloxdocs://enums/{enumName}",
604
+ name: "Roblox enum documentation",
605
+ description: "Official engine reference for an enum (e.g. robloxdocs://enums/KeyCode).",
606
+ mimeType: "text/markdown"
607
+ },
608
+ {
609
+ uriTemplate: "robloxdocs://datatypes/{dataTypeName}",
610
+ name: "Roblox datatype documentation",
611
+ description: "Official engine reference for a datatype (e.g. robloxdocs://datatypes/CFrame).",
612
+ mimeType: "text/markdown"
613
+ },
614
+ {
615
+ uriTemplate: "robloxdocs://libraries/{libraryName}",
616
+ name: "Roblox library documentation",
617
+ description: "Official engine reference for a Luau library (e.g. robloxdocs://libraries/table).",
618
+ mimeType: "text/markdown"
619
+ },
620
+ {
621
+ uriTemplate: "robloxdocs://globals/{globalsPage}",
622
+ name: "Roblox globals documentation",
623
+ description: "Official engine reference for global functions and variables (e.g. robloxdocs://globals/LuaGlobals, robloxdocs://globals/RobloxGlobals).",
624
+ mimeType: "text/markdown"
625
+ }
626
+ ]
463
627
  }));
464
628
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
465
- throw new McpError(ErrorCode.InvalidParams, `Resource ${request.params.uri} not found`);
629
+ const uri = request.params.uri;
630
+ const match = uri.match(/^robloxdocs:\/\/([^/]+)\/([^/]+)$/);
631
+ if (!match || !isDocCategory(match[1])) {
632
+ throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
633
+ }
634
+ const [, category, rawName] = match;
635
+ const name = decodeURIComponent(rawName);
636
+ try {
637
+ const content = await fetchRobloxDoc(category, name);
638
+ return {
639
+ contents: [{ uri, mimeType: "text/markdown", text: content }]
640
+ };
641
+ } catch (error) {
642
+ if (error instanceof DocNotFoundError) {
643
+ throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found. Names are case-sensitive PascalCase; valid categories: ${DOC_CATEGORIES.join(", ")}.`);
644
+ }
645
+ throw new McpError(ErrorCode.InternalError, `Failed to read ${uri}: ${error instanceof Error ? error.message : String(error)}`);
646
+ }
466
647
  });
467
648
  }
468
649
  var init_mcp_compat = __esm({
469
650
  "../core/dist/mcp-compat.js"() {
470
651
  "use strict";
652
+ init_roblox_docs();
653
+ }
654
+ });
655
+
656
+ // ../core/dist/auth.js
657
+ import { randomBytes, createHash, timingSafeEqual } from "crypto";
658
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
659
+ import { join, dirname } from "path";
660
+ import { homedir } from "os";
661
+ function authTokenFilePath() {
662
+ return join(homedir(), ".robloxstudio-mcp", "auth-token");
663
+ }
664
+ function resolveAuthToken() {
665
+ const noAuth = (process.env.ROBLOX_STUDIO_NO_AUTH || "").toLowerCase();
666
+ if (noAuth === "1" || noAuth === "true") {
667
+ return { source: "disabled" };
668
+ }
669
+ const envToken = process.env.ROBLOX_STUDIO_AUTH_TOKEN?.trim();
670
+ if (envToken) {
671
+ return { token: envToken, source: "env" };
672
+ }
673
+ const filePath = authTokenFilePath();
674
+ try {
675
+ if (existsSync(filePath)) {
676
+ const existing = readFileSync(filePath, "utf8").trim();
677
+ if (existing) {
678
+ return { token: existing, source: "file", filePath };
679
+ }
680
+ }
681
+ const fresh = randomBytes(32).toString("hex");
682
+ mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
683
+ writeFileSync(filePath, fresh + "\n", { encoding: "utf8", mode: 384 });
684
+ try {
685
+ chmodSync(filePath, 384);
686
+ } catch {
687
+ }
688
+ return { token: fresh, source: "file", filePath };
689
+ } catch (err) {
690
+ console.error(`[auth] Could not read/create ${filePath} (${err instanceof Error ? err.message : err}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
691
+ return { token: randomBytes(32).toString("hex"), source: "file" };
692
+ }
693
+ }
694
+ function tokensMatch(provided, expected) {
695
+ const a = createHash("sha256").update(provided).digest();
696
+ const b = createHash("sha256").update(expected).digest();
697
+ return timingSafeEqual(a, b);
698
+ }
699
+ var init_auth = __esm({
700
+ "../core/dist/auth.js"() {
701
+ "use strict";
471
702
  }
472
703
  });
473
704
 
474
705
  // ../core/dist/http-server.js
475
706
  import express from "express";
476
- import cors from "cors";
477
707
  import http from "http";
478
708
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
479
709
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -526,7 +756,7 @@ function requiredClosedLineRange(body, toolName) {
526
756
  }
527
757
  return { startLine: parsed.startLine, endLine: parsed.endLine };
528
758
  }
529
- function createHttpServer(tools, bridge, allowedTools, serverConfig) {
759
+ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
530
760
  const app = express();
531
761
  let mcpServerActive = false;
532
762
  let lastMCPActivity = 0;
@@ -556,7 +786,49 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
556
786
  const isPluginConnected = () => {
557
787
  return bridge.getInstances().length > 0;
558
788
  };
559
- app.use(cors());
789
+ const allowedOrigins = new Set(security?.allowedOrigins ?? []);
790
+ app.use((req, res, next) => {
791
+ const origin = req.headers.origin;
792
+ if (typeof origin !== "string" || origin === "") {
793
+ next();
794
+ return;
795
+ }
796
+ if (!allowedOrigins.has(origin)) {
797
+ res.status(403).json({
798
+ error: "forbidden_origin",
799
+ message: `Cross-origin requests are not allowed from ${origin}. Set ROBLOX_STUDIO_ALLOWED_ORIGINS to allowlist specific origins.`
800
+ });
801
+ return;
802
+ }
803
+ res.setHeader("Access-Control-Allow-Origin", origin);
804
+ res.setHeader("Vary", "Origin");
805
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
806
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-MCP-Auth, Mcp-Protocol-Version");
807
+ if (req.method === "OPTIONS") {
808
+ res.status(204).end();
809
+ return;
810
+ }
811
+ next();
812
+ });
813
+ const authToken = security?.authToken;
814
+ const authRequired = (path5) => path5 === "/mcp" || path5.startsWith("/mcp/") || path5 === "/proxy" || path5 === "/instances" || path5 === "/unregister-instance-id";
815
+ app.use((req, res, next) => {
816
+ if (!authToken || !authRequired(req.path)) {
817
+ next();
818
+ return;
819
+ }
820
+ const headerToken = req.headers["x-mcp-auth"];
821
+ const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice("Bearer ".length) : void 0;
822
+ const provided = typeof headerToken === "string" && headerToken !== "" ? headerToken : bearer;
823
+ if (provided !== void 0 && tokensMatch(provided, authToken)) {
824
+ next();
825
+ return;
826
+ }
827
+ res.status(401).json({
828
+ error: "unauthorized",
829
+ message: 'Missing or invalid auth token. Send it as "X-MCP-Auth: <token>" or "Authorization: Bearer <token>". ' + (security?.authTokenHint ?? "The token is in ~/.robloxstudio-mcp/auth-token (or ROBLOX_STUDIO_AUTH_TOKEN).")
830
+ });
831
+ });
560
832
  app.use(express.json({ limit: "50mb" }));
561
833
  app.use(express.urlencoded({ limit: "50mb", extended: true }));
562
834
  app.get("/health", (req, res) => {
@@ -658,6 +930,15 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
658
930
  }
659
931
  res.json({ success: true });
660
932
  });
933
+ app.post("/unregister-instance-id", (req, res) => {
934
+ const { instanceId } = req.body;
935
+ if (typeof instanceId !== "string" || instanceId.length === 0) {
936
+ res.status(400).json({ error: "instanceId is required" });
937
+ return;
938
+ }
939
+ const removed = bridge.unregisterInstanceId(instanceId);
940
+ res.json({ success: true, removed });
941
+ });
661
942
  app.get("/status", (req, res) => {
662
943
  const instances = bridge.getInstances();
663
944
  const publicInstances = instances.map(toPublic);
@@ -775,7 +1056,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
775
1056
  try {
776
1057
  trackMCPActivity();
777
1058
  const server = new Server({ name: serverConfig.name, version: serverConfig.version }, { capabilities: { tools: {} } });
778
- registerEmptyResourceShim(server);
1059
+ registerResourceHandlers(server);
779
1060
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
780
1061
  tools: filteredTools.map((t) => ({
781
1062
  name: t.name,
@@ -917,7 +1198,9 @@ var init_http_server = __esm({
917
1198
  "use strict";
918
1199
  init_bridge_service();
919
1200
  init_mcp_compat();
1201
+ init_auth();
920
1202
  TOOL_HANDLERS = {
1203
+ get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
921
1204
  get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
922
1205
  search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
923
1206
  get_place_info: (tools, body) => tools.getPlaceInfo(body.instance_id),
@@ -1083,8 +1366,844 @@ var init_studio_client = __esm({
1083
1366
  }
1084
1367
  });
1085
1368
 
1369
+ // ../core/dist/tools/build-interpreter.js
1370
+ import * as acorn from "acorn";
1371
+ function isPlainObject(value) {
1372
+ if (value === null || typeof value !== "object")
1373
+ return false;
1374
+ const proto = Object.getPrototypeOf(value);
1375
+ return proto === Object.prototype || proto === null;
1376
+ }
1377
+ function runRestrictedScript(code, globals, options) {
1378
+ const timeoutMs = options?.timeoutMs ?? 1e4;
1379
+ const maxOps = options?.maxOps ?? 2e7;
1380
+ const deadline = Date.now() + timeoutMs;
1381
+ let ops = 0;
1382
+ let ast;
1383
+ try {
1384
+ ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: "script" });
1385
+ } catch (err) {
1386
+ throw new Error(`Syntax error: ${err.message}`);
1387
+ }
1388
+ const globalScope = new Scope(void 0, true);
1389
+ for (const [name, value] of Object.entries(globals)) {
1390
+ globalScope.declare(name, value, "const");
1391
+ }
1392
+ function tick() {
1393
+ ops++;
1394
+ if (ops > maxOps) {
1395
+ throw new ScriptTimeoutError(`Operation budget exceeded (${maxOps} ops)`);
1396
+ }
1397
+ if ((ops & 8191) === 0 && Date.now() > deadline) {
1398
+ throw new ScriptTimeoutError(`Execution timed out after ${timeoutMs}ms`);
1399
+ }
1400
+ }
1401
+ function assertPropAllowed(prop) {
1402
+ const name = typeof prop === "symbol" ? String(prop) : String(prop);
1403
+ if (FORBIDDEN_PROPS.has(name)) {
1404
+ throw new Error(`Access to property "${name}" is not allowed`);
1405
+ }
1406
+ return name;
1407
+ }
1408
+ function getMember(obj, prop) {
1409
+ if (obj === null || obj === void 0) {
1410
+ throw new Error(`Cannot read property "${String(prop)}" of ${obj}`);
1411
+ }
1412
+ const name = assertPropAllowed(prop);
1413
+ if (Array.isArray(obj)) {
1414
+ if (name === "length")
1415
+ return obj.length;
1416
+ const idx = Number(name);
1417
+ if (Number.isInteger(idx) && idx >= 0)
1418
+ return obj[idx];
1419
+ if (ARRAY_MEMBERS.has(name))
1420
+ return obj[name];
1421
+ throw new Error(`Array method "${name}" is not allowed`);
1422
+ }
1423
+ if (typeof obj === "string") {
1424
+ if (name === "length")
1425
+ return obj.length;
1426
+ const idx = Number(name);
1427
+ if (Number.isInteger(idx) && idx >= 0)
1428
+ return obj[idx];
1429
+ if (STRING_MEMBERS.has(name))
1430
+ return obj[name];
1431
+ throw new Error(`String method "${name}" is not allowed`);
1432
+ }
1433
+ if (typeof obj === "number") {
1434
+ if (NUMBER_MEMBERS.has(name))
1435
+ return obj[name];
1436
+ throw new Error(`Number method "${name}" is not allowed`);
1437
+ }
1438
+ if (isPlainObject(obj)) {
1439
+ if (Object.prototype.hasOwnProperty.call(obj, name))
1440
+ return obj[name];
1441
+ return void 0;
1442
+ }
1443
+ throw new Error(`Property access on this value type is not allowed`);
1444
+ }
1445
+ function setMember(obj, prop, value) {
1446
+ if (obj === null || obj === void 0) {
1447
+ throw new Error(`Cannot set property "${String(prop)}" of ${obj}`);
1448
+ }
1449
+ const name = assertPropAllowed(prop);
1450
+ if (Object.isFrozen(obj)) {
1451
+ throw new Error(`Cannot assign to property "${name}" of a frozen object`);
1452
+ }
1453
+ if (Array.isArray(obj)) {
1454
+ const idx = Number(name);
1455
+ if (Number.isInteger(idx) && idx >= 0 || name === "length") {
1456
+ obj[name] = value;
1457
+ return;
1458
+ }
1459
+ throw new Error(`Cannot set property "${name}" on an array`);
1460
+ }
1461
+ if (isPlainObject(obj)) {
1462
+ obj[name] = value;
1463
+ return;
1464
+ }
1465
+ throw new Error(`Property assignment on this value type is not allowed`);
1466
+ }
1467
+ function callFunction(fn, thisArg, args, desc) {
1468
+ if (typeof fn !== "function") {
1469
+ throw new Error(`${desc} is not a function`);
1470
+ }
1471
+ return fn.apply(thisArg, args);
1472
+ }
1473
+ function bindPattern(pattern, value, scope, kind) {
1474
+ switch (pattern.type) {
1475
+ case "Identifier":
1476
+ scope.declare(pattern.name, value, kind);
1477
+ return;
1478
+ case "AssignmentPattern":
1479
+ bindPattern(pattern.left, value === void 0 ? evaluate(pattern.right, scope) : value, scope, kind);
1480
+ return;
1481
+ case "ArrayPattern": {
1482
+ if (value === null || value === void 0)
1483
+ throw new Error("Cannot destructure null/undefined");
1484
+ let i = 0;
1485
+ for (const el of pattern.elements) {
1486
+ if (el === null) {
1487
+ i++;
1488
+ continue;
1489
+ }
1490
+ if (el.type === "RestElement") {
1491
+ bindPattern(el.argument, Array.isArray(value) ? value.slice(i) : [], scope, kind);
1492
+ break;
1493
+ }
1494
+ bindPattern(el, getMember(value, i), scope, kind);
1495
+ i++;
1496
+ }
1497
+ return;
1498
+ }
1499
+ case "ObjectPattern": {
1500
+ if (value === null || value === void 0)
1501
+ throw new Error("Cannot destructure null/undefined");
1502
+ const used = /* @__PURE__ */ new Set();
1503
+ for (const propNode of pattern.properties) {
1504
+ if (propNode.type === "RestElement") {
1505
+ const rest = {};
1506
+ if (isPlainObject(value)) {
1507
+ for (const k of Object.keys(value)) {
1508
+ if (!used.has(k) && !FORBIDDEN_PROPS.has(k))
1509
+ rest[k] = value[k];
1510
+ }
1511
+ }
1512
+ bindPattern(propNode.argument, rest, scope, kind);
1513
+ continue;
1514
+ }
1515
+ const key = propNode.computed ? String(evaluate(propNode.key, scope)) : propNode.key.type === "Identifier" ? propNode.key.name : String(propNode.key.value);
1516
+ used.add(key);
1517
+ bindPattern(propNode.value, getMember(value, key), scope, kind);
1518
+ }
1519
+ return;
1520
+ }
1521
+ default:
1522
+ throw new Error(`Unsupported binding pattern: ${pattern.type}`);
1523
+ }
1524
+ }
1525
+ function makeFunction(node, closureScope) {
1526
+ return (...args) => {
1527
+ tick();
1528
+ const fnScope = new Scope(closureScope, true);
1529
+ node.params.forEach((param, i) => {
1530
+ if (param.type === "RestElement") {
1531
+ bindPattern(param.argument, args.slice(i), fnScope, "let");
1532
+ } else {
1533
+ bindPattern(param, args[i], fnScope, "let");
1534
+ }
1535
+ });
1536
+ try {
1537
+ if (node.body.type === "BlockStatement") {
1538
+ executeBlock(node.body.body, new Scope(fnScope));
1539
+ return void 0;
1540
+ }
1541
+ return evaluate(node.body, fnScope);
1542
+ } catch (signal) {
1543
+ if (signal instanceof ReturnSignal)
1544
+ return signal.value;
1545
+ throw signal;
1546
+ }
1547
+ };
1548
+ }
1549
+ function hoistFunctions(statements, scope) {
1550
+ for (const stmt of statements) {
1551
+ if (stmt.type === "FunctionDeclaration" && stmt.id) {
1552
+ if (stmt.async || stmt.generator)
1553
+ throw new Error("async/generator functions are not supported");
1554
+ scope.declare(stmt.id.name, makeFunction(stmt, scope), "let");
1555
+ }
1556
+ }
1557
+ }
1558
+ function executeBlock(statements, scope) {
1559
+ hoistFunctions(statements, scope);
1560
+ for (const stmt of statements) {
1561
+ execute(stmt, scope);
1562
+ }
1563
+ }
1564
+ function execute(node, scope) {
1565
+ tick();
1566
+ switch (node.type) {
1567
+ case "ExpressionStatement":
1568
+ evaluate(node.expression, scope);
1569
+ return;
1570
+ case "VariableDeclaration":
1571
+ for (const decl of node.declarations) {
1572
+ const value = decl.init ? evaluate(decl.init, scope) : void 0;
1573
+ bindPattern(decl.id, value, scope, node.kind);
1574
+ }
1575
+ return;
1576
+ case "FunctionDeclaration":
1577
+ return;
1578
+ // hoisted by executeBlock
1579
+ case "BlockStatement":
1580
+ executeBlock(node.body, new Scope(scope));
1581
+ return;
1582
+ case "EmptyStatement":
1583
+ return;
1584
+ case "IfStatement":
1585
+ if (evaluate(node.test, scope)) {
1586
+ execute(node.consequent, scope);
1587
+ } else if (node.alternate) {
1588
+ execute(node.alternate, scope);
1589
+ }
1590
+ return;
1591
+ case "ForStatement": {
1592
+ const forScope = new Scope(scope);
1593
+ if (node.init) {
1594
+ if (node.init.type === "VariableDeclaration")
1595
+ execute(node.init, forScope);
1596
+ else
1597
+ evaluate(node.init, forScope);
1598
+ }
1599
+ while (node.test === null || evaluate(node.test, forScope)) {
1600
+ tick();
1601
+ try {
1602
+ execute(node.body, new Scope(forScope));
1603
+ } catch (signal) {
1604
+ if (signal instanceof BreakSignal)
1605
+ break;
1606
+ if (!(signal instanceof ContinueSignal))
1607
+ throw signal;
1608
+ }
1609
+ if (node.update)
1610
+ evaluate(node.update, forScope);
1611
+ }
1612
+ return;
1613
+ }
1614
+ case "ForOfStatement": {
1615
+ const iterable = evaluate(node.right, scope);
1616
+ if (!Array.isArray(iterable) && typeof iterable !== "string") {
1617
+ throw new Error("for...of only supports arrays and strings");
1618
+ }
1619
+ for (const item of iterable) {
1620
+ tick();
1621
+ const iterScope = new Scope(scope);
1622
+ if (node.left.type === "VariableDeclaration") {
1623
+ bindPattern(node.left.declarations[0].id, item, iterScope, node.left.kind);
1624
+ } else {
1625
+ assignTo(node.left, item, iterScope);
1626
+ }
1627
+ try {
1628
+ execute(node.body, iterScope);
1629
+ } catch (signal) {
1630
+ if (signal instanceof BreakSignal)
1631
+ break;
1632
+ if (!(signal instanceof ContinueSignal))
1633
+ throw signal;
1634
+ }
1635
+ }
1636
+ return;
1637
+ }
1638
+ case "ForInStatement": {
1639
+ const obj = evaluate(node.right, scope);
1640
+ const keys = isPlainObject(obj) ? Object.keys(obj) : Array.isArray(obj) ? obj.map((_, i) => String(i)) : [];
1641
+ for (const key of keys) {
1642
+ tick();
1643
+ const iterScope = new Scope(scope);
1644
+ if (node.left.type === "VariableDeclaration") {
1645
+ bindPattern(node.left.declarations[0].id, key, iterScope, node.left.kind);
1646
+ } else {
1647
+ assignTo(node.left, key, iterScope);
1648
+ }
1649
+ try {
1650
+ execute(node.body, iterScope);
1651
+ } catch (signal) {
1652
+ if (signal instanceof BreakSignal)
1653
+ break;
1654
+ if (!(signal instanceof ContinueSignal))
1655
+ throw signal;
1656
+ }
1657
+ }
1658
+ return;
1659
+ }
1660
+ case "WhileStatement":
1661
+ while (evaluate(node.test, scope)) {
1662
+ tick();
1663
+ try {
1664
+ execute(node.body, new Scope(scope));
1665
+ } catch (signal) {
1666
+ if (signal instanceof BreakSignal)
1667
+ break;
1668
+ if (!(signal instanceof ContinueSignal))
1669
+ throw signal;
1670
+ }
1671
+ }
1672
+ return;
1673
+ case "DoWhileStatement":
1674
+ do {
1675
+ tick();
1676
+ try {
1677
+ execute(node.body, new Scope(scope));
1678
+ } catch (signal) {
1679
+ if (signal instanceof BreakSignal)
1680
+ break;
1681
+ if (!(signal instanceof ContinueSignal))
1682
+ throw signal;
1683
+ }
1684
+ } while (evaluate(node.test, scope));
1685
+ return;
1686
+ case "BreakStatement":
1687
+ throw new BreakSignal(node.label?.name);
1688
+ case "ContinueStatement":
1689
+ throw new ContinueSignal(node.label?.name);
1690
+ case "ReturnStatement":
1691
+ throw new ReturnSignal(node.argument ? evaluate(node.argument, scope) : void 0);
1692
+ case "ThrowStatement":
1693
+ throw evaluate(node.argument, scope);
1694
+ case "TryStatement": {
1695
+ try {
1696
+ execute(node.block, scope);
1697
+ } catch (err) {
1698
+ if (err instanceof BreakSignal || err instanceof ContinueSignal || err instanceof ReturnSignal)
1699
+ throw err;
1700
+ if (err instanceof ScriptTimeoutError)
1701
+ throw err;
1702
+ if (node.handler) {
1703
+ const catchScope = new Scope(scope);
1704
+ if (node.handler.param) {
1705
+ const message = err instanceof Error ? { message: err.message } : err;
1706
+ bindPattern(node.handler.param, message, catchScope, "let");
1707
+ }
1708
+ execute(node.handler.body, catchScope);
1709
+ }
1710
+ } finally {
1711
+ if (node.finalizer)
1712
+ execute(node.finalizer, scope);
1713
+ }
1714
+ return;
1715
+ }
1716
+ case "SwitchStatement": {
1717
+ const disc = evaluate(node.discriminant, scope);
1718
+ const switchScope = new Scope(scope);
1719
+ let matched = false;
1720
+ try {
1721
+ for (const caseNode of node.cases) {
1722
+ if (!matched) {
1723
+ if (caseNode.test === null)
1724
+ matched = true;
1725
+ else if (evaluate(caseNode.test, switchScope) === disc)
1726
+ matched = true;
1727
+ }
1728
+ if (matched) {
1729
+ for (const stmt of caseNode.consequent)
1730
+ execute(stmt, switchScope);
1731
+ }
1732
+ }
1733
+ } catch (signal) {
1734
+ if (!(signal instanceof BreakSignal))
1735
+ throw signal;
1736
+ }
1737
+ return;
1738
+ }
1739
+ case "LabeledStatement":
1740
+ try {
1741
+ execute(node.body, scope);
1742
+ } catch (signal) {
1743
+ if (signal instanceof BreakSignal && signal.label === node.label.name)
1744
+ return;
1745
+ throw signal;
1746
+ }
1747
+ return;
1748
+ default:
1749
+ throw new Error(`Unsupported statement: ${node.type}`);
1750
+ }
1751
+ }
1752
+ function assignTo(target, value, scope) {
1753
+ if (target.type === "Identifier") {
1754
+ scope.set(target.name, value);
1755
+ } else if (target.type === "MemberExpression") {
1756
+ const obj = evaluate(target.object, scope);
1757
+ const prop = target.computed ? evaluate(target.property, scope) : target.property.name;
1758
+ setMember(obj, prop, value);
1759
+ } else {
1760
+ throw new Error(`Unsupported assignment target: ${target.type}`);
1761
+ }
1762
+ }
1763
+ function evaluate(node, scope) {
1764
+ tick();
1765
+ switch (node.type) {
1766
+ case "Literal":
1767
+ if (node.regex)
1768
+ throw new Error("Regular expression literals are not supported");
1769
+ return node.value;
1770
+ case "TemplateLiteral": {
1771
+ let out = "";
1772
+ node.quasis.forEach((quasi, i) => {
1773
+ out += quasi.value.cooked ?? "";
1774
+ if (i < node.expressions.length)
1775
+ out += String(evaluate(node.expressions[i], scope));
1776
+ });
1777
+ return out;
1778
+ }
1779
+ case "Identifier":
1780
+ return scope.get(node.name);
1781
+ case "ArrayExpression": {
1782
+ const arr = [];
1783
+ for (const el of node.elements) {
1784
+ if (el === null) {
1785
+ arr.length++;
1786
+ continue;
1787
+ }
1788
+ if (el.type === "SpreadElement") {
1789
+ const spread = evaluate(el.argument, scope);
1790
+ if (!Array.isArray(spread) && typeof spread !== "string") {
1791
+ throw new Error("Spread only supports arrays and strings");
1792
+ }
1793
+ for (const item of spread)
1794
+ arr.push(item);
1795
+ } else {
1796
+ arr.push(evaluate(el, scope));
1797
+ }
1798
+ }
1799
+ return arr;
1800
+ }
1801
+ case "ObjectExpression": {
1802
+ const obj = {};
1803
+ for (const prop of node.properties) {
1804
+ if (prop.type === "SpreadElement") {
1805
+ const spread = evaluate(prop.argument, scope);
1806
+ if (isPlainObject(spread)) {
1807
+ for (const k of Object.keys(spread)) {
1808
+ if (!FORBIDDEN_PROPS.has(k))
1809
+ obj[k] = spread[k];
1810
+ }
1811
+ }
1812
+ continue;
1813
+ }
1814
+ if (prop.kind !== "init")
1815
+ throw new Error("Getters/setters are not supported");
1816
+ const key = prop.computed ? String(evaluate(prop.key, scope)) : prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
1817
+ assertPropAllowed(key);
1818
+ obj[key] = evaluate(prop.value, scope);
1819
+ }
1820
+ return obj;
1821
+ }
1822
+ case "ArrowFunctionExpression":
1823
+ case "FunctionExpression":
1824
+ if (node.async || node.generator)
1825
+ throw new Error("async/generator functions are not supported");
1826
+ return makeFunction(node, scope);
1827
+ case "UnaryExpression": {
1828
+ if (node.operator === "typeof" && node.argument.type === "Identifier" && !scope.has(node.argument.name)) {
1829
+ return "undefined";
1830
+ }
1831
+ if (node.operator === "delete") {
1832
+ if (node.argument.type !== "MemberExpression")
1833
+ return true;
1834
+ const obj = evaluate(node.argument.object, scope);
1835
+ const prop = node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name;
1836
+ const name = assertPropAllowed(prop);
1837
+ if (isPlainObject(obj) && !Object.isFrozen(obj)) {
1838
+ delete obj[name];
1839
+ return true;
1840
+ }
1841
+ throw new Error("delete is only allowed on plain objects");
1842
+ }
1843
+ const arg = evaluate(node.argument, scope);
1844
+ switch (node.operator) {
1845
+ case "-":
1846
+ return -arg;
1847
+ case "+":
1848
+ return +arg;
1849
+ case "!":
1850
+ return !arg;
1851
+ case "~":
1852
+ return ~arg;
1853
+ case "typeof":
1854
+ return typeof arg;
1855
+ case "void":
1856
+ return void 0;
1857
+ default:
1858
+ throw new Error(`Unsupported unary operator: ${node.operator}`);
1859
+ }
1860
+ }
1861
+ case "UpdateExpression": {
1862
+ const old = node.argument.type === "Identifier" ? scope.get(node.argument.name) : getMember(evaluate(node.argument.object, scope), node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name);
1863
+ const next = node.operator === "++" ? old + 1 : old - 1;
1864
+ assignTo(node.argument, next, scope);
1865
+ return node.prefix ? next : old;
1866
+ }
1867
+ case "BinaryExpression": {
1868
+ if (node.operator === "in") {
1869
+ const key = assertPropAllowed(evaluate(node.left, scope));
1870
+ const obj = evaluate(node.right, scope);
1871
+ if (isPlainObject(obj))
1872
+ return Object.prototype.hasOwnProperty.call(obj, key);
1873
+ if (Array.isArray(obj))
1874
+ return Number(key) >= 0 && Number(key) < obj.length;
1875
+ throw new Error('"in" is only supported on plain objects and arrays');
1876
+ }
1877
+ if (node.operator === "instanceof")
1878
+ throw new Error("instanceof is not supported");
1879
+ const l = evaluate(node.left, scope);
1880
+ const r = evaluate(node.right, scope);
1881
+ switch (node.operator) {
1882
+ case "+":
1883
+ return l + r;
1884
+ case "-":
1885
+ return l - r;
1886
+ case "*":
1887
+ return l * r;
1888
+ case "/":
1889
+ return l / r;
1890
+ case "%":
1891
+ return l % r;
1892
+ case "**":
1893
+ return l ** r;
1894
+ case "==":
1895
+ return l == r;
1896
+ // eslint-disable-line eqeqeq
1897
+ case "!=":
1898
+ return l != r;
1899
+ // eslint-disable-line eqeqeq
1900
+ case "===":
1901
+ return l === r;
1902
+ case "!==":
1903
+ return l !== r;
1904
+ case "<":
1905
+ return l < r;
1906
+ case "<=":
1907
+ return l <= r;
1908
+ case ">":
1909
+ return l > r;
1910
+ case ">=":
1911
+ return l >= r;
1912
+ case "&":
1913
+ return l & r;
1914
+ case "|":
1915
+ return l | r;
1916
+ case "^":
1917
+ return l ^ r;
1918
+ case "<<":
1919
+ return l << r;
1920
+ case ">>":
1921
+ return l >> r;
1922
+ case ">>>":
1923
+ return l >>> r;
1924
+ default:
1925
+ throw new Error(`Unsupported binary operator: ${node.operator}`);
1926
+ }
1927
+ }
1928
+ case "LogicalExpression": {
1929
+ const l = evaluate(node.left, scope);
1930
+ if (node.operator === "&&")
1931
+ return l ? evaluate(node.right, scope) : l;
1932
+ if (node.operator === "||")
1933
+ return l ? l : evaluate(node.right, scope);
1934
+ if (node.operator === "??")
1935
+ return l ?? evaluate(node.right, scope);
1936
+ throw new Error(`Unsupported logical operator: ${node.operator}`);
1937
+ }
1938
+ case "AssignmentExpression": {
1939
+ let value;
1940
+ if (node.operator === "=") {
1941
+ value = evaluate(node.right, scope);
1942
+ } else {
1943
+ const current = node.left.type === "Identifier" ? scope.get(node.left.name) : getMember(evaluate(node.left.object, scope), node.left.computed ? evaluate(node.left.property, scope) : node.left.property.name);
1944
+ const rhs = () => evaluate(node.right, scope);
1945
+ switch (node.operator) {
1946
+ case "+=":
1947
+ value = current + rhs();
1948
+ break;
1949
+ case "-=":
1950
+ value = current - rhs();
1951
+ break;
1952
+ case "*=":
1953
+ value = current * rhs();
1954
+ break;
1955
+ case "/=":
1956
+ value = current / rhs();
1957
+ break;
1958
+ case "%=":
1959
+ value = current % rhs();
1960
+ break;
1961
+ case "**=":
1962
+ value = current ** rhs();
1963
+ break;
1964
+ case "&=":
1965
+ value = current & rhs();
1966
+ break;
1967
+ case "|=":
1968
+ value = current | rhs();
1969
+ break;
1970
+ case "^=":
1971
+ value = current ^ rhs();
1972
+ break;
1973
+ case "<<=":
1974
+ value = current << rhs();
1975
+ break;
1976
+ case ">>=":
1977
+ value = current >> rhs();
1978
+ break;
1979
+ case ">>>=":
1980
+ value = current >>> rhs();
1981
+ break;
1982
+ case "&&=":
1983
+ value = current ? rhs() : current;
1984
+ break;
1985
+ case "||=":
1986
+ value = current ? current : rhs();
1987
+ break;
1988
+ case "??=":
1989
+ value = current ?? rhs();
1990
+ break;
1991
+ default:
1992
+ throw new Error(`Unsupported assignment operator: ${node.operator}`);
1993
+ }
1994
+ }
1995
+ assignTo(node.left, value, scope);
1996
+ return value;
1997
+ }
1998
+ case "ConditionalExpression":
1999
+ return evaluate(node.test, scope) ? evaluate(node.consequent, scope) : evaluate(node.alternate, scope);
2000
+ case "CallExpression": {
2001
+ const args = [];
2002
+ for (const argNode of node.arguments) {
2003
+ if (argNode.type === "SpreadElement") {
2004
+ const spread = evaluate(argNode.argument, scope);
2005
+ if (!Array.isArray(spread) && typeof spread !== "string") {
2006
+ throw new Error("Spread only supports arrays and strings");
2007
+ }
2008
+ for (const item of spread)
2009
+ args.push(item);
2010
+ } else {
2011
+ args.push(evaluate(argNode, scope));
2012
+ }
2013
+ }
2014
+ if (node.callee.type === "MemberExpression") {
2015
+ const obj = evaluate(node.callee.object, scope);
2016
+ if (node.optional && (obj === null || obj === void 0))
2017
+ return void 0;
2018
+ const prop = node.callee.computed ? evaluate(node.callee.property, scope) : node.callee.property.name;
2019
+ const method = getMember(obj, prop);
2020
+ if (node.callee.optional && (method === null || method === void 0))
2021
+ return void 0;
2022
+ return callFunction(method, obj, args, `${String(prop)}`);
2023
+ }
2024
+ const fn = evaluate(node.callee, scope);
2025
+ if (node.optional && (fn === null || fn === void 0))
2026
+ return void 0;
2027
+ const desc = node.callee.type === "Identifier" ? node.callee.name : "expression";
2028
+ return callFunction(fn, void 0, args, desc);
2029
+ }
2030
+ case "MemberExpression": {
2031
+ const obj = evaluate(node.object, scope);
2032
+ if (node.optional && (obj === null || obj === void 0))
2033
+ return void 0;
2034
+ const prop = node.computed ? evaluate(node.property, scope) : node.property.name;
2035
+ return getMember(obj, prop);
2036
+ }
2037
+ case "ChainExpression":
2038
+ return evaluate(node.expression, scope);
2039
+ case "SequenceExpression": {
2040
+ let result;
2041
+ for (const expr of node.expressions)
2042
+ result = evaluate(expr, scope);
2043
+ return result;
2044
+ }
2045
+ case "NewExpression":
2046
+ throw new Error('"new" is not supported in build code');
2047
+ case "ThisExpression":
2048
+ throw new Error('"this" is not supported in build code');
2049
+ case "AwaitExpression":
2050
+ throw new Error("await is not supported in build code");
2051
+ case "TaggedTemplateExpression":
2052
+ throw new Error("Tagged templates are not supported in build code");
2053
+ default:
2054
+ throw new Error(`Unsupported expression: ${node.type}`);
2055
+ }
2056
+ }
2057
+ executeBlock(ast.body, globalScope);
2058
+ }
2059
+ var ScriptTimeoutError, BreakSignal, ContinueSignal, ReturnSignal, FORBIDDEN_PROPS, ARRAY_MEMBERS, STRING_MEMBERS, NUMBER_MEMBERS, Scope;
2060
+ var init_build_interpreter = __esm({
2061
+ "../core/dist/tools/build-interpreter.js"() {
2062
+ "use strict";
2063
+ ScriptTimeoutError = class extends Error {
2064
+ constructor(message) {
2065
+ super(message);
2066
+ this.name = "ScriptTimeoutError";
2067
+ }
2068
+ };
2069
+ BreakSignal = class {
2070
+ label;
2071
+ constructor(label) {
2072
+ this.label = label;
2073
+ }
2074
+ };
2075
+ ContinueSignal = class {
2076
+ label;
2077
+ constructor(label) {
2078
+ this.label = label;
2079
+ }
2080
+ };
2081
+ ReturnSignal = class {
2082
+ value;
2083
+ constructor(value) {
2084
+ this.value = value;
2085
+ }
2086
+ };
2087
+ FORBIDDEN_PROPS = /* @__PURE__ */ new Set([
2088
+ "__proto__",
2089
+ "constructor",
2090
+ "prototype",
2091
+ "__defineGetter__",
2092
+ "__defineSetter__",
2093
+ "__lookupGetter__",
2094
+ "__lookupSetter__",
2095
+ "caller",
2096
+ "callee",
2097
+ "arguments",
2098
+ "bind",
2099
+ "call",
2100
+ "apply"
2101
+ ]);
2102
+ ARRAY_MEMBERS = /* @__PURE__ */ new Set([
2103
+ "length",
2104
+ "push",
2105
+ "pop",
2106
+ "shift",
2107
+ "unshift",
2108
+ "slice",
2109
+ "splice",
2110
+ "indexOf",
2111
+ "lastIndexOf",
2112
+ "includes",
2113
+ "join",
2114
+ "map",
2115
+ "filter",
2116
+ "forEach",
2117
+ "reduce",
2118
+ "reduceRight",
2119
+ "concat",
2120
+ "reverse",
2121
+ "sort",
2122
+ "find",
2123
+ "findIndex",
2124
+ "some",
2125
+ "every",
2126
+ "flat",
2127
+ "flatMap",
2128
+ "fill"
2129
+ ]);
2130
+ STRING_MEMBERS = /* @__PURE__ */ new Set([
2131
+ "length",
2132
+ "charAt",
2133
+ "charCodeAt",
2134
+ "codePointAt",
2135
+ "indexOf",
2136
+ "lastIndexOf",
2137
+ "includes",
2138
+ "startsWith",
2139
+ "endsWith",
2140
+ "slice",
2141
+ "substring",
2142
+ "toUpperCase",
2143
+ "toLowerCase",
2144
+ "trim",
2145
+ "trimStart",
2146
+ "trimEnd",
2147
+ "split",
2148
+ "repeat",
2149
+ "padStart",
2150
+ "padEnd",
2151
+ "concat",
2152
+ "at",
2153
+ "replace",
2154
+ "replaceAll"
2155
+ ]);
2156
+ NUMBER_MEMBERS = /* @__PURE__ */ new Set(["toFixed", "toPrecision", "toString"]);
2157
+ Scope = class {
2158
+ parent;
2159
+ isFunctionScope;
2160
+ vars = /* @__PURE__ */ new Map();
2161
+ consts = /* @__PURE__ */ new Set();
2162
+ constructor(parent, isFunctionScope = false) {
2163
+ this.parent = parent;
2164
+ this.isFunctionScope = isFunctionScope;
2165
+ }
2166
+ declare(name, value, kind) {
2167
+ if (kind === "var" && !this.isFunctionScope && this.parent) {
2168
+ this.parent.declare(name, value, kind);
2169
+ return;
2170
+ }
2171
+ this.vars.set(name, value);
2172
+ if (kind === "const")
2173
+ this.consts.add(name);
2174
+ }
2175
+ resolve(name) {
2176
+ if (this.vars.has(name))
2177
+ return this;
2178
+ return this.parent?.resolve(name);
2179
+ }
2180
+ globalScope() {
2181
+ return this.parent ? this.parent.globalScope() : this;
2182
+ }
2183
+ has(name) {
2184
+ return this.resolve(name) !== void 0;
2185
+ }
2186
+ get(name) {
2187
+ const s = this.resolve(name);
2188
+ if (!s)
2189
+ throw new Error(`${name} is not defined`);
2190
+ return s.vars.get(name);
2191
+ }
2192
+ set(name, value) {
2193
+ const s = this.resolve(name);
2194
+ if (!s) {
2195
+ this.globalScope().vars.set(name, value);
2196
+ return;
2197
+ }
2198
+ if (s.consts.has(name))
2199
+ throw new Error(`Assignment to constant variable "${name}"`);
2200
+ s.vars.set(name, value);
2201
+ }
2202
+ };
2203
+ }
2204
+ });
2205
+
1086
2206
  // ../core/dist/tools/build-executor.js
1087
- import * as vm from "vm";
1088
2207
  function createSeededRng(seed) {
1089
2208
  let s = seed;
1090
2209
  return () => {
@@ -1434,6 +2553,7 @@ function runBuildExecutor(code, palette, seed, options) {
1434
2553
  ]);
1435
2554
  }
1436
2555
  const rng = createSeededRng(seed ?? 42);
2556
+ const safeMath = Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors(Math)));
1437
2557
  const sandbox = {
1438
2558
  part: partFn,
1439
2559
  rpart: rpartFn,
@@ -1450,25 +2570,21 @@ function runBuildExecutor(code, palette, seed, options) {
1450
2570
  column: columnFn,
1451
2571
  pew: pewFn,
1452
2572
  fence: fenceFn,
1453
- Math,
2573
+ Math: safeMath,
1454
2574
  GRID_SIZE: 1,
1455
2575
  rng,
1456
- console: { log: () => {
2576
+ console: Object.freeze({ log: () => {
1457
2577
  }, warn: () => {
1458
2578
  }, error: () => {
1459
- } }
2579
+ } })
1460
2580
  };
1461
- const context = vm.createContext(sandbox, {
1462
- codeGeneration: { strings: false, wasm: false }
1463
- });
1464
- const script = new vm.Script(code, { filename: "build-generator.js" });
1465
2581
  try {
1466
- script.runInContext(context, { timeout });
2582
+ runRestrictedScript(code, sandbox, { timeoutMs: timeout });
1467
2583
  } catch (err) {
1468
- if (err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
2584
+ if (err instanceof ScriptTimeoutError) {
1469
2585
  throw new Error(`Build code execution timed out after ${timeout}ms`);
1470
2586
  }
1471
- throw new Error(`Build code execution error: ${err.message}`);
2587
+ throw new Error(`Build code execution error: ${err?.message ?? String(err)}`);
1472
2588
  }
1473
2589
  if (parts.length === 0) {
1474
2590
  throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
@@ -1480,6 +2596,7 @@ var DEFAULT_TIMEOUT, DEFAULT_MAX_PARTS, VALID_SHAPES;
1480
2596
  var init_build_executor = __esm({
1481
2597
  "../core/dist/tools/build-executor.js"() {
1482
2598
  "use strict";
2599
+ init_build_interpreter();
1483
2600
  DEFAULT_TIMEOUT = 1e4;
1484
2601
  DEFAULT_MAX_PARTS = 1e4;
1485
2602
  VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
@@ -2105,7 +3222,7 @@ var init_managed_instance_registry = __esm({
2105
3222
 
2106
3223
  // ../core/dist/studio-instance-manager.js
2107
3224
  import { execFileSync, spawn } from "child_process";
2108
- import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
3225
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
2109
3226
  import { randomUUID } from "crypto";
2110
3227
  import * as os2 from "os";
2111
3228
  import * as path2 from "path";
@@ -2120,14 +3237,14 @@ function isWsl() {
2120
3237
  if (process.platform !== "linux")
2121
3238
  return false;
2122
3239
  try {
2123
- return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
3240
+ return /microsoft|wsl/i.test(readFileSync3("/proc/version", "utf8"));
2124
3241
  } catch {
2125
3242
  return false;
2126
3243
  }
2127
3244
  }
2128
3245
  function powershell(script) {
2129
3246
  return run("powershell.exe", ["-NoProfile", "-Command", script], {
2130
- cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3247
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
2131
3248
  });
2132
3249
  }
2133
3250
  function windowsLocalAppData() {
@@ -2137,7 +3254,7 @@ function windowsLocalAppData() {
2137
3254
  return void 0;
2138
3255
  try {
2139
3256
  return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
2140
- cwd: existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3257
+ cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
2141
3258
  });
2142
3259
  } catch {
2143
3260
  return void 0;
@@ -2149,7 +3266,7 @@ function toWslPath(windowsPath) {
2149
3266
  return run("wslpath", ["-u", windowsPath]);
2150
3267
  }
2151
3268
  function toStudioLaunchArg(arg) {
2152
- if (!isWsl() || !path2.isAbsolute(arg) || !existsSync(arg))
3269
+ if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
2153
3270
  return arg;
2154
3271
  return run("wslpath", ["-w", arg]);
2155
3272
  }
@@ -2175,13 +3292,44 @@ function resolveBaseplateTemplatePath() {
2175
3292
  path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
2176
3293
  ];
2177
3294
  for (const candidate of candidates) {
2178
- if (existsSync(candidate))
3295
+ if (existsSync2(candidate))
2179
3296
  return candidate;
2180
3297
  }
2181
3298
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
2182
3299
  }
3300
+ function isProcessAlive(pid) {
3301
+ try {
3302
+ process.kill(pid, 0);
3303
+ return true;
3304
+ } catch (err) {
3305
+ return err.code === "EPERM";
3306
+ }
3307
+ }
3308
+ function sweepStaleBaseplateFiles() {
3309
+ let entries;
3310
+ try {
3311
+ entries = readdirSync2(BASEPLATE_TEMP_DIR);
3312
+ } catch {
3313
+ return;
3314
+ }
3315
+ const cutoff = Date.now() - STALE_BASEPLATE_MAX_AGE_MS;
3316
+ for (const entry of entries) {
3317
+ const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
3318
+ if (!match)
3319
+ continue;
3320
+ if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
3321
+ continue;
3322
+ const file = path2.join(BASEPLATE_TEMP_DIR, entry);
3323
+ try {
3324
+ if (statSync2(file).mtimeMs < cutoff)
3325
+ rmSync2(file, { force: true });
3326
+ } catch {
3327
+ }
3328
+ }
3329
+ }
2183
3330
  function createBaseplatePlaceFile() {
2184
- mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
3331
+ mkdirSync3(BASEPLATE_TEMP_DIR, { recursive: true });
3332
+ sweepStaleBaseplateFiles();
2185
3333
  const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
2186
3334
  copyFileSync(resolveBaseplateTemplatePath(), file);
2187
3335
  return file;
@@ -2195,8 +3343,12 @@ function cleanupManagedBaseplateFiles(record) {
2195
3343
  return;
2196
3344
  if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
2197
3345
  return;
2198
- rmSync2(record.localPlaceFile, { force: true });
2199
- rmSync2(`${record.localPlaceFile}.lock`, { force: true });
3346
+ for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
3347
+ try {
3348
+ rmSync2(file, { force: true });
3349
+ } catch {
3350
+ }
3351
+ }
2200
3352
  }
2201
3353
  function prepareStudioLaunchOptions(options) {
2202
3354
  if (options.source !== "baseplate" || options.localPlaceFile)
@@ -2217,10 +3369,10 @@ function resolveStudioExe() {
2217
3369
  }
2218
3370
  const localAppData = windowsLocalAppData();
2219
3371
  const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
2220
- if (!existsSync(root)) {
3372
+ if (!existsSync2(root)) {
2221
3373
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
2222
3374
  }
2223
- const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3375
+ const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
2224
3376
  if (candidates.length === 0) {
2225
3377
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
2226
3378
  }
@@ -2255,7 +3407,7 @@ function listStudioProcesses() {
2255
3407
  function currentBootId() {
2256
3408
  if (process.platform === "linux") {
2257
3409
  try {
2258
- return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
3410
+ return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
2259
3411
  } catch {
2260
3412
  }
2261
3413
  }
@@ -2312,7 +3464,7 @@ function delay(ms) {
2312
3464
  function basenameAny(filePath) {
2313
3465
  return path2.basename(filePath.replace(/\\/g, "/"));
2314
3466
  }
2315
- var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
3467
+ var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
2316
3468
  var init_studio_instance_manager = __esm({
2317
3469
  "../core/dist/studio-instance-manager.js"() {
2318
3470
  "use strict";
@@ -2320,6 +3472,8 @@ var init_studio_instance_manager = __esm({
2320
3472
  BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
2321
3473
  BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
2322
3474
  BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
3475
+ STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3476
+ BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
2323
3477
  StudioInstanceManager = class {
2324
3478
  managedByInstanceId = /* @__PURE__ */ new Map();
2325
3479
  pending = /* @__PURE__ */ new Set();
@@ -2364,7 +3518,7 @@ var init_studio_instance_manager = __esm({
2364
3518
  const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
2365
3519
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
2366
3520
  const spawnOptions = {
2367
- cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
3521
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
2368
3522
  detached: true,
2369
3523
  stdio: "ignore"
2370
3524
  };
@@ -4388,6 +5542,7 @@ var init_tools = __esm({
4388
5542
  init_roblox_cookie_client();
4389
5543
  init_studio_instance_manager();
4390
5544
  init_image_decode();
5545
+ init_roblox_docs();
4391
5546
  init_jpeg_encoder();
4392
5547
  init_png_encoder();
4393
5548
  MAX_INLINE_IMAGE_BYTES = 6e6;
@@ -4459,6 +5614,17 @@ var init_tools = __esm({
4459
5614
  _textResult(body) {
4460
5615
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
4461
5616
  }
5617
+ async getRobloxDocs(name, docType, section) {
5618
+ if (!name || typeof name !== "string") {
5619
+ throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
5620
+ }
5621
+ const category = docType ?? "classes";
5622
+ if (!isDocCategory(category)) {
5623
+ throw new Error(`Invalid doc_type "${category}". Valid categories: ${DOC_CATEGORIES.join(", ")}`);
5624
+ }
5625
+ const result = await getRobloxDoc(category, name.trim(), section);
5626
+ return { content: [{ type: "text", text: result.content }] };
5627
+ }
4462
5628
  _parseTextResult(result) {
4463
5629
  const text = result?.content?.[0]?.text;
4464
5630
  if (typeof text !== "string")
@@ -6094,9 +7260,9 @@ ${code}`
6094
7260
  if (instance_id) {
6095
7261
  const managedClose = this.instanceManager.closeByInstanceId(instance_id);
6096
7262
  if (managedClose.status !== "not_found") {
6097
- this.bridge.unregisterInstanceId(instance_id);
7263
+ await this.bridge.unregisterInstanceIdEverywhere(instance_id);
6098
7264
  await sleep(500);
6099
- this.bridge.unregisterInstanceId(instance_id);
7265
+ await this.bridge.unregisterInstanceIdEverywhere(instance_id);
6100
7266
  return this._textResult({
6101
7267
  instance_id,
6102
7268
  message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
@@ -6119,7 +7285,7 @@ ${code}`
6119
7285
  instance_id
6120
7286
  });
6121
7287
  }
6122
- this.bridge.unregisterInstanceId(instance_id);
7288
+ await this.bridge.unregisterInstanceIdEverywhere(instance_id);
6123
7289
  return this._textResult({
6124
7290
  instance_id,
6125
7291
  message: "Studio instance closed."
@@ -6138,11 +7304,11 @@ ${code}`
6138
7304
  record2 = active[0];
6139
7305
  }
6140
7306
  if (record2.instanceId)
6141
- this.bridge.unregisterInstanceId(record2.instanceId);
7307
+ await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
6142
7308
  const closeResult = this.instanceManager.close(record2);
6143
7309
  if (record2.instanceId) {
6144
7310
  await sleep(500);
6145
- this.bridge.unregisterInstanceId(record2.instanceId);
7311
+ await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
6146
7312
  }
6147
7313
  return this._textResult({
6148
7314
  instance_id: record2.instanceId,
@@ -7903,21 +9069,31 @@ var init_proxy_bridge_service = __esm({
7903
9069
  init_bridge_service();
7904
9070
  ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
7905
9071
  primaryBaseUrl;
9072
+ authToken;
7906
9073
  proxyInstanceId;
7907
9074
  proxyRequestTimeout = 3e4;
7908
9075
  cachedInstances = [];
7909
9076
  refreshTimer;
7910
9077
  static REFRESH_INTERVAL_MS = 1e3;
7911
- constructor(primaryBaseUrl) {
9078
+ constructor(primaryBaseUrl, authToken) {
7912
9079
  super();
7913
9080
  this.primaryBaseUrl = primaryBaseUrl;
9081
+ this.authToken = authToken;
7914
9082
  this.proxyInstanceId = uuidv42();
7915
9083
  this.refreshInstances();
7916
9084
  this.refreshTimer = setInterval(() => this.refreshInstances(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
7917
9085
  }
9086
+ authHeaders(extra) {
9087
+ const headers = { ...extra };
9088
+ if (this.authToken)
9089
+ headers["X-MCP-Auth"] = this.authToken;
9090
+ return headers;
9091
+ }
7918
9092
  async refreshInstances() {
7919
9093
  try {
7920
- const res = await fetch(`${this.primaryBaseUrl}/instances`);
9094
+ const res = await fetch(`${this.primaryBaseUrl}/instances`, {
9095
+ headers: this.authHeaders()
9096
+ });
7921
9097
  if (!res.ok)
7922
9098
  return;
7923
9099
  const body = await res.json();
@@ -7930,6 +9106,24 @@ var init_proxy_bridge_service = __esm({
7930
9106
  getInstances() {
7931
9107
  return this.cachedInstances;
7932
9108
  }
9109
+ async unregisterInstanceIdEverywhere(instanceId) {
9110
+ const response = await fetch(`${this.primaryBaseUrl}/unregister-instance-id`, {
9111
+ method: "POST",
9112
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
9113
+ body: JSON.stringify({ instanceId })
9114
+ });
9115
+ if (!response.ok) {
9116
+ const body = await response.text().catch(() => "");
9117
+ throw new Error(`Proxy unregister failed (${response.status}): ${body || response.statusText}`);
9118
+ }
9119
+ const result = await response.json();
9120
+ const removed = Array.isArray(result.removed) ? result.removed : [];
9121
+ const removedKeys = new Set(removed.map((inst) => `${inst.instanceId}\0${inst.role}`));
9122
+ if (removedKeys.size > 0) {
9123
+ this.cachedInstances = this.cachedInstances.filter((inst) => !removedKeys.has(`${inst.instanceId}\0${inst.role}`));
9124
+ }
9125
+ return removed;
9126
+ }
7933
9127
  /** Called when this proxy is being discarded (e.g. promotion to primary
7934
9128
  replaced it). Stops the background refresh so it doesn't leak. */
7935
9129
  stop() {
@@ -7944,7 +9138,7 @@ var init_proxy_bridge_service = __esm({
7944
9138
  try {
7945
9139
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
7946
9140
  method: "POST",
7947
- headers: { "Content-Type": "application/json" },
9141
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
7948
9142
  body: JSON.stringify({
7949
9143
  endpoint,
7950
9144
  data,
@@ -7989,6 +9183,7 @@ var init_server = __esm({
7989
9183
  "../core/dist/server.js"() {
7990
9184
  "use strict";
7991
9185
  init_http_server();
9186
+ init_auth();
7992
9187
  init_tools();
7993
9188
  init_bridge_service();
7994
9189
  init_proxy_bridge_service();
@@ -8012,7 +9207,7 @@ var init_server = __esm({
8012
9207
  });
8013
9208
  this.bridge = new BridgeService();
8014
9209
  this.tools = new RobloxStudioTools(this.bridge);
8015
- registerEmptyResourceShim(this.server);
9210
+ registerResourceHandlers(this.server);
8016
9211
  this.setupToolHandlers();
8017
9212
  }
8018
9213
  setupToolHandlers() {
@@ -8058,14 +9253,28 @@ var init_server = __esm({
8058
9253
  }
8059
9254
  async run() {
8060
9255
  const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
8061
- const host = process.env.ROBLOX_STUDIO_HOST || "0.0.0.0";
9256
+ const host = process.env.ROBLOX_STUDIO_HOST?.trim() || "127.0.0.1";
9257
+ if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
9258
+ console.error(`WARNING: ROBLOX_STUDIO_HOST=${host} exposes the MCP bridge beyond this machine. Anyone who can reach the port and knows the auth token can control Roblox Studio.`);
9259
+ }
9260
+ const auth = resolveAuthToken();
9261
+ const security = {
9262
+ authToken: auth.token,
9263
+ authTokenHint: auth.source === "env" ? "The token comes from ROBLOX_STUDIO_AUTH_TOKEN." : auth.filePath ? `The token is in ${auth.filePath}.` : void 0,
9264
+ allowedOrigins: (process.env.ROBLOX_STUDIO_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o !== "")
9265
+ };
9266
+ if (auth.source === "disabled") {
9267
+ console.error("WARNING: ROBLOX_STUDIO_NO_AUTH is set - tool endpoints accept unauthenticated requests.");
9268
+ } else if (auth.filePath) {
9269
+ console.error(`Auth token loaded from ${auth.filePath} (HTTP clients must send X-MCP-Auth or Authorization: Bearer)`);
9270
+ }
8062
9271
  let bridgeMode = "primary";
8063
9272
  let httpHandle;
8064
9273
  let primaryApp;
8065
9274
  let boundPort = 0;
8066
9275
  let promotionInterval;
8067
9276
  try {
8068
- primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
9277
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
8069
9278
  const result = await listenWithRetry(primaryApp, host, basePort, 1);
8070
9279
  httpHandle = result.server;
8071
9280
  boundPort = result.port;
@@ -8074,7 +9283,7 @@ var init_server = __esm({
8074
9283
  } catch {
8075
9284
  bridgeMode = "proxy";
8076
9285
  primaryApp = void 0;
8077
- const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`);
9286
+ const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`, auth.token);
8078
9287
  this.bridge = proxyBridge;
8079
9288
  this.tools = new RobloxStudioTools(this.bridge);
8080
9289
  console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
@@ -8082,7 +9291,7 @@ var init_server = __esm({
8082
9291
  promotionInterval = setInterval(async () => {
8083
9292
  const candidateBridge = new BridgeService();
8084
9293
  const candidateTools = new RobloxStudioTools(candidateBridge);
8085
- const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
9294
+ const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config, security);
8086
9295
  try {
8087
9296
  const result = await listenWithRetry(candidateApp, host, basePort, 1);
8088
9297
  const oldBridge = this.bridge;
@@ -8107,7 +9316,7 @@ var init_server = __esm({
8107
9316
  let legacyHandle;
8108
9317
  let legacyApp;
8109
9318
  if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
8110
- legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
9319
+ legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
8111
9320
  try {
8112
9321
  const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
8113
9322
  legacyHandle = result.server;
@@ -9964,7 +11173,7 @@ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders,
9964
11173
  EXAMPLE - compact cabin (17 lines):
9965
11174
  room(0,0,0,8,4,6,"a","b","a")
9966
11175
  roof(0,4,0,8,6,"gable","c")
9967
- wall(-4,0,-2,4,0,-2,4,1,"a")
11176
+ wall(-4,-2,4,-2,4,1,"a")
9968
11177
  part(0,2,3,3,3,0.3,"a","Block",0.4)
9969
11178
  row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
9970
11179
  column(-3,0,-2,4,0.5,"a","b")
@@ -10758,6 +11967,31 @@ part(0,2,0,2,1,1,"b")`,
10758
11967
  },
10759
11968
  required: ["pattern", "replacement"]
10760
11969
  }
11970
+ },
11971
+ // === Documentation ===
11972
+ {
11973
+ name: "get_roblox_docs",
11974
+ category: "read",
11975
+ description: 'Fetch official Roblox engine API documentation as markdown from create.roblox.com. Call this BEFORE writing or editing code that uses an engine class, enum, datatype, or Luau library you are not fully certain about (e.g. ProximityPrompt, Enum.KeyCode, CFrame, TweenService) \u2014 the page includes the description, properties, methods, events, and code samples. Results are cached, so repeat lookups are cheap. Very large pages are truncated with a section index; pass section (e.g. "Properties", "Methods", "Events") to read one section in full.',
11976
+ inputSchema: {
11977
+ type: "object",
11978
+ properties: {
11979
+ name: {
11980
+ type: "string",
11981
+ description: 'Exact PascalCase name of the class, enum, datatype, or library (e.g. "ProximityPrompt", "KeyCode", "CFrame", "table")'
11982
+ },
11983
+ doc_type: {
11984
+ type: "string",
11985
+ enum: ["classes", "enums", "datatypes", "libraries", "globals"],
11986
+ description: "Documentation category (default: classes)"
11987
+ },
11988
+ section: {
11989
+ type: "string",
11990
+ description: 'Optional "##"-level section to return instead of the whole page (e.g. "Description", "Properties", "Methods", "Events", "Code Samples")'
11991
+ }
11992
+ },
11993
+ required: ["name"]
11994
+ }
10761
11995
  }
10762
11996
  ];
10763
11997
  DEPRECATED_TOOL_DEFINITIONS = [
@@ -10917,15 +12151,15 @@ part(0,2,0,2,1,1,"b")`,
10917
12151
  });
10918
12152
 
10919
12153
  // ../core/dist/install-plugin-helpers.js
10920
- import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
12154
+ import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync } from "fs";
10921
12155
  import { execSync } from "child_process";
10922
- import { join as join4 } from "path";
10923
- import { homedir as homedir4 } from "os";
12156
+ import { join as join5 } from "path";
12157
+ import { homedir as homedir5 } from "os";
10924
12158
  function isWSL() {
10925
12159
  if (process.platform !== "linux")
10926
12160
  return false;
10927
12161
  try {
10928
- const v = readFileSync5("/proc/version", "utf8");
12162
+ const v = readFileSync6("/proc/version", "utf8");
10929
12163
  return /microsoft|wsl/i.test(v);
10930
12164
  } catch {
10931
12165
  return false;
@@ -10943,7 +12177,7 @@ function getWindowsUserPluginsDir() {
10943
12177
  }).toString().trim();
10944
12178
  if (!linuxPath)
10945
12179
  return null;
10946
- return join4(linuxPath, "Roblox", "Plugins");
12180
+ return join5(linuxPath, "Roblox", "Plugins");
10947
12181
  } catch {
10948
12182
  return null;
10949
12183
  }
@@ -10952,7 +12186,7 @@ function getPluginsFolder() {
10952
12186
  if (process.env.MCP_PLUGINS_DIR)
10953
12187
  return process.env.MCP_PLUGINS_DIR;
10954
12188
  if (process.platform === "win32") {
10955
- return join4(process.env.LOCALAPPDATA || join4(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
12189
+ return join5(process.env.LOCALAPPDATA || join5(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
10956
12190
  }
10957
12191
  if (isWSL()) {
10958
12192
  const win = getWindowsUserPluginsDir();
@@ -10960,11 +12194,11 @@ function getPluginsFolder() {
10960
12194
  return win;
10961
12195
  console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
10962
12196
  }
10963
- return join4(homedir4(), "Documents", "Roblox", "Plugins");
12197
+ return join5(homedir5(), "Documents", "Roblox", "Plugins");
10964
12198
  }
10965
12199
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
10966
- const otherDest = join4(pluginsFolder, otherAssetName);
10967
- if (!existsSync4(otherDest))
12200
+ const otherDest = join5(pluginsFolder, otherAssetName);
12201
+ if (!existsSync5(otherDest))
10968
12202
  return;
10969
12203
  if (replace) {
10970
12204
  try {
@@ -11009,8 +12243,8 @@ __export(install_plugin_exports, {
11009
12243
  installBundledPlugin: () => installBundledPlugin,
11010
12244
  installPlugin: () => installPlugin
11011
12245
  });
11012
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
11013
- import { dirname as dirname3, join as join5 } from "path";
12246
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
12247
+ import { dirname as dirname4, join as join6 } from "path";
11014
12248
  import { fileURLToPath } from "url";
11015
12249
  import { get } from "https";
11016
12250
  function httpsGet(url) {
@@ -11070,8 +12304,8 @@ function prepareInstall({
11070
12304
  warn
11071
12305
  }) {
11072
12306
  const pluginsFolder = getPluginsFolder();
11073
- if (!existsSync5(pluginsFolder)) {
11074
- mkdirSync4(pluginsFolder, { recursive: true });
12307
+ if (!existsSync6(pluginsFolder)) {
12308
+ mkdirSync5(pluginsFolder, { recursive: true });
11075
12309
  }
11076
12310
  handleVariantConflict({
11077
12311
  pluginsFolder,
@@ -11083,23 +12317,23 @@ function prepareInstall({
11083
12317
  return pluginsFolder;
11084
12318
  }
11085
12319
  function bundledAssetPath() {
11086
- const currentDir = dirname3(fileURLToPath(import.meta.url));
12320
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
11087
12321
  const candidates = [
11088
- join5(currentDir, "..", "studio-plugin", ASSET_NAME),
11089
- join5(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
12322
+ join6(currentDir, "..", "studio-plugin", ASSET_NAME),
12323
+ join6(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
11090
12324
  ];
11091
- return candidates.find((candidate) => existsSync5(candidate)) ?? null;
12325
+ return candidates.find((candidate) => existsSync6(candidate)) ?? null;
11092
12326
  }
11093
12327
  function packageVersion() {
11094
- const currentDir = dirname3(fileURLToPath(import.meta.url));
11095
- const pkg = JSON.parse(readFileSync6(join5(currentDir, "..", "package.json"), "utf8"));
12328
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
12329
+ const pkg = JSON.parse(readFileSync7(join6(currentDir, "..", "package.json"), "utf8"));
11096
12330
  if (!pkg.version) {
11097
12331
  throw new Error("Package version not found");
11098
12332
  }
11099
12333
  return pkg.version;
11100
12334
  }
11101
12335
  function bundledPluginVersion(source) {
11102
- const match = readFileSync6(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
12336
+ const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
11103
12337
  return match ? match[1] : null;
11104
12338
  }
11105
12339
  function assertBundledPluginVersion(source) {
@@ -11112,9 +12346,9 @@ function assertBundledPluginVersion(source) {
11112
12346
  }
11113
12347
  }
11114
12348
  function filesMatch(a, b) {
11115
- if (!existsSync5(b)) return false;
11116
- const aBytes = readFileSync6(a);
11117
- const bBytes = readFileSync6(b);
12349
+ if (!existsSync6(b)) return false;
12350
+ const aBytes = readFileSync7(a);
12351
+ const bBytes = readFileSync7(b);
11118
12352
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
11119
12353
  }
11120
12354
  async function installBundledPlugin(options = {}) {
@@ -11127,7 +12361,7 @@ async function installBundledPlugin(options = {}) {
11127
12361
  }
11128
12362
  assertBundledPluginVersion(source);
11129
12363
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
11130
- const dest = join5(pluginsFolder, ASSET_NAME);
12364
+ const dest = join6(pluginsFolder, ASSET_NAME);
11131
12365
  if (filesMatch(source, dest)) return;
11132
12366
  copyFileSync2(source, dest);
11133
12367
  log(`Installed ${ASSET_NAME} to ${dest}`);
@@ -11140,7 +12374,7 @@ async function installPlugin(options = {}) {
11140
12374
  const bundled = bundledAssetPath();
11141
12375
  if (bundled) {
11142
12376
  assertBundledPluginVersion(bundled);
11143
- const dest2 = join5(pluginsFolder, ASSET_NAME);
12377
+ const dest2 = join6(pluginsFolder, ASSET_NAME);
11144
12378
  if (filesMatch(bundled, dest2)) {
11145
12379
  log(`${ASSET_NAME} already installed.`);
11146
12380
  return;
@@ -11155,7 +12389,7 @@ async function installPlugin(options = {}) {
11155
12389
  if (!asset) {
11156
12390
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
11157
12391
  }
11158
- const dest = join5(pluginsFolder, ASSET_NAME);
12392
+ const dest = join6(pluginsFolder, ASSET_NAME);
11159
12393
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
11160
12394
  await download(asset.browser_download_url, dest);
11161
12395
  log(`Installed to ${dest}`);