diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..be4d04b
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,8 @@
+{
+ "mcpServers": {
+ "opennest": {
+ "command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe",
+ "args": []
+ }
+ }
+}
diff --git a/CLAUDE.md b/CLAUDE.md
index 3ab710f..86fe8c2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,13 +14,13 @@ This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-wind
dotnet build OpenNest.sln
```
-NuGet dependency: `ACadSharp` 3.1.32 (DXF/DWG import/export), `System.Drawing.Common` 8.0.10.
+NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp).
No test projects exist in this solution.
## Architecture
-Three projects form a layered architecture:
+Five projects form a layered architecture:
### OpenNest.Core (class library)
Domain model, geometry, and CNC primitives organized into namespaces:
@@ -41,13 +41,29 @@ Nesting algorithms. `NestEngine` orchestrates filling plates with parts.
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
- `BestCombination`: Finds optimal mix of normal/rotated columns for grid fills.
-### OpenNest (WinForms WinExe, depends on Core + Engine)
+### OpenNest.IO (class library, depends on Core)
+File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
+
+- `DxfImporter`/`DxfExporter` — DXF file import/export via ACadSharp.
+- `NestReader`/`NestWriter` — custom ZIP-based nest format (XML metadata + G-code programs).
+- `ProgramReader` — G-code text parser.
+- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
+
+### OpenNest.Mcp (console app, depends on Core + Engine + IO)
+MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
+
+- **Tools/InputTools**: `load_nest`, `import_dxf`, `create_drawing` (built-in shapes or G-code).
+- **Tools/SetupTools**: `create_plate`, `clear_plate`.
+- **Tools/NestingTools**: `fill_plate`, `fill_area`, `fill_remnants`, `pack_plate`.
+- **Tools/InspectionTools**: `get_plate_info`, `get_parts`, `check_overlaps`.
+- `NestSession` — in-memory state across tool calls (current Nest, standalone plates/drawings).
+
+### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
The UI application with MDI interface.
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
- **Actions/**: User interaction modes — `ActionSelect`, `ActionAddPart`, `ActionClone`, `ActionFillArea`, `ActionZoomWindow`, `ActionSetSequence`.
-- **IO/**: `DxfImporter`/`DxfExporter` (via ACadSharp library), `NestReader`/`NestWriter` (custom ZIP-based format with XML metadata + G-code programs), `ProgramReader`.
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
## File Format
diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs
index 5e01d5b..2614922 100644
--- a/OpenNest.Core/Plate.cs
+++ b/OpenNest.Core/Plate.cs
@@ -473,5 +473,87 @@ namespace OpenNest
return pts.Count > 0;
}
+
+ ///
+ /// Finds rectangular remnant (empty) regions on the plate.
+ /// Returns strips along edges that are clear of parts.
+ ///
+ public List GetRemnants()
+ {
+ var work = WorkArea();
+ var results = new List();
+
+ if (Parts.Count == 0)
+ {
+ results.Add(work);
+ return results;
+ }
+
+ var obstacles = new List();
+ foreach (var part in Parts)
+ obstacles.Add(part.BoundingBox.Offset(PartSpacing));
+
+ // Right strip: from the rightmost part edge to the work area right edge
+ var maxRight = double.MinValue;
+ foreach (var box in obstacles)
+ {
+ if (box.Right > maxRight)
+ maxRight = box.Right;
+ }
+
+ if (maxRight < work.Right)
+ {
+ var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Height);
+ if (strip.Area() > 1.0)
+ results.Add(strip);
+ }
+
+ // Top strip: from the topmost part edge to the work area top edge
+ var maxTop = double.MinValue;
+ foreach (var box in obstacles)
+ {
+ if (box.Top > maxTop)
+ maxTop = box.Top;
+ }
+
+ if (maxTop < work.Top)
+ {
+ var strip = new Box(work.Left, maxTop, work.Width, work.Top - maxTop);
+ if (strip.Area() > 1.0)
+ results.Add(strip);
+ }
+
+ // Bottom strip: from work area bottom to the lowest part edge
+ var minBottom = double.MaxValue;
+ foreach (var box in obstacles)
+ {
+ if (box.Bottom < minBottom)
+ minBottom = box.Bottom;
+ }
+
+ if (minBottom > work.Bottom)
+ {
+ var strip = new Box(work.Left, work.Bottom, work.Width, minBottom - work.Bottom);
+ if (strip.Area() > 1.0)
+ results.Add(strip);
+ }
+
+ // Left strip: from work area left to the leftmost part edge
+ var minLeft = double.MaxValue;
+ foreach (var box in obstacles)
+ {
+ if (box.Left < minLeft)
+ minLeft = box.Left;
+ }
+
+ if (minLeft > work.Left)
+ {
+ var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Height);
+ if (strip.Area() > 1.0)
+ results.Add(strip);
+ }
+
+ return results;
+ }
}
}
diff --git a/OpenNest/IO/DxfExporter.cs b/OpenNest.IO/DxfExporter.cs
similarity index 100%
rename from OpenNest/IO/DxfExporter.cs
rename to OpenNest.IO/DxfExporter.cs
diff --git a/OpenNest/IO/DxfImporter.cs b/OpenNest.IO/DxfImporter.cs
similarity index 100%
rename from OpenNest/IO/DxfImporter.cs
rename to OpenNest.IO/DxfImporter.cs
diff --git a/OpenNest/IO/Extensions.cs b/OpenNest.IO/Extensions.cs
similarity index 100%
rename from OpenNest/IO/Extensions.cs
rename to OpenNest.IO/Extensions.cs
diff --git a/OpenNest/IO/NestReader.cs b/OpenNest.IO/NestReader.cs
similarity index 100%
rename from OpenNest/IO/NestReader.cs
rename to OpenNest.IO/NestReader.cs
diff --git a/OpenNest/IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs
similarity index 100%
rename from OpenNest/IO/NestWriter.cs
rename to OpenNest.IO/NestWriter.cs
diff --git a/OpenNest.IO/OpenNest.IO.csproj b/OpenNest.IO/OpenNest.IO.csproj
new file mode 100644
index 0000000..cf96aaf
--- /dev/null
+++ b/OpenNest.IO/OpenNest.IO.csproj
@@ -0,0 +1,11 @@
+
+
+ net8.0-windows
+ OpenNest.IO
+ OpenNest.IO
+
+
+
+
+
+
diff --git a/OpenNest/IO/ProgramReader.cs b/OpenNest.IO/ProgramReader.cs
similarity index 99%
rename from OpenNest/IO/ProgramReader.cs
rename to OpenNest.IO/ProgramReader.cs
index 1ffb031..6776d22 100644
--- a/OpenNest/IO/ProgramReader.cs
+++ b/OpenNest.IO/ProgramReader.cs
@@ -6,7 +6,7 @@ using OpenNest.Geometry;
namespace OpenNest.IO
{
- internal sealed class ProgramReader
+ public sealed class ProgramReader
{
private const int BufferSize = 200;
diff --git a/OpenNest.Mcp/NestSession.cs b/OpenNest.Mcp/NestSession.cs
new file mode 100644
index 0000000..2dbd993
--- /dev/null
+++ b/OpenNest.Mcp/NestSession.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+
+namespace OpenNest.Mcp
+{
+ public class NestSession
+ {
+ public Nest Nest { get; set; }
+ public List Plates { get; } = new();
+ public List Drawings { get; } = new();
+
+ public Plate GetPlate(int index)
+ {
+ if (Nest != null && index < Nest.Plates.Count)
+ return Nest.Plates[index];
+
+ var adjustedIndex = index - (Nest?.Plates.Count ?? 0);
+ if (adjustedIndex >= 0 && adjustedIndex < Plates.Count)
+ return Plates[adjustedIndex];
+
+ return null;
+ }
+
+ public Drawing GetDrawing(string name)
+ {
+ if (Nest != null)
+ {
+ foreach (var d in Nest.Drawings)
+ {
+ if (d.Name == name)
+ return d;
+ }
+ }
+
+ foreach (var d in Drawings)
+ {
+ if (d.Name == name)
+ return d;
+ }
+
+ return null;
+ }
+
+ public List AllPlates()
+ {
+ var all = new List();
+ if (Nest != null)
+ all.AddRange(Nest.Plates);
+ all.AddRange(Plates);
+ return all;
+ }
+
+ public List AllDrawings()
+ {
+ var all = new List();
+ if (Nest != null)
+ all.AddRange(Nest.Drawings);
+ all.AddRange(Drawings);
+ return all;
+ }
+ }
+}
diff --git a/OpenNest.Mcp/OpenNest.Mcp.csproj b/OpenNest.Mcp/OpenNest.Mcp.csproj
new file mode 100644
index 0000000..c99afd8
--- /dev/null
+++ b/OpenNest.Mcp/OpenNest.Mcp.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net8.0-windows
+ OpenNest.Mcp
+ OpenNest.Mcp
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenNest.Mcp/Program.cs b/OpenNest.Mcp/Program.cs
new file mode 100644
index 0000000..c924f9f
--- /dev/null
+++ b/OpenNest.Mcp/Program.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using ModelContextProtocol.Server;
+using OpenNest.Mcp;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.Services.AddSingleton();
+builder.Services
+ .AddMcpServer()
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly(typeof(Program).Assembly);
+
+var app = builder.Build();
+await app.RunAsync();
diff --git a/OpenNest.Mcp/Tools/InputTools.cs b/OpenNest.Mcp/Tools/InputTools.cs
new file mode 100644
index 0000000..e3f1a28
--- /dev/null
+++ b/OpenNest.Mcp/Tools/InputTools.cs
@@ -0,0 +1,214 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Text;
+using ModelContextProtocol.Server;
+using OpenNest.Converters;
+using OpenNest.Geometry;
+using OpenNest.IO;
+using CncProgram = OpenNest.CNC.Program;
+
+namespace OpenNest.Mcp.Tools
+{
+ [McpServerToolType]
+ public class InputTools
+ {
+ private readonly NestSession _session;
+
+ public InputTools(NestSession session)
+ {
+ _session = session;
+ }
+
+ [McpServerTool(Name = "load_nest")]
+ [Description("Load a .nest zip file into the session. Returns a summary of plates, parts, and drawings.")]
+ public string LoadNest([Description("Absolute path to the .nest file")] string path)
+ {
+ if (!File.Exists(path))
+ return $"Error: file not found: {path}";
+
+ var reader = new NestReader(path);
+ var nest = reader.Read();
+ _session.Nest = nest;
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Loaded nest: {nest.Name}");
+ sb.AppendLine($"Units: {nest.Units}");
+ sb.AppendLine($"Plates: {nest.Plates.Count}");
+
+ for (var i = 0; i < nest.Plates.Count; i++)
+ {
+ var plate = nest.Plates[i];
+ var work = plate.WorkArea();
+ sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Height:F1}, " +
+ $"parts={plate.Parts.Count}, " +
+ $"utilization={plate.Utilization():P1}, " +
+ $"work area={work.Width:F1} x {work.Height:F1}");
+ }
+
+ sb.AppendLine($"Drawings: {nest.Drawings.Count}");
+
+ foreach (var dwg in nest.Drawings)
+ {
+ var bbox = dwg.Program.BoundingBox();
+ sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Height:F2}, " +
+ $"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}");
+ }
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "import_dxf")]
+ [Description("Import a DXF file as a new drawing. Returns drawing name and bounding box.")]
+ public string ImportDxf(
+ [Description("Absolute path to the DXF file")] string path,
+ [Description("Name for the drawing (defaults to filename without extension)")] string name = null)
+ {
+ if (!File.Exists(path))
+ return $"Error: file not found: {path}";
+
+ var importer = new DxfImporter();
+
+ if (!importer.GetGeometry(path, out var geometry))
+ return "Error: failed to read DXF file";
+
+ if (geometry.Count == 0)
+ return "Error: no geometry found in DXF file";
+
+ var pgm = ConvertGeometry.ToProgram(geometry);
+
+ if (pgm == null)
+ return "Error: failed to convert geometry to program";
+
+ var drawingName = name ?? Path.GetFileNameWithoutExtension(path);
+ var drawing = new Drawing(drawingName, pgm);
+ _session.Drawings.Add(drawing);
+
+ var bbox = pgm.BoundingBox();
+ return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
+ }
+
+ [McpServerTool(Name = "create_drawing")]
+ [Description("Create a drawing from a built-in shape or G-code string. Shape can be: rectangle, circle, l_shape, t_shape, gcode.")]
+ public string CreateDrawing(
+ [Description("Name for the drawing")] string name,
+ [Description("Shape type: rectangle, circle, l_shape, t_shape, gcode")] string shape,
+ [Description("Width of the shape (not used for circle or gcode)")] double width = 10,
+ [Description("Height of the shape (not used for circle or gcode)")] double height = 10,
+ [Description("Radius for circle shape")] double radius = 5,
+ [Description("G-code string (only used when shape is 'gcode')")] string gcode = null)
+ {
+ CncProgram pgm;
+
+ switch (shape.ToLower())
+ {
+ case "rectangle":
+ pgm = CreateRectangle(width, height);
+ break;
+
+ case "circle":
+ pgm = CreateCircle(radius);
+ break;
+
+ case "l_shape":
+ pgm = CreateLShape(width, height);
+ break;
+
+ case "t_shape":
+ pgm = CreateTShape(width, height);
+ break;
+
+ case "gcode":
+ if (string.IsNullOrWhiteSpace(gcode))
+ return "Error: gcode parameter is required when shape is 'gcode'";
+ pgm = ParseGcode(gcode);
+ if (pgm == null)
+ return "Error: failed to parse G-code";
+ break;
+
+ default:
+ return $"Error: unknown shape '{shape}'. Use: rectangle, circle, l_shape, t_shape, gcode";
+ }
+
+ var drawing = new Drawing(name, pgm);
+ _session.Drawings.Add(drawing);
+
+ var bbox = pgm.BoundingBox();
+ return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
+ }
+
+ private static CncProgram CreateRectangle(double width, double height)
+ {
+ var entities = new List
+ {
+ new Line(0, 0, width, 0),
+ new Line(width, 0, width, height),
+ new Line(width, height, 0, height),
+ new Line(0, height, 0, 0)
+ };
+
+ return ConvertGeometry.ToProgram(entities);
+ }
+
+ private static CncProgram CreateCircle(double radius)
+ {
+ var entities = new List
+ {
+ new Circle(0, 0, radius)
+ };
+
+ return ConvertGeometry.ToProgram(entities);
+ }
+
+ private static CncProgram CreateLShape(double width, double height)
+ {
+ var hw = width / 2;
+ var hh = height / 2;
+
+ var entities = new List
+ {
+ new Line(0, 0, width, 0),
+ new Line(width, 0, width, hh),
+ new Line(width, hh, hw, hh),
+ new Line(hw, hh, hw, height),
+ new Line(hw, height, 0, height),
+ new Line(0, height, 0, 0)
+ };
+
+ return ConvertGeometry.ToProgram(entities);
+ }
+
+ private static CncProgram CreateTShape(double width, double height)
+ {
+ var stemWidth = width / 3;
+ var topHeight = height / 3;
+ var stemLeft = (width - stemWidth) / 2;
+ var stemRight = stemLeft + stemWidth;
+ var stemBottom = 0.0;
+ var stemTop = height - topHeight;
+
+ var entities = new List
+ {
+ new Line(stemLeft, stemBottom, stemRight, stemBottom),
+ new Line(stemRight, stemBottom, stemRight, stemTop),
+ new Line(stemRight, stemTop, width, stemTop),
+ new Line(width, stemTop, width, height),
+ new Line(width, height, 0, height),
+ new Line(0, height, 0, stemTop),
+ new Line(0, stemTop, stemLeft, stemTop),
+ new Line(stemLeft, stemTop, stemLeft, stemBottom)
+ };
+
+ return ConvertGeometry.ToProgram(entities);
+ }
+
+ private static CncProgram ParseGcode(string gcode)
+ {
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(gcode));
+ var reader = new ProgramReader(stream);
+ var pgm = reader.Read();
+ reader.Close();
+ return pgm;
+ }
+ }
+}
diff --git a/OpenNest.Mcp/Tools/InspectionTools.cs b/OpenNest.Mcp/Tools/InspectionTools.cs
new file mode 100644
index 0000000..50c95eb
--- /dev/null
+++ b/OpenNest.Mcp/Tools/InspectionTools.cs
@@ -0,0 +1,136 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using ModelContextProtocol.Server;
+using OpenNest.Geometry;
+using OpenNest.Math;
+
+namespace OpenNest.Mcp.Tools
+{
+ [McpServerToolType]
+ public class InspectionTools
+ {
+ private readonly NestSession _session;
+
+ public InspectionTools(NestSession session)
+ {
+ _session = session;
+ }
+
+ [McpServerTool(Name = "get_plate_info")]
+ [Description("Get detailed information about a plate including dimensions, part count, utilization, remnants, and drawing breakdown.")]
+ public string GetPlateInfo(
+ [Description("Index of the plate")] int plateIndex)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ var work = plate.WorkArea();
+ var remnants = plate.GetRemnants();
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Plate {plateIndex}:");
+ sb.AppendLine($" Size: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
+ sb.AppendLine($" Quadrant: {plate.Quadrant}");
+ sb.AppendLine($" Thickness: {plate.Thickness:F2}");
+ sb.AppendLine($" Material: {plate.Material.Name}");
+ sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
+ sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
+ sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Height:F1}");
+ sb.AppendLine($" Parts: {plate.Parts.Count}");
+ sb.AppendLine($" Utilization: {plate.Utilization():P1}");
+ sb.AppendLine($" Quantity: {plate.Quantity}");
+
+ // Drawing breakdown
+ if (plate.Parts.Count > 0)
+ {
+ sb.AppendLine(" Drawings:");
+ var groups = plate.Parts.GroupBy(p => p.BaseDrawing.Name);
+ foreach (var group in groups)
+ sb.AppendLine($" {group.Key}: {group.Count()}");
+ }
+
+ // Remnants
+ sb.AppendLine($" Remnants: {remnants.Count}");
+ for (var i = 0; i < remnants.Count; i++)
+ {
+ var r = remnants[i];
+ sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Height:F1}, area={r.Area():F1}");
+ }
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "get_parts")]
+ [Description("List placed parts on a plate with index, drawing name, location, rotation, and bounding box.")]
+ public string GetParts(
+ [Description("Index of the plate")] int plateIndex,
+ [Description("Maximum number of parts to list (default 50)")] int limit = 50)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ if (plate.Parts.Count == 0)
+ return $"Plate {plateIndex} has no parts";
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Plate {plateIndex}: {plate.Parts.Count} parts (showing up to {limit})");
+
+ var count = System.Math.Min(plate.Parts.Count, limit);
+
+ for (var i = 0; i < count; i++)
+ {
+ var part = plate.Parts[i];
+ var bbox = part.BoundingBox;
+ var rotDeg = Angle.ToDegrees(part.Rotation);
+
+ sb.AppendLine($" [{i}] {part.BaseDrawing.Name}: " +
+ $"loc=({part.Location.X:F2},{part.Location.Y:F2}), " +
+ $"rot={rotDeg:F1} deg, " +
+ $"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Height:F2})");
+ }
+
+ if (plate.Parts.Count > limit)
+ sb.AppendLine($" ... and {plate.Parts.Count - limit} more");
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "check_overlaps")]
+ [Description("Check a plate for overlapping parts. Reports collision points if any.")]
+ public string CheckOverlaps(
+ [Description("Index of the plate")] int plateIndex)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ if (plate.Parts.Count < 2)
+ return $"Plate {plateIndex}: no overlaps possible (fewer than 2 parts)";
+
+ var hasOverlaps = plate.HasOverlappingParts(out var pts);
+
+ if (!hasOverlaps)
+ return $"Plate {plateIndex}: no overlapping parts detected";
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Plate {plateIndex}: {pts.Count} collision point(s) detected!");
+
+ var limit = System.Math.Min(pts.Count, 20);
+
+ for (var i = 0; i < limit; i++)
+ {
+ var pt = pts[i];
+ sb.AppendLine($" Collision at ({pt.X:F2}, {pt.Y:F2})");
+ }
+
+ if (pts.Count > limit)
+ sb.AppendLine($" ... and {pts.Count - limit} more collision points");
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs
new file mode 100644
index 0000000..46b865d
--- /dev/null
+++ b/OpenNest.Mcp/Tools/NestingTools.cs
@@ -0,0 +1,194 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using ModelContextProtocol.Server;
+using OpenNest.Geometry;
+
+namespace OpenNest.Mcp.Tools
+{
+ [McpServerToolType]
+ public class NestingTools
+ {
+ private readonly NestSession _session;
+
+ public NestingTools(NestSession session)
+ {
+ _session = session;
+ }
+
+ [McpServerTool(Name = "fill_plate")]
+ [Description("Fill an entire plate with a single drawing. Returns parts added and utilization.")]
+ public string FillPlate(
+ [Description("Index of the plate to fill")] int plateIndex,
+ [Description("Name of the drawing to fill with")] string drawingName,
+ [Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ var drawing = _session.GetDrawing(drawingName);
+ if (drawing == null)
+ return $"Error: drawing '{drawingName}' not found";
+
+ var countBefore = plate.Parts.Count;
+ var engine = new NestEngine(plate);
+ var item = new NestItem { Drawing = drawing, Quantity = quantity };
+ var success = engine.Fill(item);
+
+ var countAfter = plate.Parts.Count;
+ var added = countAfter - countBefore;
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Fill plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}");
+ sb.AppendLine($" Parts added: {added}");
+ sb.AppendLine($" Total parts: {countAfter}");
+ sb.AppendLine($" Utilization: {plate.Utilization():P1}");
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "fill_area")]
+ [Description("Fill a specific rectangular area on a plate with a single drawing.")]
+ public string FillArea(
+ [Description("Index of the plate")] int plateIndex,
+ [Description("Name of the drawing to fill with")] string drawingName,
+ [Description("X origin of the area")] double x,
+ [Description("Y origin of the area")] double y,
+ [Description("Width of the area")] double width,
+ [Description("Height of the area")] double height,
+ [Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ var drawing = _session.GetDrawing(drawingName);
+ if (drawing == null)
+ return $"Error: drawing '{drawingName}' not found";
+
+ var countBefore = plate.Parts.Count;
+ var engine = new NestEngine(plate);
+ var item = new NestItem { Drawing = drawing, Quantity = quantity };
+ var area = new Box(x, y, width, height);
+ var success = engine.Fill(item, area);
+
+ var countAfter = plate.Parts.Count;
+ var added = countAfter - countBefore;
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Fill area ({x:F1},{y:F1} {width:F1}x{height:F1}) on plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}");
+ sb.AppendLine($" Parts added: {added}");
+ sb.AppendLine($" Total parts: {countAfter}");
+ sb.AppendLine($" Utilization: {plate.Utilization():P1}");
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "fill_remnants")]
+ [Description("Find empty remnant regions on a plate and fill each with a drawing.")]
+ public string FillRemnants(
+ [Description("Index of the plate")] int plateIndex,
+ [Description("Name of the drawing to fill with")] string drawingName,
+ [Description("Maximum quantity per remnant (0 = unlimited)")] int quantity = 0)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ var drawing = _session.GetDrawing(drawingName);
+ if (drawing == null)
+ return $"Error: drawing '{drawingName}' not found";
+
+ var remnants = plate.GetRemnants();
+
+ if (remnants.Count == 0)
+ return $"No remnant areas found on plate {plateIndex}";
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Found {remnants.Count} remnant area(s) on plate {plateIndex}");
+
+ var totalAdded = 0;
+ var engine = new NestEngine(plate);
+
+ for (var i = 0; i < remnants.Count; i++)
+ {
+ var remnant = remnants[i];
+ var countBefore = plate.Parts.Count;
+ var item = new NestItem { Drawing = drawing, Quantity = quantity };
+ var success = engine.Fill(item, remnant);
+ var added = plate.Parts.Count - countBefore;
+ totalAdded += added;
+
+ sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Height:F1}) -> {added} parts {(success ? "" : "(no fit)")}");
+ }
+
+ sb.AppendLine($"Total parts added: {totalAdded}");
+ sb.AppendLine($"Utilization: {plate.Utilization():P1}");
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "pack_plate")]
+ [Description("Pack multiple drawings onto a plate using bin-packing. Specify drawings and quantities as comma-separated lists.")]
+ public string PackPlate(
+ [Description("Index of the plate")] int plateIndex,
+ [Description("Comma-separated drawing names")] string drawingNames,
+ [Description("Comma-separated quantities for each drawing")] string quantities)
+ {
+ var plate = _session.GetPlate(plateIndex);
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ if (string.IsNullOrWhiteSpace(drawingNames))
+ return "Error: drawingNames is required";
+
+ if (string.IsNullOrWhiteSpace(quantities))
+ return "Error: quantities is required";
+
+ var names = drawingNames.Split(',').Select(n => n.Trim()).ToArray();
+ var qtyStrings = quantities.Split(',').Select(q => q.Trim()).ToArray();
+ var qtys = new int[qtyStrings.Length];
+
+ for (var i = 0; i < qtyStrings.Length; i++)
+ {
+ if (!int.TryParse(qtyStrings[i], out qtys[i]))
+ return $"Error: '{qtyStrings[i]}' is not a valid quantity";
+ }
+
+ if (names.Length != qtys.Length)
+ return $"Error: drawing names count ({names.Length}) does not match quantities count ({qtys.Length})";
+
+ var items = new List();
+
+ for (var i = 0; i < names.Length; i++)
+ {
+ var drawing = _session.GetDrawing(names[i]);
+ if (drawing == null)
+ return $"Error: drawing '{names[i]}' not found";
+
+ items.Add(new NestItem { Drawing = drawing, Quantity = qtys[i] });
+ }
+
+ var countBefore = plate.Parts.Count;
+ var engine = new NestEngine(plate);
+ var success = engine.Pack(items);
+ var countAfter = plate.Parts.Count;
+ var added = countAfter - countBefore;
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Pack plate {plateIndex}: {(success ? "success" : "failed")}");
+ sb.AppendLine($" Parts added: {added}");
+ sb.AppendLine($" Total parts: {countAfter}");
+ sb.AppendLine($" Utilization: {plate.Utilization():P1}");
+
+ // Breakdown by drawing
+ var groups = plate.Parts.GroupBy(p => p.BaseDrawing.Name);
+ foreach (var group in groups)
+ sb.AppendLine($" {group.Key}: {group.Count()}");
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/OpenNest.Mcp/Tools/SetupTools.cs b/OpenNest.Mcp/Tools/SetupTools.cs
new file mode 100644
index 0000000..d093738
--- /dev/null
+++ b/OpenNest.Mcp/Tools/SetupTools.cs
@@ -0,0 +1,69 @@
+using System.ComponentModel;
+using System.Text;
+using ModelContextProtocol.Server;
+using OpenNest.Geometry;
+
+namespace OpenNest.Mcp.Tools
+{
+ [McpServerToolType]
+ public class SetupTools
+ {
+ private readonly NestSession _session;
+
+ public SetupTools(NestSession session)
+ {
+ _session = session;
+ }
+
+ [McpServerTool(Name = "create_plate")]
+ [Description("Create a new plate with the given dimensions and spacing. Returns plate index and work area.")]
+ public string CreatePlate(
+ [Description("Plate width")] double width,
+ [Description("Plate height")] double height,
+ [Description("Spacing between parts (default 0)")] double partSpacing = 0,
+ [Description("Edge spacing on all sides (default 0)")] double edgeSpacing = 0,
+ [Description("Quadrant 1-4 (default 1). 1=TopRight, 2=TopLeft, 3=BottomLeft, 4=BottomRight")] int quadrant = 1,
+ [Description("Material name (optional)")] string material = null)
+ {
+ var plate = new Plate(width, height);
+ plate.PartSpacing = partSpacing;
+ plate.EdgeSpacing = new Spacing(edgeSpacing, edgeSpacing);
+ plate.Quadrant = quadrant;
+ plate.Quantity = 1;
+
+ if (!string.IsNullOrEmpty(material))
+ plate.Material.Name = material;
+
+ _session.Plates.Add(plate);
+
+ var allPlates = _session.AllPlates();
+ var index = allPlates.Count - 1;
+ var work = plate.WorkArea();
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
+ sb.AppendLine($" Quadrant: {plate.Quadrant}");
+ sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
+ sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
+ sb.AppendLine($" Work area: {work.Width:F1} x {work.Height:F1}");
+
+ return sb.ToString();
+ }
+
+ [McpServerTool(Name = "clear_plate")]
+ [Description("Remove all parts from a plate. Returns how many parts were removed.")]
+ public string ClearPlate(
+ [Description("Index of the plate to clear")] int plateIndex)
+ {
+ var plate = _session.GetPlate(plateIndex);
+
+ if (plate == null)
+ return $"Error: plate {plateIndex} not found";
+
+ var count = plate.Parts.Count;
+ plate.Parts.Clear();
+
+ return $"Cleared plate {plateIndex}: removed {count} parts";
+ }
+ }
+}
diff --git a/OpenNest.sln b/OpenNest.sln
index 251ebd6..42a267c 100644
--- a/OpenNest.sln
+++ b/OpenNest.sln
@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Engine", "OpenNest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Gpu", "OpenNest.Gpu\OpenNest.Gpu.csproj", "{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.IO", "OpenNest.IO\OpenNest.IO.csproj", "{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Mcp", "OpenNest.Mcp\OpenNest.Mcp.csproj", "{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +73,30 @@ Global
{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x64.Build.0 = Release|Any CPU
{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x86.ActiveCfg = Release|Any CPU
{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x86.Build.0 = Release|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x64.Build.0 = Debug|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x86.Build.0 = Debug|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x64.ActiveCfg = Release|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x64.Build.0 = Release|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x86.ActiveCfg = Release|Any CPU
+ {1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x86.Build.0 = Release|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x64.Build.0 = Debug|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x86.Build.0 = Debug|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|Any CPU.Build.0 = Release|Any CPU
+ {61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x64.ActiveCfg = Release|Any CPU
+ {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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/OpenNest/OpenNest.csproj b/OpenNest/OpenNest.csproj
index 4eafc18..b2c2b77 100644
--- a/OpenNest/OpenNest.csproj
+++ b/OpenNest/OpenNest.csproj
@@ -14,7 +14,7 @@
-
+