diff --git a/OpenNest.Console/OpenNest.Console.csproj b/OpenNest.Console/OpenNest.Console.csproj
new file mode 100644
index 0000000..dfb174b
--- /dev/null
+++ b/OpenNest.Console/OpenNest.Console.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net8.0-windows
+ OpenNest.Console
+ OpenNest.Console
+ $(DefineConstants);DEBUG;TRACE
+
+
+
+
+
+
+
diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs
new file mode 100644
index 0000000..75a5258
--- /dev/null
+++ b/OpenNest.Console/Program.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using OpenNest;
+using OpenNest.Geometry;
+using OpenNest.IO;
+
+// Parse arguments.
+var nestFile = (string)null;
+var drawingName = (string)null;
+var plateIndex = 0;
+var outputFile = (string)null;
+var quantity = 0;
+var spacing = (double?)null;
+var plateWidth = (double?)null;
+var plateHeight = (double?)null;
+var checkOverlaps = false;
+var noSave = false;
+var noLog = false;
+var keepParts = false;
+
+for (var i = 0; i < args.Length; i++)
+{
+ switch (args[i])
+ {
+ case "--drawing" when i + 1 < args.Length:
+ drawingName = args[++i];
+ break;
+ case "--plate" when i + 1 < args.Length:
+ plateIndex = int.Parse(args[++i]);
+ break;
+ case "--output" when i + 1 < args.Length:
+ outputFile = args[++i];
+ break;
+ case "--quantity" when i + 1 < args.Length:
+ quantity = int.Parse(args[++i]);
+ break;
+ case "--spacing" when i + 1 < args.Length:
+ spacing = double.Parse(args[++i]);
+ break;
+ case "--size" when i + 1 < args.Length:
+ var parts = args[++i].Split('x');
+ if (parts.Length == 2)
+ {
+ plateWidth = double.Parse(parts[0]);
+ plateHeight = double.Parse(parts[1]);
+ }
+ break;
+ case "--check-overlaps":
+ checkOverlaps = true;
+ break;
+ case "--no-save":
+ noSave = true;
+ break;
+ case "--no-log":
+ noLog = true;
+ break;
+ case "--keep-parts":
+ keepParts = true;
+ break;
+ case "--help":
+ case "-h":
+ PrintUsage();
+ return 0;
+ default:
+ if (!args[i].StartsWith("--") && nestFile == null)
+ nestFile = args[i];
+ break;
+ }
+}
+
+if (string.IsNullOrEmpty(nestFile) || !File.Exists(nestFile))
+{
+ PrintUsage();
+ return 1;
+}
+
+// Set up debug log file.
+StreamWriter logWriter = null;
+
+if (!noLog)
+{
+ var logDir = Path.Combine(Path.GetDirectoryName(nestFile), "test-harness-logs");
+ Directory.CreateDirectory(logDir);
+ var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
+ logWriter = new StreamWriter(logFile) { AutoFlush = true };
+ Trace.Listeners.Add(new TextWriterTraceListener(logWriter));
+ Console.WriteLine($"Debug log: {logFile}");
+}
+
+// Load nest.
+var reader = new NestReader(nestFile);
+var nest = reader.Read();
+
+if (nest.Plates.Count == 0)
+{
+ Console.Error.WriteLine("Error: nest file contains no plates");
+ return 1;
+}
+
+if (plateIndex >= nest.Plates.Count)
+{
+ Console.Error.WriteLine($"Error: plate index {plateIndex} out of range (0-{nest.Plates.Count - 1})");
+ return 1;
+}
+
+var plate = nest.Plates[plateIndex];
+
+// Apply overrides.
+if (spacing.HasValue)
+ plate.PartSpacing = spacing.Value;
+
+if (plateWidth.HasValue && plateHeight.HasValue)
+ plate.Size = new Size(plateWidth.Value, plateHeight.Value);
+
+// Find drawing.
+var drawing = drawingName != null
+ ? nest.Drawings.FirstOrDefault(d => d.Name == drawingName)
+ : nest.Drawings.FirstOrDefault();
+
+if (drawing == null)
+{
+ Console.Error.WriteLine(drawingName != null
+ ? $"Error: drawing '{drawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
+ : "Error: nest file contains no drawings");
+ return 1;
+}
+
+// Clear existing parts.
+var existingCount = plate.Parts.Count;
+
+if (!keepParts)
+ plate.Parts.Clear();
+
+Console.WriteLine($"Nest: {nest.Name}");
+Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Height:F1}), spacing={plate.PartSpacing:F2}");
+Console.WriteLine($"Drawing: {drawing.Name}");
+
+if (!keepParts)
+ Console.WriteLine($"Cleared {existingCount} existing parts");
+else
+ Console.WriteLine($"Keeping {existingCount} existing parts");
+
+Console.WriteLine("---");
+
+// Run fill.
+var sw = Stopwatch.StartNew();
+var engine = new NestEngine(plate);
+var item = new NestItem { Drawing = drawing, Quantity = quantity };
+var success = engine.Fill(item);
+sw.Stop();
+
+// Check overlaps.
+var overlapCount = 0;
+
+if (checkOverlaps && plate.Parts.Count > 0)
+{
+ List overlapPts;
+ var hasOverlaps = plate.HasOverlappingParts(out overlapPts);
+ overlapCount = overlapPts.Count;
+
+ if (hasOverlaps)
+ Console.WriteLine($"OVERLAPS DETECTED: {overlapCount} intersection points");
+ else
+ Console.WriteLine("Overlap check: PASS");
+}
+
+// Flush and close the log.
+Trace.Flush();
+logWriter?.Dispose();
+
+// Print results.
+Console.WriteLine($"Result: {(success ? "success" : "failed")}");
+Console.WriteLine($"Parts placed: {plate.Parts.Count}");
+Console.WriteLine($"Utilization: {plate.Utilization():P1}");
+Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");
+
+// Save output.
+if (!noSave)
+{
+ if (outputFile == null)
+ {
+ var dir = Path.GetDirectoryName(nestFile);
+ var name = Path.GetFileNameWithoutExtension(nestFile);
+ outputFile = Path.Combine(dir, $"{name}-result.zip");
+ }
+
+ var writer = new NestWriter(nest);
+ writer.Write(outputFile);
+ Console.WriteLine($"Saved: {outputFile}");
+}
+
+return checkOverlaps && overlapCount > 0 ? 1 : 0;
+
+void PrintUsage()
+{
+ Console.Error.WriteLine("Usage: OpenNest.Console [options]");
+ Console.Error.WriteLine();
+ Console.Error.WriteLine("Arguments:");
+ Console.Error.WriteLine(" nest-file Path to a .zip nest file");
+ Console.Error.WriteLine();
+ Console.Error.WriteLine("Options:");
+ Console.Error.WriteLine(" --drawing Drawing name to fill with (default: first drawing)");
+ Console.Error.WriteLine(" --plate Plate index to fill (default: 0)");
+ Console.Error.WriteLine(" --quantity Max parts to place (default: 0 = unlimited)");
+ Console.Error.WriteLine(" --spacing Override part spacing");
+ Console.Error.WriteLine(" --size Override plate size (e.g. 120x60)");
+ Console.Error.WriteLine(" --output Output nest file path (default: -result.zip)");
+ Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
+ Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
+ Console.Error.WriteLine(" --no-save Skip saving output file");
+ Console.Error.WriteLine(" --no-log Skip writing debug log file");
+ Console.Error.WriteLine(" -h, --help Show this help");
+}
diff --git a/OpenNest.Mcp/Tools/TestTools.cs b/OpenNest.Mcp/Tools/TestTools.cs
new file mode 100644
index 0000000..971861b
--- /dev/null
+++ b/OpenNest.Mcp/Tools/TestTools.cs
@@ -0,0 +1,85 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using ModelContextProtocol.Server;
+
+namespace OpenNest.Mcp.Tools
+{
+ [McpServerToolType]
+ public class TestTools
+ {
+ private const string SolutionRoot = @"C:\Users\AJ\Desktop\Projects\OpenNest";
+
+ private static readonly string HarnessProject = Path.Combine(
+ SolutionRoot, "OpenNest.Console", "OpenNest.Console.csproj");
+
+ [McpServerTool(Name = "test_engine")]
+ [Description("Build and run the nesting engine against a nest file. Returns fill results and a debug log file path for grepping. Use this to test engine changes without restarting the MCP server.")]
+ public string TestEngine(
+ [Description("Path to the nest .zip file")] string nestFile = @"C:\Users\AJ\Desktop\4980 A24 PT02 60x120 45pcs v2.zip",
+ [Description("Drawing name to fill with (default: first drawing)")] string drawingName = null,
+ [Description("Plate index to fill (default: 0)")] int plateIndex = 0,
+ [Description("Output nest file path (default: -result.zip)")] string outputFile = null)
+ {
+ if (!File.Exists(nestFile))
+ return $"Error: nest file not found: {nestFile}";
+
+ var processArgs = new StringBuilder();
+ processArgs.Append($"\"{nestFile}\"");
+
+ if (!string.IsNullOrEmpty(drawingName))
+ processArgs.Append($" --drawing \"{drawingName}\"");
+
+ processArgs.Append($" --plate {plateIndex}");
+
+ if (!string.IsNullOrEmpty(outputFile))
+ processArgs.Append($" --output \"{outputFile}\"");
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"run --project \"{HarnessProject}\" -- {processArgs}",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = SolutionRoot
+ };
+
+ var sb = new StringBuilder();
+
+ try
+ {
+ using var process = Process.Start(psi);
+ var stderrTask = process.StandardError.ReadToEndAsync();
+ var stdout = process.StandardOutput.ReadToEnd();
+ process.WaitForExit(120_000);
+ var stderr = stderrTask.Result;
+
+ if (!string.IsNullOrWhiteSpace(stdout))
+ sb.Append(stdout.TrimEnd());
+
+ if (!string.IsNullOrWhiteSpace(stderr))
+ {
+ sb.AppendLine();
+ sb.AppendLine();
+ sb.AppendLine("=== Errors ===");
+ sb.Append(stderr.TrimEnd());
+ }
+
+ if (process.ExitCode != 0)
+ {
+ sb.AppendLine();
+ sb.AppendLine($"Process exited with code {process.ExitCode}");
+ }
+ }
+ catch (System.Exception ex)
+ {
+ sb.AppendLine($"Error running test harness: {ex.Message}");
+ }
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/OpenNest.sln b/OpenNest.sln
index 42a267c..8e76f0c 100644
--- a/OpenNest.sln
+++ b/OpenNest.sln
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.IO", "OpenNest.IO\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Mcp", "OpenNest.Mcp\OpenNest.Mcp.csproj", "{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNest.Console\OpenNest.Console.csproj", "{58E00A25-86B5-42C7-87B5-DE4AD22381EA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -97,6 +99,18 @@ Global
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x64.Build.0 = Release|Any CPU
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x86.ActiveCfg = Release|Any CPU
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x86.Build.0 = Release|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Debug|x64.Build.0 = Debug|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Debug|x86.Build.0 = Debug|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x64.ActiveCfg = Release|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x64.Build.0 = Release|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.ActiveCfg = Release|Any CPU
+ {58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE