scad-gltf 0.1.4 → 0.2.2

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/bin/scad-mcp.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import os from "os";
5
+ import { spawn, execSync } from "child_process";
4
6
  import { fileURLToPath } from "url";
5
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -11,6 +13,7 @@ import {
11
13
  import puppeteer from "puppeteer";
12
14
  import { convertScadToGltf } from "../src/convert.js";
13
15
  import { generatePrompt } from "../src/prompt.js";
16
+ import { runGodotAsync } from "../src/godot-utils.js";
14
17
 
15
18
  const __filename = fileURLToPath(import.meta.url);
16
19
  const __dirname = path.dirname(__filename);
@@ -38,7 +41,7 @@ global.fetch = async (url, options) => {
38
41
  const server = new Server(
39
42
  {
40
43
  name: "scad-mcp-server",
41
- version: "1.2.0",
44
+ version: "1.3.0",
42
45
  },
43
46
  {
44
47
  capabilities: {
@@ -62,6 +65,88 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
62
65
  type: "string",
63
66
  description: "The description of the object you want to design.",
64
67
  },
68
+ options: {
69
+ type: "object",
70
+ description:
71
+ "Optional feature toggles to customize the generated prompt syntax rules.",
72
+ properties: {
73
+ basic: {
74
+ type: "boolean",
75
+ description:
76
+ "Include rules for basic PBR attributes: metalness and roughness. (Default: true)",
77
+ },
78
+ transmission: {
79
+ type: "boolean",
80
+ description:
81
+ "Include rules for transparent/glass volumes: transmission, thickness, ior, attenuationColor, and attenuationDistance. (Default: true)",
82
+ },
83
+ clearcoat: {
84
+ type: "boolean",
85
+ description:
86
+ "Include rules for clearcoat and clearcoatRoughness. (Default: true)",
87
+ },
88
+ sheen: {
89
+ type: "boolean",
90
+ description:
91
+ "Include rules for cloth/velvet sheen: sheen, sheenColor, and sheenRoughness. (Default: true)",
92
+ },
93
+ emissive: {
94
+ type: "boolean",
95
+ description:
96
+ "Include rules for glowing materials: emissive and emissiveIntensity. (Default: true)",
97
+ },
98
+ specular: {
99
+ type: "boolean",
100
+ description:
101
+ "Include rules for specular reflections: specularColor and specularIntensity. (Default: true)",
102
+ },
103
+ iridescence: {
104
+ type: "boolean",
105
+ description:
106
+ "Include rules for thin-film interference: iridescence and iridescenceIOR. (Default: true)",
107
+ },
108
+ autoSmoothAngle: {
109
+ type: "boolean",
110
+ description:
111
+ "Include rules for smooth shading vertex normals via $asa. (Default: true)",
112
+ },
113
+ animation: {
114
+ type: "boolean",
115
+ description:
116
+ "Include rules for hierarchical node animations using armature() and bone(). (Default: true)",
117
+ },
118
+ bakeColors: {
119
+ type: "boolean",
120
+ description:
121
+ "Include rules for baking colors from high-poly onto low-poly meshes. (Default: false)",
122
+ },
123
+ bakeNormals: {
124
+ type: "boolean",
125
+ description:
126
+ "Include rules for baking tangent-space normal maps. (Default: false)",
127
+ },
128
+ bakeOrm: {
129
+ type: "boolean",
130
+ description:
131
+ "Include rules for baking Occlusion/Roughness/Metallic (ORM) textures. (Default: false)",
132
+ },
133
+ bakeUvs: {
134
+ type: "boolean",
135
+ description:
136
+ "Include rules for generating textureless UV coordinates and tangents. (Default: false)",
137
+ },
138
+ lazyUnion: {
139
+ type: "boolean",
140
+ description:
141
+ "Include rules for the lazy-union compiler optimization. (Default: false)",
142
+ },
143
+ modelName: {
144
+ type: "boolean",
145
+ description:
146
+ "Include output instructions requiring the '/* Model Name: ... */' header comment. (Default: true)",
147
+ },
148
+ },
149
+ },
65
150
  },
66
151
  required: ["description"],
67
152
  },
@@ -104,10 +189,65 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
104
189
  description:
105
190
  "The index of the animation track to play if multiple exist. Default is 0.",
106
191
  },
192
+ return_images: {
193
+ type: "boolean",
194
+ description:
195
+ "Return image renderings of the model. Set to false for text-only validation (faster). Default is true.",
196
+ },
107
197
  },
108
198
  required: ["scad_code"],
109
199
  },
110
200
  },
201
+ {
202
+ name: "test_godot_project",
203
+ description:
204
+ "Runs a specified Godot project for a short duration to detect script errors, missing resources, or runtime crashes. Returns the console output logs and a visual screenshot of the gameplay.",
205
+ inputSchema: {
206
+ type: "object",
207
+ properties: {
208
+ project_dir: {
209
+ type: "string",
210
+ description:
211
+ "The relative or absolute path to the directory containing project.godot. Use this if the project is already extracted on disk.",
212
+ },
213
+ nodejs_script: {
214
+ type: "string",
215
+ description:
216
+ "The complete self-contained Node.js script that generates the Godot project. If provided, the tool will execute this script in a temporary directory to extract the project before testing it. Use this during the generation phase to test your code before providing the final answer.",
217
+ },
218
+ run_time: {
219
+ type: "number",
220
+ description:
221
+ "Time in seconds to run the project before terminating. Default is 5.0.",
222
+ },
223
+ return_images: {
224
+ type: "boolean",
225
+ description:
226
+ "Capture a screenshot of the project. Set to false for text-only logs (faster). Default is true.",
227
+ },
228
+ },
229
+ },
230
+ },
231
+ {
232
+ name: "compile_bevy_project",
233
+ description:
234
+ "Compiles a specified Rust Bevy project to detect syntax errors, missing dependencies, or compilation failures. Returns the cargo console output logs.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ project_dir: {
239
+ type: "string",
240
+ description:
241
+ "The relative or absolute path to the directory containing Cargo.toml. Use this if the project is already extracted on disk.",
242
+ },
243
+ nodejs_script: {
244
+ type: "string",
245
+ description:
246
+ "The complete self-contained Node.js script that generates the Bevy project. If provided, the tool will execute this script in a temporary directory to extract the project before compiling it. Use this during the generation phase to test your code before providing the final answer.",
247
+ },
248
+ },
249
+ },
250
+ },
111
251
  ],
112
252
  };
113
253
  });
@@ -121,7 +261,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
121
261
  // ------------------------------------------
122
262
  if (name === "get_scad_prompt") {
123
263
  try {
124
- const promptText = generatePrompt(args.description, {});
264
+ const promptText = generatePrompt(args.description, args.options || {});
125
265
  return {
126
266
  content: [
127
267
  {
@@ -153,12 +293,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
153
293
  : ["front", "top", "isometric"];
154
294
  const animTime = args.animation_time || 0.0;
155
295
  const animIndex = args.animation_index || 0;
296
+ const returnImages = args.return_images !== false;
156
297
 
157
298
  // 1. Convert SCAD to GLB ArrayBuffer
158
299
  const glbDataArray = await convertScadToGltf(scadCode, {
159
300
  wasmUrl: `file://${wasmPath}`,
160
301
  });
161
302
 
303
+ if (!returnImages) {
304
+ return {
305
+ content: [
306
+ {
307
+ type: "text",
308
+ text: `Successfully compiled SCAD without syntax errors. (Visual rendering skipped because return_images is false).`,
309
+ },
310
+ ],
311
+ };
312
+ }
313
+
162
314
  // Convert Uint8Array to base64
163
315
  const glbBase64 = Buffer.from(glbDataArray).toString("base64");
164
316
 
@@ -492,6 +644,493 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
492
644
  }
493
645
  }
494
646
 
647
+ // ------------------------------------------
648
+ // TOOL 3: test_godot_project
649
+ // ------------------------------------------
650
+ if (name === "test_godot_project") {
651
+ let cleanupDir = null;
652
+ try {
653
+ let projectDir = null;
654
+ const runTime = args.run_time || 5.0;
655
+
656
+ if (args.nodejs_script) {
657
+ // Create a unique temporary directory
658
+ const tempBase = fs.mkdtempSync(
659
+ path.join(os.tmpdir(), "scad-godot-test-"),
660
+ );
661
+ cleanupDir = tempBase;
662
+
663
+ const scriptPath = path.join(tempBase, "generate.js");
664
+ fs.writeFileSync(scriptPath, args.nodejs_script, "utf-8");
665
+
666
+ try {
667
+ execSync(`node generate.js`, {
668
+ cwd: tempBase,
669
+ stdio: "pipe",
670
+ timeout: 15000, // Time out after 15 seconds if it hangs
671
+ });
672
+ } catch (err) {
673
+ const stderr = err.stderr ? err.stderr.toString() : "";
674
+ const stdout = err.stdout ? err.stdout.toString() : "";
675
+ return {
676
+ content: [
677
+ {
678
+ type: "text",
679
+ text: `Error: The Node.js script failed to execute properly.\n\nSTDERR:\n${stderr}\n\nSTDOUT:\n${stdout}\n\nException: ${err.message}`,
680
+ },
681
+ ],
682
+ isError: true,
683
+ };
684
+ }
685
+
686
+ // Find the generated folder containing project.godot
687
+ const items = fs.readdirSync(tempBase);
688
+ for (const item of items) {
689
+ const itemPath = path.join(tempBase, item);
690
+ if (
691
+ fs.statSync(itemPath).isDirectory() &&
692
+ fs.existsSync(path.join(itemPath, "project.godot"))
693
+ ) {
694
+ projectDir = itemPath;
695
+ break;
696
+ }
697
+ }
698
+
699
+ if (!projectDir) {
700
+ return {
701
+ content: [
702
+ {
703
+ type: "text",
704
+ text: `Error: The Node.js script finished running, but NO Godot project was found. A valid Godot project requires a 'project.godot' file. Ensure your script creates a root project directory and places 'project.godot' inside it.`,
705
+ },
706
+ ],
707
+ isError: true,
708
+ };
709
+ }
710
+ } else if (args.project_dir) {
711
+ projectDir = path.resolve(process.cwd(), args.project_dir);
712
+ } else {
713
+ return {
714
+ content: [
715
+ {
716
+ type: "text",
717
+ text: `Error: Invalid arguments. You must provide either 'project_dir' or 'nodejs_script'.`,
718
+ },
719
+ ],
720
+ isError: true,
721
+ };
722
+ }
723
+
724
+ if (!fs.existsSync(path.join(projectDir, "project.godot"))) {
725
+ return {
726
+ content: [
727
+ {
728
+ type: "text",
729
+ text: `Error: The specified directory is not a valid Godot project. 'project.godot' was not found in: ${projectDir}`,
730
+ },
731
+ ],
732
+ isError: true,
733
+ };
734
+ }
735
+
736
+ const returnImages = args.return_images !== false;
737
+
738
+ // 1. Headless Editor Import Step (Wait for Godot to import .scad and other assets)
739
+ const importResult = await runGodotAsync(
740
+ ["--headless", "--editor", "--quit", "--path", projectDir],
741
+ projectDir,
742
+ 30000,
743
+ );
744
+
745
+ const projectGodotPath = path.join(projectDir, "project.godot");
746
+ const originalProjectGodot = fs.readFileSync(projectGodotPath, "utf8");
747
+ let playResult;
748
+
749
+ if (returnImages) {
750
+ // 1.5 Prepare Screenshot Autoload Script
751
+ const snapTime = Math.max(0.5, runTime - 0.5);
752
+
753
+ const screenshotScript = `extends Node
754
+ func _ready():
755
+ \tprint("[ScreenshotMCP] Autoload ready. Waiting ${snapTime} seconds...")
756
+ \tawait get_tree().create_timer(${snapTime}).timeout
757
+ \tvar ds = DisplayServer.get_name()
758
+ \tprint("[ScreenshotMCP] DisplayServer is: ", ds)
759
+ \tif ds != "headless":
760
+ \t\tawait RenderingServer.frame_post_draw
761
+ \t\tvar tex = get_viewport().get_texture()
762
+ \t\tif tex != null:
763
+ \t\t\tvar img = tex.get_image()
764
+ \t\t\tif img != null and not img.is_empty():
765
+ \t\t\t\tvar w = img.get_width()
766
+ \t\t\t\tvar h = img.get_height()
767
+ \t\t\t\tvar size = mini(w, h)
768
+ \t\t\t\tvar x = (w - size) / 2
769
+ \t\t\t\tvar y = (h - size) / 2
770
+ \t\t\t\tvar cropped = img.get_region(Rect2i(x, y, size, size))
771
+ \t\t\t\tvar path = ProjectSettings.globalize_path("res://screenshot_mcp.png")
772
+ \t\t\t\tvar err = cropped.save_png(path)
773
+ \t\t\t\tprint("[ScreenshotMCP] Save PNG to ", path, " returned error code: ", err)
774
+ \t\t\telse:
775
+ \t\t\t\tprint("[ScreenshotMCP] Error: Image is null or empty.")
776
+ \t\telse:
777
+ \t\t\tprint("[ScreenshotMCP] Error: Viewport texture is null.")
778
+ \tget_tree().quit()
779
+ `;
780
+ fs.writeFileSync(
781
+ path.join(projectDir, "screenshot_mcp.gd"),
782
+ screenshotScript,
783
+ );
784
+
785
+ let tempProjectGodot = originalProjectGodot;
786
+ if (tempProjectGodot.includes("[autoload]")) {
787
+ tempProjectGodot = tempProjectGodot.replace(
788
+ "[autoload]",
789
+ '[autoload]\nScreenshotMCP="*res://screenshot_mcp.gd"',
790
+ );
791
+ } else {
792
+ tempProjectGodot += `\n[autoload]\nScreenshotMCP="*res://screenshot_mcp.gd"\n`;
793
+ }
794
+ fs.writeFileSync(projectGodotPath, tempProjectGodot, "utf8");
795
+
796
+ // 2. Play Game Step
797
+ // Always try windowed first to get the screenshot.
798
+ // If the environment lacks a display server, we'll catch the error and fallback.
799
+ let playArgs = [
800
+ "--windowed", // Use windowed instead of headless to ensure rendering works for screenshot
801
+ "--resolution",
802
+ "512x512",
803
+ "--audio-driver",
804
+ "Dummy",
805
+ "--path",
806
+ projectDir,
807
+ ];
808
+
809
+ playResult = await runGodotAsync(
810
+ playArgs,
811
+ projectDir,
812
+ // Give Godot extra buffer time to boot, take the shot, and quit gracefully
813
+ runTime * 1000 + 5000,
814
+ );
815
+
816
+ let windowedOutput = playResult.output;
817
+
818
+ // Fallback if windowed mode fails due to display server issues
819
+ if (
820
+ playResult.code !== 0 &&
821
+ (playResult.output.includes("Unable to create DisplayServer") ||
822
+ playResult.output.includes("Display driver"))
823
+ ) {
824
+ playResult = await runGodotAsync(
825
+ ["--headless", "--audio-driver", "Dummy", "--path", projectDir],
826
+ projectDir,
827
+ runTime * 1000,
828
+ );
829
+ // Explicitly inject the display error into the final output so we aren't flying blind!
830
+ playResult.output =
831
+ "--- WINDOWED LAUNCH FAILED (Display Error) ---\n" +
832
+ windowedOutput.trim() +
833
+ "\n\n--- FALLBACK HEADLESS LAUNCH ---\n" +
834
+ playResult.output;
835
+ }
836
+
837
+ // 3. Cleanup Autoloads
838
+ fs.writeFileSync(projectGodotPath, originalProjectGodot, "utf8");
839
+ if (fs.existsSync(path.join(projectDir, "screenshot_mcp.gd"))) {
840
+ fs.unlinkSync(path.join(projectDir, "screenshot_mcp.gd"));
841
+ }
842
+ } else {
843
+ // Text-only mode: Run headless immediately
844
+ playResult = await runGodotAsync(
845
+ ["--headless", "--audio-driver", "Dummy", "--path", projectDir],
846
+ projectDir,
847
+ runTime * 1000,
848
+ );
849
+ }
850
+
851
+ // 4. Read Screenshot
852
+ let screenshotBase64 = null;
853
+ if (returnImages) {
854
+ const screenshotPath = path.join(projectDir, "screenshot_mcp.png");
855
+ if (fs.existsSync(screenshotPath)) {
856
+ screenshotBase64 = fs.readFileSync(screenshotPath).toString("base64");
857
+ fs.unlinkSync(screenshotPath); // Remove the image after reading
858
+ }
859
+ }
860
+
861
+ // Format Text Output
862
+ const fullOutput =
863
+ "--- IMPORT PHASE ---\n" +
864
+ importResult.output +
865
+ "\n\n--- PLAY PHASE ---\n" +
866
+ playResult.output;
867
+
868
+ const errorLines = fullOutput
869
+ .split("\n")
870
+ .filter(
871
+ (line) =>
872
+ line.includes("ERROR:") ||
873
+ line.includes("SCRIPT ERROR:") ||
874
+ line.includes("Parse Error:"),
875
+ );
876
+
877
+ let responseText = `Godot Import Exit Code: ${importResult.code}\nGodot Play Exit Code: ${playResult.code}\n\n`;
878
+
879
+ if (errorLines.length > 0) {
880
+ responseText +=
881
+ "🚨 Errors detected in Godot output:\n" +
882
+ errorLines.join("\n") +
883
+ "\n\n";
884
+ } else {
885
+ responseText +=
886
+ "✅ No obvious errors detected in the Godot output log.\n\n";
887
+ }
888
+
889
+ const maxOutputLen = 4000;
890
+ let truncatedOutput = fullOutput;
891
+ if (fullOutput.length > maxOutputLen) {
892
+ truncatedOutput = fullOutput.substring(
893
+ fullOutput.length - maxOutputLen,
894
+ );
895
+ responseText += `...[truncated]...\n`;
896
+ }
897
+ responseText += truncatedOutput;
898
+
899
+ // 5. Construct Final MCP Response
900
+ const responseContent = [
901
+ {
902
+ type: "text",
903
+ text: responseText,
904
+ },
905
+ ];
906
+
907
+ // If screenshot was captured successfully, attach it
908
+ if (returnImages) {
909
+ if (screenshotBase64) {
910
+ responseContent.push({
911
+ type: "image",
912
+ data: screenshotBase64,
913
+ mimeType: "image/png",
914
+ });
915
+ } else {
916
+ responseContent.push({
917
+ type: "text",
918
+ text: "⚠️ A visual screenshot could not be captured (Godot may have crashed immediately or rendering failed).",
919
+ });
920
+ }
921
+ }
922
+
923
+ return {
924
+ content: responseContent,
925
+ };
926
+ } catch (error) {
927
+ return {
928
+ content: [
929
+ {
930
+ type: "text",
931
+ text: `Error testing Godot project: ${error.message}`,
932
+ },
933
+ ],
934
+ isError: true,
935
+ };
936
+ } finally {
937
+ // Clean up temporary directory if we created one
938
+ if (cleanupDir && fs.existsSync(cleanupDir)) {
939
+ try {
940
+ fs.rmSync(cleanupDir, { recursive: true, force: true });
941
+ } catch (e) {
942
+ console.error(`Failed to clean up temp dir: ${cleanupDir}`, e);
943
+ }
944
+ }
945
+ }
946
+ }
947
+
948
+ // ------------------------------------------
949
+ // TOOL 4: compile_bevy_project
950
+ // ------------------------------------------
951
+ if (name === "compile_bevy_project") {
952
+ let cleanupDir = null;
953
+ try {
954
+ let projectDir = null;
955
+
956
+ if (args.nodejs_script) {
957
+ // Create a unique temporary directory
958
+ const tempBase = fs.mkdtempSync(
959
+ path.join(os.tmpdir(), "scad-bevy-compile-"),
960
+ );
961
+ cleanupDir = tempBase;
962
+
963
+ const scriptPath = path.join(tempBase, "generate.js");
964
+ fs.writeFileSync(scriptPath, args.nodejs_script, "utf-8");
965
+
966
+ try {
967
+ execSync(`node generate.js`, {
968
+ cwd: tempBase,
969
+ stdio: "pipe",
970
+ timeout: 15000, // Time out after 15 seconds if it hangs
971
+ });
972
+ } catch (err) {
973
+ const stderr = err.stderr ? err.stderr.toString() : "";
974
+ const stdout = err.stdout ? err.stdout.toString() : "";
975
+ return {
976
+ content: [
977
+ {
978
+ type: "text",
979
+ text: `Error: The Node.js script failed to execute properly.\n\nSTDERR:\n${stderr}\n\nSTDOUT:\n${stdout}\n\nException: ${err.message}`,
980
+ },
981
+ ],
982
+ isError: true,
983
+ };
984
+ }
985
+
986
+ // Find the generated folder containing Cargo.toml
987
+ const items = fs.readdirSync(tempBase);
988
+ for (const item of items) {
989
+ const itemPath = path.join(tempBase, item);
990
+ if (
991
+ fs.statSync(itemPath).isDirectory() &&
992
+ fs.existsSync(path.join(itemPath, "Cargo.toml"))
993
+ ) {
994
+ projectDir = itemPath;
995
+ break;
996
+ }
997
+ }
998
+
999
+ if (!projectDir) {
1000
+ return {
1001
+ content: [
1002
+ {
1003
+ type: "text",
1004
+ text: `Error: The Node.js script finished running, but NO Bevy project was found. A valid Rust Bevy project requires a 'Cargo.toml' file. Ensure your script creates a root project directory and places 'Cargo.toml' inside it.`,
1005
+ },
1006
+ ],
1007
+ isError: true,
1008
+ };
1009
+ }
1010
+ } else if (args.project_dir) {
1011
+ projectDir = path.resolve(process.cwd(), args.project_dir);
1012
+ } else {
1013
+ return {
1014
+ content: [
1015
+ {
1016
+ type: "text",
1017
+ text: `Error: Invalid arguments. You must provide either 'project_dir' or 'nodejs_script'.`,
1018
+ },
1019
+ ],
1020
+ isError: true,
1021
+ };
1022
+ }
1023
+
1024
+ if (!fs.existsSync(path.join(projectDir, "Cargo.toml"))) {
1025
+ return {
1026
+ content: [
1027
+ {
1028
+ type: "text",
1029
+ text: `Error: The specified directory is not a valid Rust Bevy project. 'Cargo.toml' was not found in: ${projectDir}`,
1030
+ },
1031
+ ],
1032
+ isError: true,
1033
+ };
1034
+ }
1035
+
1036
+ const runCargoAsync = () =>
1037
+ new Promise((resolve) => {
1038
+ const cargoCmd = process.platform === "win32" ? "cargo.exe" : "cargo";
1039
+ const child = spawn(cargoCmd, ["check"], {
1040
+ cwd: projectDir,
1041
+ });
1042
+
1043
+ let output = "";
1044
+ if (child.stdout)
1045
+ child.stdout.on("data", (data) => (output += data.toString()));
1046
+ if (child.stderr)
1047
+ child.stderr.on("data", (data) => (output += data.toString()));
1048
+
1049
+ child.on("error", (error) => {
1050
+ output += `\nError launching cargo: ${error.message}`;
1051
+ resolve({ code: 1, output });
1052
+ });
1053
+
1054
+ child.on("close", (code) => {
1055
+ resolve({ code, output });
1056
+ });
1057
+
1058
+ // 2-minute timeout for Bevy compilation (can be slow as dependencies download)
1059
+ setTimeout(() => {
1060
+ try {
1061
+ child.kill();
1062
+ } catch (e) {}
1063
+ output += "\n--- COMPILATION TIMED OUT (2 minutes) ---";
1064
+ resolve({ code: 1, output });
1065
+ }, 120000);
1066
+ });
1067
+
1068
+ const compileResult = await runCargoAsync();
1069
+ const compileOutput = compileResult.output;
1070
+ const compileCode = compileResult.code;
1071
+
1072
+ const fullOutput = "--- CARGO CHECK ---\n" + compileOutput.trim();
1073
+
1074
+ const errorLines = fullOutput
1075
+ .split("\n")
1076
+ .filter(
1077
+ (line) =>
1078
+ line.includes("error:") ||
1079
+ line.includes("error[E") ||
1080
+ line.includes("could not compile"),
1081
+ );
1082
+
1083
+ let responseText = `Cargo Check Exit Code: ${compileCode}\n\n`;
1084
+
1085
+ if (compileCode !== 0 || errorLines.length > 0) {
1086
+ responseText +=
1087
+ "🚨 Errors detected in Cargo output:\n" +
1088
+ errorLines.join("\n") +
1089
+ "\n\n";
1090
+ } else {
1091
+ responseText +=
1092
+ "✅ No obvious errors detected. Project checked successfully.\n\n";
1093
+ }
1094
+
1095
+ const maxOutputLen = 4000;
1096
+ let truncatedOutput = fullOutput;
1097
+ if (fullOutput.length > maxOutputLen) {
1098
+ truncatedOutput = fullOutput.substring(
1099
+ fullOutput.length - maxOutputLen,
1100
+ );
1101
+ responseText += `...[truncated]...\n`;
1102
+ }
1103
+ responseText += truncatedOutput;
1104
+
1105
+ return {
1106
+ content: [
1107
+ {
1108
+ type: "text",
1109
+ text: responseText,
1110
+ },
1111
+ ],
1112
+ };
1113
+ } catch (error) {
1114
+ return {
1115
+ content: [
1116
+ {
1117
+ type: "text",
1118
+ text: `Error compiling Bevy project: ${error.message}`,
1119
+ },
1120
+ ],
1121
+ isError: true,
1122
+ };
1123
+ } finally {
1124
+ if (cleanupDir && fs.existsSync(cleanupDir)) {
1125
+ try {
1126
+ fs.rmSync(cleanupDir, { recursive: true, force: true });
1127
+ } catch (e) {
1128
+ console.error(`Failed to clean up temp dir: ${cleanupDir}`, e);
1129
+ }
1130
+ }
1131
+ }
1132
+ }
1133
+
495
1134
  throw new Error(`Tool not found: ${name}`);
496
1135
  });
497
1136