@n24q02m/better-godot-mcp 1.2.1 → 1.2.3

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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.mjs +103 -113
  3. package/build/src/tools/composite/animation.d.ts.map +1 -1
  4. package/build/src/tools/composite/animation.js +2 -1
  5. package/build/src/tools/composite/animation.js.map +1 -1
  6. package/build/src/tools/composite/audio.d.ts.map +1 -1
  7. package/build/src/tools/composite/audio.js +2 -1
  8. package/build/src/tools/composite/audio.js.map +1 -1
  9. package/build/src/tools/composite/config.d.ts.map +1 -1
  10. package/build/src/tools/composite/config.js +4 -0
  11. package/build/src/tools/composite/config.js.map +1 -1
  12. package/build/src/tools/composite/editor.js +4 -4
  13. package/build/src/tools/composite/editor.js.map +1 -1
  14. package/build/src/tools/composite/navigation.d.ts.map +1 -1
  15. package/build/src/tools/composite/navigation.js +2 -1
  16. package/build/src/tools/composite/navigation.js.map +1 -1
  17. package/build/src/tools/composite/nodes.d.ts.map +1 -1
  18. package/build/src/tools/composite/nodes.js +2 -1
  19. package/build/src/tools/composite/nodes.js.map +1 -1
  20. package/build/src/tools/composite/physics.d.ts.map +1 -1
  21. package/build/src/tools/composite/physics.js +3 -2
  22. package/build/src/tools/composite/physics.js.map +1 -1
  23. package/build/src/tools/composite/project.d.ts.map +1 -1
  24. package/build/src/tools/composite/project.js +2 -1
  25. package/build/src/tools/composite/project.js.map +1 -1
  26. package/build/src/tools/composite/resources.d.ts.map +1 -1
  27. package/build/src/tools/composite/resources.js +12 -14
  28. package/build/src/tools/composite/resources.js.map +1 -1
  29. package/build/src/tools/composite/scenes.d.ts.map +1 -1
  30. package/build/src/tools/composite/scenes.js +13 -13
  31. package/build/src/tools/composite/scenes.js.map +1 -1
  32. package/build/src/tools/composite/scripts.d.ts.map +1 -1
  33. package/build/src/tools/composite/scripts.js +10 -9
  34. package/build/src/tools/composite/scripts.js.map +1 -1
  35. package/build/src/tools/composite/shader.d.ts.map +1 -1
  36. package/build/src/tools/composite/shader.js +5 -4
  37. package/build/src/tools/composite/shader.js.map +1 -1
  38. package/build/src/tools/composite/signals.d.ts.map +1 -1
  39. package/build/src/tools/composite/signals.js +2 -1
  40. package/build/src/tools/composite/signals.js.map +1 -1
  41. package/build/src/tools/composite/tilemap.d.ts.map +1 -1
  42. package/build/src/tools/composite/tilemap.js +4 -3
  43. package/build/src/tools/composite/tilemap.js.map +1 -1
  44. package/build/src/tools/composite/ui.d.ts.map +1 -1
  45. package/build/src/tools/composite/ui.js +3 -2
  46. package/build/src/tools/composite/ui.js.map +1 -1
  47. package/package.json +5 -5
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Composite MCP Server for Godot Engine - Optimized for AI Agents**
4
4
 
5
5
  [![CI](https://github.com/n24q02m/better-godot-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/n24q02m/better-godot-mcp/actions/workflows/ci.yml)
6
- [![Codecov](https://img.shields.io/codecov/c/github/n24q02m/better-godot-mcp?logo=codecov&logoColor=white)](https://codecov.io/gh/n24q02m/better-godot-mcp)
6
+ [![codecov](https://codecov.io/gh/n24q02m/better-godot-mcp/graph/badge.svg?token=PF94LT0K2L)](https://codecov.io/gh/n24q02m/better-godot-mcp)
7
7
  [![npm](https://img.shields.io/npm/v/@n24q02m/better-godot-mcp?logo=npm&logoColor=white)](https://www.npmjs.com/package/@n24q02m/better-godot-mcp)
8
8
  [![Docker](https://img.shields.io/docker/v/n24q02m/better-godot-mcp?label=docker&logo=docker&logoColor=white&sort=semver)](https://hub.docker.com/r/n24q02m/better-godot-mcp)
9
9
  [![License: MIT](https://img.shields.io/github/license/n24q02m/better-godot-mcp)](LICENSE)
package/bin/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // @n24q02m/better-godot-mcp v1.2.1
2
+ // @n24q02m/better-godot-mcp v1.2.3
3
3
  // Auto-generated CLI bundle - do not edit directly
4
4
 
5
5
 
@@ -161,7 +161,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
161
161
 
162
162
  // src/tools/composite/animation.ts
163
163
  import { existsSync as existsSync2, readFileSync, writeFileSync } from "node:fs";
164
- import { resolve } from "node:path";
164
+ import { resolve as resolve2 } from "node:path";
165
165
 
166
166
  // src/tools/helpers/errors.ts
167
167
  var GodotMCPError = class extends Error {
@@ -198,9 +198,25 @@ function formatJSON(data) {
198
198
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
199
199
  }
200
200
 
201
+ // src/tools/helpers/paths.ts
202
+ import { isAbsolute, relative, resolve } from "node:path";
203
+ function safeResolve(baseDir, targetPath) {
204
+ const resolvedBase = resolve(baseDir);
205
+ const resolvedTarget = resolve(resolvedBase, targetPath);
206
+ const relativePath = relative(resolvedBase, resolvedTarget);
207
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
208
+ throw new GodotMCPError(
209
+ `Access denied: Path '${targetPath}' resolves to '${resolvedTarget}' which is outside the project root '${resolvedBase}'.`,
210
+ "INVALID_ARGS",
211
+ "Ensure all file paths are within the project directory."
212
+ );
213
+ }
214
+ return resolvedTarget;
215
+ }
216
+
201
217
  // src/tools/composite/animation.ts
202
218
  function resolveScene(projectPath, scenePath) {
203
- const fullPath = projectPath ? resolve(projectPath, scenePath) : resolve(scenePath);
219
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve2(scenePath);
204
220
  if (!existsSync2(fullPath))
205
221
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check the file path.");
206
222
  return fullPath;
@@ -326,13 +342,13 @@ Track data format: tracks/N/keys = { "times": PackedFloat32Array(0, 1), "values"
326
342
 
327
343
  // src/tools/composite/audio.ts
328
344
  import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
329
- import { resolve as resolve2 } from "node:path";
345
+ import { resolve as resolve3 } from "node:path";
330
346
  async function handleAudio(action, args, config) {
331
347
  const projectPath = args.project_path || config.projectPath;
332
348
  switch (action) {
333
349
  case "list_buses": {
334
350
  if (!projectPath) throw new GodotMCPError("No project path specified", "INVALID_ARGS", "Provide project_path.");
335
- const busLayoutPath = resolve2(projectPath, "default_bus_layout.tres");
351
+ const busLayoutPath = resolve3(projectPath, "default_bus_layout.tres");
336
352
  if (!existsSync3(busLayoutPath)) {
337
353
  return formatJSON({ buses: [{ name: "Master", volume: 0, effects: [] }], note: "Using default bus layout." });
338
354
  }
@@ -350,7 +366,7 @@ async function handleAudio(action, args, config) {
350
366
  const busName = args.bus_name;
351
367
  if (!busName) throw new GodotMCPError("No bus_name specified", "INVALID_ARGS", "Provide bus name.");
352
368
  const sendTo = args.send_to || "Master";
353
- const busLayoutPath = resolve2(projectPath, "default_bus_layout.tres");
369
+ const busLayoutPath = resolve3(projectPath, "default_bus_layout.tres");
354
370
  let content;
355
371
  if (existsSync3(busLayoutPath)) {
356
372
  content = readFileSync2(busLayoutPath, "utf-8");
@@ -394,7 +410,7 @@ ${newBus}
394
410
  );
395
411
  }
396
412
  const fullEffectType = effectType.startsWith("AudioEffect") ? effectType : `AudioEffect${effectType}`;
397
- const busLayoutPath = resolve2(projectPath, "default_bus_layout.tres");
413
+ const busLayoutPath = resolve3(projectPath, "default_bus_layout.tres");
398
414
  let content;
399
415
  if (existsSync3(busLayoutPath)) {
400
416
  content = readFileSync2(busLayoutPath, "utf-8");
@@ -451,7 +467,7 @@ ${effectRef}`;
451
467
  const streamType = args.stream_type || "2D";
452
468
  const parent = args.parent || ".";
453
469
  const bus = args.bus || "Master";
454
- const fullPath = projectPath ? resolve2(projectPath, scenePath) : resolve2(scenePath);
470
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve3(scenePath);
455
471
  if (!existsSync3(fullPath))
456
472
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check file path.");
457
473
  let content = readFileSync2(fullPath, "utf-8");
@@ -500,6 +516,13 @@ async function handleConfig(action, args, config) {
500
516
  if (!validKeys.includes(key)) {
501
517
  throw new GodotMCPError(`Invalid config key: ${key}`, "INVALID_ARGS", `Valid keys: ${validKeys.join(", ")}`);
502
518
  }
519
+ if ((key === "project_path" || key === "godot_path") && /[;&|`$(){}]/.test(value)) {
520
+ throw new GodotMCPError(
521
+ `Invalid characters in ${key}`,
522
+ "INVALID_ARGS",
523
+ "Path must not contain shell metacharacters: ; & | ` $ ( ) { }"
524
+ );
525
+ }
503
526
  runtimeConfig[key] = value;
504
527
  if (key === "project_path") {
505
528
  config.projectPath = value;
@@ -518,8 +541,8 @@ async function handleConfig(action, args, config) {
518
541
  }
519
542
 
520
543
  // src/tools/composite/editor.ts
521
- import { exec } from "node:child_process";
522
- import { resolve as resolve3 } from "node:path";
544
+ import { execFile } from "node:child_process";
545
+ import { resolve as resolve4 } from "node:path";
523
546
  import { promisify } from "node:util";
524
547
 
525
548
  // src/godot/headless.ts
@@ -568,11 +591,11 @@ function launchGodotEditor(godotPath, projectPath) {
568
591
  }
569
592
 
570
593
  // src/tools/composite/editor.ts
571
- var execAsync = promisify(exec);
594
+ var execFileAsync = promisify(execFile);
572
595
  async function getGodotProcessesAsync() {
573
596
  try {
574
597
  if (process.platform === "win32") {
575
- const { stdout: stdout2 } = await execAsync('tasklist /FI "IMAGENAME eq godot*" /FO CSV /NH', {
598
+ const { stdout: stdout2 } = await execFileAsync("tasklist", ["/FI", "IMAGENAME eq godot*", "/FO", "CSV", "/NH"], {
576
599
  encoding: "utf-8"
577
600
  });
578
601
  return stdout2.split("\n").filter((line) => line.includes("godot")).map((line) => {
@@ -580,7 +603,7 @@ async function getGodotProcessesAsync() {
580
603
  return { pid: parts[1] || "unknown", name: parts[0] || "godot" };
581
604
  });
582
605
  }
583
- const { stdout } = await execAsync("pgrep -la godot", {
606
+ const { stdout } = await execFileAsync("pgrep", ["-la", "godot"], {
584
607
  encoding: "utf-8"
585
608
  });
586
609
  return stdout.split("\n").filter(Boolean).map((line) => {
@@ -605,7 +628,7 @@ async function handleEditor(action, args, config) {
605
628
  if (!projectPath) {
606
629
  throw new GodotMCPError("No project path specified", "INVALID_ARGS", "Provide project_path argument.");
607
630
  }
608
- const { pid } = launchGodotEditor(config.godotPath, resolve3(projectPath));
631
+ const { pid } = launchGodotEditor(config.godotPath, resolve4(projectPath));
609
632
  return formatSuccess(`Godot editor launched (PID: ${pid})`);
610
633
  }
611
634
  case "status": {
@@ -680,7 +703,7 @@ async function handleHelp(action, args) {
680
703
 
681
704
  // src/tools/composite/input-map.ts
682
705
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
683
- import { join as join3, resolve as resolve4 } from "node:path";
706
+ import { join as join3, resolve as resolve5 } from "node:path";
684
707
  var GODOT_KEY_CODES = {
685
708
  // Letters (ASCII)
686
709
  KEY_A: 65,
@@ -790,7 +813,7 @@ function resolveMouseCode(value) {
790
813
  }
791
814
  function getProjectGodotPath(projectPath) {
792
815
  if (!projectPath) throw new GodotMCPError("No project path specified", "INVALID_ARGS", "Provide project_path.");
793
- const configPath = join3(resolve4(projectPath), "project.godot");
816
+ const configPath = join3(resolve5(projectPath), "project.godot");
794
817
  if (!existsSync5(configPath))
795
818
  throw new GodotMCPError("No project.godot found", "PROJECT_NOT_FOUND", "Verify the project path.");
796
819
  return configPath;
@@ -970,9 +993,9 @@ ${actionLine}`);
970
993
 
971
994
  // src/tools/composite/navigation.ts
972
995
  import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
973
- import { resolve as resolve5 } from "node:path";
996
+ import { resolve as resolve6 } from "node:path";
974
997
  function resolveScene2(projectPath, scenePath) {
975
- const fullPath = projectPath ? resolve5(projectPath, scenePath) : resolve5(scenePath);
998
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve6(scenePath);
976
999
  if (!existsSync6(fullPath))
977
1000
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check the file path.");
978
1001
  return fullPath;
@@ -1054,7 +1077,7 @@ async function handleNavigation(action, args, config) {
1054
1077
 
1055
1078
  // src/tools/composite/nodes.ts
1056
1079
  import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
1057
- import { resolve as resolve6 } from "node:path";
1080
+ import { resolve as resolve7 } from "node:path";
1058
1081
 
1059
1082
  // src/tools/helpers/scene-parser.ts
1060
1083
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
@@ -1293,7 +1316,7 @@ function mapToSceneNode(node) {
1293
1316
  };
1294
1317
  }
1295
1318
  function resolveScenePath(projectPath, scenePath) {
1296
- return projectPath ? resolve6(projectPath, scenePath) : resolve6(scenePath);
1319
+ return projectPath ? safeResolve(projectPath, scenePath) : resolve7(scenePath);
1297
1320
  }
1298
1321
  async function handleNodes(action, args, config) {
1299
1322
  const projectPath = args.project_path || config.projectPath;
@@ -1424,7 +1447,7 @@ ${nodeDecl}`;
1424
1447
 
1425
1448
  // src/tools/composite/physics.ts
1426
1449
  import { existsSync as existsSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
1427
- import { join as join4, resolve as resolve7 } from "node:path";
1450
+ import { join as join4, resolve as resolve8 } from "node:path";
1428
1451
 
1429
1452
  // src/tools/helpers/project-settings.ts
1430
1453
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
@@ -1531,7 +1554,7 @@ async function handlePhysics(action, args, config) {
1531
1554
  switch (action) {
1532
1555
  case "layers": {
1533
1556
  if (!projectPath) throw new GodotMCPError("No project path specified", "INVALID_ARGS", "Provide project_path.");
1534
- const configPath = join4(resolve7(projectPath), "project.godot");
1557
+ const configPath = join4(resolve8(projectPath), "project.godot");
1535
1558
  if (!existsSync8(configPath))
1536
1559
  throw new GodotMCPError("No project.godot found", "PROJECT_NOT_FOUND", "Verify project path.");
1537
1560
  const settings = parseProjectSettings(configPath);
@@ -1553,7 +1576,7 @@ async function handlePhysics(action, args, config) {
1553
1576
  if (!nodeName) throw new GodotMCPError("No node name specified", "INVALID_ARGS", "Provide node name.");
1554
1577
  const collisionLayer = args.collision_layer;
1555
1578
  const collisionMask = args.collision_mask;
1556
- const fullPath = projectPath ? resolve7(projectPath, scenePath) : resolve7(scenePath);
1579
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve8(scenePath);
1557
1580
  if (!existsSync8(fullPath))
1558
1581
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check file path.");
1559
1582
  let content = readFileSync9(fullPath, "utf-8");
@@ -1579,7 +1602,7 @@ collision_mask = ${collisionMask}`;
1579
1602
  if (!scenePath) throw new GodotMCPError("No scene_path specified", "INVALID_ARGS", "Provide scene_path.");
1580
1603
  const nodeName = args.name;
1581
1604
  if (!nodeName) throw new GodotMCPError("No node name specified", "INVALID_ARGS", "Provide node name.");
1582
- const fullPath = projectPath ? resolve7(projectPath, scenePath) : resolve7(scenePath);
1605
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve8(scenePath);
1583
1606
  if (!existsSync8(fullPath))
1584
1607
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check file path.");
1585
1608
  let content = readFileSync9(fullPath, "utf-8");
@@ -1610,7 +1633,7 @@ freeze = ${args.freeze}`;
1610
1633
  const dimension = args.dimension || "2d";
1611
1634
  const name = args.name;
1612
1635
  if (!name) throw new GodotMCPError("No name specified", "INVALID_ARGS", "Provide layer name.");
1613
- const configPath = join4(resolve7(projectPath), "project.godot");
1636
+ const configPath = join4(resolve8(projectPath), "project.godot");
1614
1637
  if (!existsSync8(configPath))
1615
1638
  throw new GodotMCPError("No project.godot found", "PROJECT_NOT_FOUND", "Verify project path.");
1616
1639
  const content = readFileSync9(configPath, "utf-8");
@@ -1632,7 +1655,7 @@ freeze = ${args.freeze}`;
1632
1655
  import { execFileSync as execFileSync3 } from "node:child_process";
1633
1656
  import { existsSync as existsSync9 } from "node:fs";
1634
1657
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1635
- import { join as join5, resolve as resolve8 } from "node:path";
1658
+ import { join as join5, resolve as resolve9 } from "node:path";
1636
1659
  async function parseProjectGodot(projectPath) {
1637
1660
  const configPath = join5(projectPath, "project.godot");
1638
1661
  if (!existsSync9(configPath)) {
@@ -1683,7 +1706,7 @@ async function handleProject(action, args, config) {
1683
1706
  "Provide project_path argument or set it via config.set action."
1684
1707
  );
1685
1708
  }
1686
- const info = await parseProjectGodot(resolve8(projectPath));
1709
+ const info = await parseProjectGodot(resolve9(projectPath));
1687
1710
  return formatJSON(info);
1688
1711
  }
1689
1712
  case "version": {
@@ -1699,7 +1722,7 @@ async function handleProject(action, args, config) {
1699
1722
  const projectPath = args.project_path || config.projectPath;
1700
1723
  if (!projectPath)
1701
1724
  throw new GodotMCPError("No project path specified", "INVALID_ARGS", "Provide project_path argument.");
1702
- const { pid } = runGodotProject(config.godotPath, resolve8(projectPath));
1725
+ const { pid } = runGodotProject(config.godotPath, resolve9(projectPath));
1703
1726
  return formatSuccess(`Godot project started (PID: ${pid})`);
1704
1727
  }
1705
1728
  case "stop": {
@@ -1720,7 +1743,7 @@ async function handleProject(action, args, config) {
1720
1743
  const key = args.key;
1721
1744
  if (!key)
1722
1745
  throw new GodotMCPError("No key specified", "INVALID_ARGS", 'Provide key (e.g., "application/config/name").');
1723
- const configPath = join5(resolve8(projectPath), "project.godot");
1746
+ const configPath = join5(resolve9(projectPath), "project.godot");
1724
1747
  if (!existsSync9(configPath))
1725
1748
  throw new GodotMCPError("No project.godot found", "PROJECT_NOT_FOUND", "Verify the project path.");
1726
1749
  const settings = await parseProjectSettingsAsync(configPath);
@@ -1734,7 +1757,7 @@ async function handleProject(action, args, config) {
1734
1757
  const value = args.value;
1735
1758
  if (!key || value === void 0)
1736
1759
  throw new GodotMCPError("key and value required", "INVALID_ARGS", "Provide key and value.");
1737
- const configPath = join5(resolve8(projectPath), "project.godot");
1760
+ const configPath = join5(resolve9(projectPath), "project.godot");
1738
1761
  if (!existsSync9(configPath))
1739
1762
  throw new GodotMCPError("No project.godot found", "PROJECT_NOT_FOUND", "Verify the project path.");
1740
1763
  const content = await readFile2(configPath, "utf-8");
@@ -1759,10 +1782,10 @@ async function handleProject(action, args, config) {
1759
1782
  const result = execGodotSync(config.godotPath, [
1760
1783
  "--headless",
1761
1784
  "--path",
1762
- resolve8(projectPath),
1785
+ resolve9(projectPath),
1763
1786
  "--export-release",
1764
1787
  preset,
1765
- resolve8(projectPath, outputPath)
1788
+ safeResolve(projectPath, outputPath)
1766
1789
  ]);
1767
1790
  return formatSuccess(`Export complete: ${outputPath}
1768
1791
  ${result.stdout}`);
@@ -1778,12 +1801,11 @@ ${result.stdout}`);
1778
1801
 
1779
1802
  // src/tools/composite/resources.ts
1780
1803
  import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, unlinkSync } from "node:fs";
1781
- import { extname, join as join6, relative, resolve as resolve9 } from "node:path";
1804
+ import { extname, join as join6, relative as relative2, resolve as resolve10 } from "node:path";
1782
1805
  var RESOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
1783
1806
  ".tres",
1784
1807
  ".res",
1785
1808
  ".tscn",
1786
- ".tscn",
1787
1809
  ".png",
1788
1810
  ".jpg",
1789
1811
  ".jpeg",
@@ -1798,19 +1820,17 @@ var RESOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
1798
1820
  ".gdshaderinc",
1799
1821
  ".import"
1800
1822
  ]);
1801
- function findResourceFiles(dir, extensions) {
1823
+ function findResourceFiles(dir, extensions, results = []) {
1802
1824
  const exts = extensions || RESOURCE_EXTENSIONS;
1803
- const results = [];
1804
1825
  try {
1805
- const entries = readdirSync2(dir);
1826
+ const entries = readdirSync2(dir, { withFileTypes: true });
1806
1827
  for (const entry of entries) {
1807
- if (entry.startsWith(".") || entry === "node_modules" || entry === "build") continue;
1808
- const fullPath = join6(dir, entry);
1809
- const stat = statSync(fullPath);
1810
- if (stat.isDirectory()) {
1811
- results.push(...findResourceFiles(fullPath, exts));
1812
- } else if (exts.has(extname(entry).toLowerCase())) {
1813
- results.push({ path: fullPath, size: stat.size });
1828
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "build") continue;
1829
+ const fullPath = join6(dir, entry.name);
1830
+ if (entry.isDirectory()) {
1831
+ findResourceFiles(fullPath, exts, results);
1832
+ } else if (exts.has(extname(entry.name).toLowerCase())) {
1833
+ results.push({ path: fullPath, size: statSync(fullPath).size });
1814
1834
  }
1815
1835
  }
1816
1836
  } catch {
@@ -1822,7 +1842,7 @@ async function handleResources(action, args, config) {
1822
1842
  switch (action) {
1823
1843
  case "list": {
1824
1844
  if (!projectPath) throw new GodotMCPError("No project path specified", "INVALID_ARGS", "Provide project_path.");
1825
- const resolvedPath = resolve9(projectPath);
1845
+ const resolvedPath = resolve10(projectPath);
1826
1846
  const filterType = args.type;
1827
1847
  let exts;
1828
1848
  if (filterType) {
@@ -1838,7 +1858,7 @@ async function handleResources(action, args, config) {
1838
1858
  }
1839
1859
  const resources = findResourceFiles(resolvedPath, exts);
1840
1860
  const relativePaths = resources.map((r) => ({
1841
- path: relative(resolvedPath, r.path).replace(/\\/g, "/"),
1861
+ path: relative2(resolvedPath, r.path).replace(/\\/g, "/"),
1842
1862
  ext: extname(r.path),
1843
1863
  size: r.size
1844
1864
  }));
@@ -1847,7 +1867,7 @@ async function handleResources(action, args, config) {
1847
1867
  case "info": {
1848
1868
  const resPath = args.resource_path;
1849
1869
  if (!resPath) throw new GodotMCPError("No resource_path specified", "INVALID_ARGS", "Provide resource_path.");
1850
- const fullPath = projectPath ? resolve9(projectPath, resPath) : resolve9(resPath);
1870
+ const fullPath = projectPath ? safeResolve(projectPath, resPath) : resolve10(resPath);
1851
1871
  if (!existsSync10(fullPath))
1852
1872
  throw new GodotMCPError(`Resource not found: ${resPath}`, "RESOURCE_ERROR", "Check the file path.");
1853
1873
  const stat = statSync(fullPath);
@@ -1870,7 +1890,7 @@ async function handleResources(action, args, config) {
1870
1890
  case "delete": {
1871
1891
  const resPath = args.resource_path;
1872
1892
  if (!resPath) throw new GodotMCPError("No resource_path specified", "INVALID_ARGS", "Provide resource_path.");
1873
- const fullPath = projectPath ? resolve9(projectPath, resPath) : resolve9(resPath);
1893
+ const fullPath = projectPath ? safeResolve(projectPath, resPath) : resolve10(resPath);
1874
1894
  if (!existsSync10(fullPath))
1875
1895
  throw new GodotMCPError(`Resource not found: ${resPath}`, "RESOURCE_ERROR", "Check the file path.");
1876
1896
  unlinkSync(fullPath);
@@ -1881,7 +1901,7 @@ async function handleResources(action, args, config) {
1881
1901
  case "import_config": {
1882
1902
  const resPath = args.resource_path;
1883
1903
  if (!resPath) throw new GodotMCPError("No resource_path specified", "INVALID_ARGS", "Provide resource_path.");
1884
- const importPath = projectPath ? resolve9(projectPath, `${resPath}.import`) : resolve9(`${resPath}.import`);
1904
+ const importPath = projectPath ? safeResolve(projectPath, `${resPath}.import`) : resolve10(`${resPath}.import`);
1885
1905
  if (!existsSync10(importPath)) {
1886
1906
  return formatJSON({ path: resPath, imported: false, message: "No .import file found." });
1887
1907
  }
@@ -1900,18 +1920,9 @@ ${content}`);
1900
1920
  }
1901
1921
 
1902
1922
  // src/tools/composite/scenes.ts
1903
- import {
1904
- copyFileSync,
1905
- existsSync as existsSync11,
1906
- mkdirSync,
1907
- readdirSync as readdirSync3,
1908
- readFileSync as readFileSync11,
1909
- statSync as statSync2,
1910
- unlinkSync as unlinkSync2,
1911
- writeFileSync as writeFileSync9
1912
- } from "node:fs";
1923
+ import { copyFileSync, existsSync as existsSync11, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
1913
1924
  import { readFile as readFile3 } from "node:fs/promises";
1914
- import { basename, dirname, extname as extname2, join as join7, relative as relative2, resolve as resolve10 } from "node:path";
1925
+ import { basename, dirname, extname as extname2, join as join7, relative as relative3, resolve as resolve11 } from "node:path";
1915
1926
  async function parseTscnFile(filePath) {
1916
1927
  const content = await readFile3(filePath, "utf-8");
1917
1928
  const lines = content.split("\n");
@@ -1951,18 +1962,15 @@ async function parseTscnFile(filePath) {
1951
1962
  }
1952
1963
  return { path: filePath, rootNode, rootType, nodeCount: nodes.length, nodes, resources };
1953
1964
  }
1954
- function findSceneFiles(dir) {
1955
- const results = [];
1965
+ function findSceneFiles(dir, results = []) {
1956
1966
  try {
1957
- const entries = readdirSync3(dir);
1967
+ const entries = readdirSync3(dir, { withFileTypes: true });
1958
1968
  for (const entry of entries) {
1959
- if (entry.startsWith(".") || entry === "node_modules" || entry === "build") continue;
1960
- const fullPath = join7(dir, entry);
1961
- const stat = statSync2(fullPath);
1962
- if (stat.isDirectory()) {
1963
- results.push(...findSceneFiles(fullPath));
1964
- } else if (extname2(entry) === ".tscn") {
1965
- results.push(fullPath);
1969
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "build") continue;
1970
+ if (entry.isDirectory()) {
1971
+ findSceneFiles(join7(dir, entry.name), results);
1972
+ } else if (extname2(entry.name) === ".tscn") {
1973
+ results.push(join7(dir, entry.name));
1966
1974
  }
1967
1975
  }
1968
1976
  } catch {
@@ -1992,8 +2000,9 @@ function validateSceneArgs(action, args, config) {
1992
2000
  }
1993
2001
  return { projectPath, scenePath, newPath };
1994
2002
  }
1995
- function resolvePath(base, relative6) {
1996
- return base ? resolve10(base, relative6) : resolve10(relative6);
2003
+ function resolvePath(base, relativePath) {
2004
+ if (base) return safeResolve(base, relativePath);
2005
+ return resolve11(relativePath);
1997
2006
  }
1998
2007
  async function handleScenes(action, args, config) {
1999
2008
  const { projectPath, scenePath, newPath } = validateSceneArgs(action, args, config);
@@ -2001,7 +2010,7 @@ async function handleScenes(action, args, config) {
2001
2010
  case "create": {
2002
2011
  const rootType = args.root_type || "Node2D";
2003
2012
  const rootName = args.root_name || basename(scenePath, ".tscn");
2004
- const fullPath = resolve10(projectPath, scenePath);
2013
+ const fullPath = resolve11(projectPath, scenePath);
2005
2014
  if (existsSync11(fullPath)) {
2006
2015
  throw new GodotMCPError(
2007
2016
  `Scene already exists: ${scenePath}`,
@@ -2016,9 +2025,9 @@ async function handleScenes(action, args, config) {
2016
2025
  Root: ${rootName} (${rootType})`);
2017
2026
  }
2018
2027
  case "list": {
2019
- const resolvedPath = resolve10(projectPath);
2028
+ const resolvedPath = resolve11(projectPath);
2020
2029
  const scenes = findSceneFiles(resolvedPath);
2021
- const relativePaths = scenes.map((s) => relative2(resolvedPath, s).replace(/\\/g, "/"));
2030
+ const relativePaths = scenes.map((s) => relative3(resolvedPath, s).replace(/\\/g, "/"));
2022
2031
  return formatJSON({
2023
2032
  project: resolvedPath,
2024
2033
  count: relativePaths.length,
@@ -2059,7 +2068,7 @@ Root: ${rootName} (${rootType})`);
2059
2068
  return formatSuccess(`Duplicated: ${scenePath} -> ${newPath}`);
2060
2069
  }
2061
2070
  case "set_main": {
2062
- const configPath = join7(resolve10(projectPath), "project.godot");
2071
+ const configPath = join7(resolve11(projectPath), "project.godot");
2063
2072
  if (!existsSync11(configPath)) {
2064
2073
  throw new GodotMCPError("No project.godot found", "PROJECT_NOT_FOUND", "Verify the project path.");
2065
2074
  }
@@ -2079,26 +2088,8 @@ Root: ${rootName} (${rootType})`);
2079
2088
  }
2080
2089
 
2081
2090
  // src/tools/composite/scripts.ts
2082
- import { existsSync as existsSync12, mkdirSync as mkdirSync2, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
2091
+ import { existsSync as existsSync12, mkdirSync as mkdirSync2, readdirSync as readdirSync4, readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
2083
2092
  import { dirname as dirname2, extname as extname3, join as join8, relative as relative4, resolve as resolve12 } from "node:path";
2084
-
2085
- // src/tools/helpers/paths.ts
2086
- import { isAbsolute, relative as relative3, resolve as resolve11 } from "node:path";
2087
- function safeResolve(baseDir, targetPath) {
2088
- const resolvedBase = resolve11(baseDir);
2089
- const resolvedTarget = resolve11(resolvedBase, targetPath);
2090
- const relativePath = relative3(resolvedBase, resolvedTarget);
2091
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
2092
- throw new GodotMCPError(
2093
- `Access denied: Path '${targetPath}' resolves to '${resolvedTarget}' which is outside the project root '${resolvedBase}'.`,
2094
- "INVALID_ARGS",
2095
- "Ensure all file paths are within the project directory."
2096
- );
2097
- }
2098
- return resolvedTarget;
2099
- }
2100
-
2101
- // src/tools/composite/scripts.ts
2102
2093
  var SCRIPT_TEMPLATES = {
2103
2094
  Node: `extends Node
2104
2095
 
@@ -2192,15 +2183,14 @@ func _ready() -> void:
2192
2183
  }
2193
2184
  function findScriptFiles(dir, results = []) {
2194
2185
  try {
2195
- const entries = readdirSync4(dir);
2186
+ const entries = readdirSync4(dir, { withFileTypes: true });
2196
2187
  for (const entry of entries) {
2197
- if (entry.startsWith(".") || entry === "node_modules" || entry === "build" || entry === "addons") continue;
2198
- const fullPath = join8(dir, entry);
2199
- const stat = statSync3(fullPath);
2200
- if (stat.isDirectory()) {
2201
- findScriptFiles(fullPath, results);
2202
- } else if (extname3(entry) === ".gd") {
2203
- results.push(fullPath);
2188
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "build" || entry.name === "addons")
2189
+ continue;
2190
+ if (entry.isDirectory()) {
2191
+ findScriptFiles(join8(dir, entry.name), results);
2192
+ } else if (extname3(entry.name) === ".gd") {
2193
+ results.push(join8(dir, entry.name));
2204
2194
  }
2205
2195
  }
2206
2196
  } catch {
@@ -2443,7 +2433,7 @@ async function handleShader(action, args, config) {
2443
2433
  );
2444
2434
  const shaderType = args.shader_type || "canvas_item";
2445
2435
  const content = args.content || SHADER_TEMPLATES[shaderType] || SHADER_TEMPLATES.canvas_item;
2446
- const fullPath = projectPath ? resolve13(projectPath, shaderPath) : resolve13(shaderPath);
2436
+ const fullPath = projectPath ? safeResolve(projectPath, shaderPath) : resolve13(shaderPath);
2447
2437
  await mkdir(dirname3(fullPath), { recursive: true });
2448
2438
  try {
2449
2439
  await writeFile3(fullPath, content, { encoding: "utf-8", flag: "wx" });
@@ -2458,7 +2448,7 @@ async function handleShader(action, args, config) {
2458
2448
  case "read": {
2459
2449
  const shaderPath = args.shader_path;
2460
2450
  if (!shaderPath) throw new GodotMCPError("No shader_path specified", "INVALID_ARGS", "Provide shader_path.");
2461
- const fullPath = projectPath ? resolve13(projectPath, shaderPath) : resolve13(shaderPath);
2451
+ const fullPath = projectPath ? safeResolve(projectPath, shaderPath) : resolve13(shaderPath);
2462
2452
  try {
2463
2453
  const content = await readFile4(fullPath, "utf-8");
2464
2454
  return formatSuccess(`File: ${shaderPath}
@@ -2476,7 +2466,7 @@ ${content}`);
2476
2466
  if (!shaderPath) throw new GodotMCPError("No shader_path specified", "INVALID_ARGS", "Provide shader_path.");
2477
2467
  const content = args.content;
2478
2468
  if (!content) throw new GodotMCPError("No content specified", "INVALID_ARGS", "Provide shader content.");
2479
- const fullPath = projectPath ? resolve13(projectPath, shaderPath) : resolve13(shaderPath);
2469
+ const fullPath = projectPath ? safeResolve(projectPath, shaderPath) : resolve13(shaderPath);
2480
2470
  await mkdir(dirname3(fullPath), { recursive: true });
2481
2471
  await writeFile3(fullPath, content, "utf-8");
2482
2472
  return formatSuccess(`Written: ${shaderPath} (${content.length} chars)`);
@@ -2484,7 +2474,7 @@ ${content}`);
2484
2474
  case "get_params": {
2485
2475
  const shaderPath = args.shader_path;
2486
2476
  if (!shaderPath) throw new GodotMCPError("No shader_path specified", "INVALID_ARGS", "Provide shader_path.");
2487
- const fullPath = projectPath ? resolve13(projectPath, shaderPath) : resolve13(shaderPath);
2477
+ const fullPath = projectPath ? safeResolve(projectPath, shaderPath) : resolve13(shaderPath);
2488
2478
  try {
2489
2479
  const content = await readFile4(fullPath, "utf-8");
2490
2480
  const params = [];
@@ -2533,7 +2523,7 @@ async function handleSignals(action, args, config) {
2533
2523
  const projectPath = args.project_path || config.projectPath;
2534
2524
  const scenePath = args.scene_path;
2535
2525
  if (!scenePath) throw new GodotMCPError("No scene_path specified", "INVALID_ARGS", "Provide scene_path.");
2536
- const fullPath = projectPath ? resolve14(projectPath, scenePath) : resolve14(scenePath);
2526
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve14(scenePath);
2537
2527
  async function readScene() {
2538
2528
  try {
2539
2529
  return await readFile5(fullPath, "utf-8");
@@ -2647,7 +2637,7 @@ async function handleTilemap(action, args, config) {
2647
2637
  'Provide tileset_path (e.g., "tilesets/main.tres").'
2648
2638
  );
2649
2639
  const tileSize = args.tile_size || 16;
2650
- const fullPath = projectPath ? resolve15(projectPath, tilesetPath) : resolve15(tilesetPath);
2640
+ const fullPath = projectPath ? safeResolve(projectPath, tilesetPath) : resolve15(tilesetPath);
2651
2641
  if (existsSync14(fullPath)) {
2652
2642
  throw new GodotMCPError(`TileSet already exists: ${tilesetPath}`, "TILEMAP_ERROR", "Use a different path.");
2653
2643
  }
@@ -2669,7 +2659,7 @@ async function handleTilemap(action, args, config) {
2669
2659
  if (!tilesetPath || !texturePath) {
2670
2660
  throw new GodotMCPError("tileset_path and texture_path required", "INVALID_ARGS", "Both are required.");
2671
2661
  }
2672
- const fullPath = projectPath ? resolve15(projectPath, tilesetPath) : resolve15(tilesetPath);
2662
+ const fullPath = projectPath ? safeResolve(projectPath, tilesetPath) : resolve15(tilesetPath);
2673
2663
  if (!existsSync14(fullPath))
2674
2664
  throw new GodotMCPError(`TileSet not found: ${tilesetPath}`, "TILEMAP_ERROR", "Create the tileset first.");
2675
2665
  let content = readFileSync13(fullPath, "utf-8");
@@ -2699,7 +2689,7 @@ async function handleTilemap(action, args, config) {
2699
2689
  case "list": {
2700
2690
  const scenePath = args.scene_path;
2701
2691
  if (!scenePath) throw new GodotMCPError("No scene_path specified", "INVALID_ARGS", "Provide scene_path.");
2702
- const fullPath = projectPath ? resolve15(projectPath, scenePath) : resolve15(scenePath);
2692
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve15(scenePath);
2703
2693
  if (!existsSync14(fullPath))
2704
2694
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check the file path.");
2705
2695
  const content = readFileSync13(fullPath, "utf-8");
@@ -2743,7 +2733,7 @@ var CONTROL_TEMPLATES = {
2743
2733
  GridContainer: { columns: "2" }
2744
2734
  };
2745
2735
  function resolveScene3(projectPath, scenePath) {
2746
- const fullPath = projectPath ? resolve16(projectPath, scenePath) : resolve16(scenePath);
2736
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve16(scenePath);
2747
2737
  if (!existsSync15(fullPath))
2748
2738
  throw new GodotMCPError(`Scene not found: ${scenePath}`, "SCENE_ERROR", "Check the file path.");
2749
2739
  return fullPath;
@@ -2791,7 +2781,7 @@ ${nodeDecl}`;
2791
2781
  "INVALID_ARGS",
2792
2782
  'Provide theme_path (e.g., "themes/main.tres").'
2793
2783
  );
2794
- const fullPath = projectPath ? resolve16(projectPath, themePath) : resolve16(themePath);
2784
+ const fullPath = projectPath ? safeResolve(projectPath, themePath) : resolve16(themePath);
2795
2785
  const fontSize = args.font_size || 16;
2796
2786
  const _fontColor = args.font_color || "Color(1, 1, 1, 1)";
2797
2787
  const _bgColor = args.bg_color || "Color(0.15, 0.15, 0.15, 1)";
@@ -1 +1 @@
1
- {"version":3,"file":"animation.d.ts","sourceRoot":"","sources":["../../../../src/tools/composite/animation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAUvD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW;;;;;GAuIvG"}
1
+ {"version":3,"file":"animation.d.ts","sourceRoot":"","sources":["../../../../src/tools/composite/animation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAWvD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW;;;;;GAuIvG"}
@@ -5,8 +5,9 @@
5
5
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
6
  import { resolve } from 'node:path';
7
7
  import { formatJSON, formatSuccess, GodotMCPError } from '../helpers/errors.js';
8
+ import { safeResolve } from '../helpers/paths.js';
8
9
  function resolveScene(projectPath, scenePath) {
9
- const fullPath = projectPath ? resolve(projectPath, scenePath) : resolve(scenePath);
10
+ const fullPath = projectPath ? safeResolve(projectPath, scenePath) : resolve(scenePath);
10
11
  if (!existsSync(fullPath))
11
12
  throw new GodotMCPError(`Scene not found: ${scenePath}`, 'SCENE_ERROR', 'Check the file path.');
12
13
  return fullPath;