From 3516199f250f6077f9735494b49a33e6ff9e6e43 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 9 Mar 2026 18:33:13 -0400 Subject: [PATCH] docs: add MCP service design and implementation plan Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-03-08-mcp-service-design.md | 91 ++ docs/plans/2026-03-08-mcp-service-plan.md | 1047 +++++++++++++++++++ 2 files changed, 1138 insertions(+) create mode 100644 docs/plans/2026-03-08-mcp-service-design.md create mode 100644 docs/plans/2026-03-08-mcp-service-plan.md diff --git a/docs/plans/2026-03-08-mcp-service-design.md b/docs/plans/2026-03-08-mcp-service-design.md new file mode 100644 index 0000000..2caf7d7 --- /dev/null +++ b/docs/plans/2026-03-08-mcp-service-design.md @@ -0,0 +1,91 @@ +# OpenNest MCP Service + IO Library Refactor + +## Goal + +Create an MCP server so Claude Code can load nest files, run nesting algorithms, and inspect results — enabling rapid iteration on nesting strategies without launching the WinForms app. + +## Project Changes + +``` +OpenNest.Core (no external deps) — add Plate.GetRemnants() +OpenNest.Engine → Core +OpenNest.IO (NEW) → Core + ACadSharp — extracted from OpenNest/IO/ +OpenNest.Mcp (NEW) → Core + Engine + IO +OpenNest (WinForms) → Core + Engine + IO (drops ACadSharp direct ref) +``` + +## OpenNest.IO Library + +New class library. Move from the UI project (`OpenNest/IO/`): + +- `DxfImporter` +- `DxfExporter` +- `NestReader` +- `NestWriter` +- `ProgramReader` +- ACadSharp NuGet dependency (3.1.32) + +The WinForms project drops its direct ACadSharp reference and references OpenNest.IO instead. + +## Plate.GetRemnants() + +Add to `Plate` in Core. Simple strip-based scan: + +1. Collect all part bounding boxes inflated by `PartSpacing`. +2. Scan the work area for clear rectangular strips along edges and between part columns/rows. +3. Return `List` of usable empty regions. + +No engine dependency — uses only work area and part bounding boxes already available on Plate. + +## MCP Tools + +### Input +| Tool | Description | +|------|-------------| +| `load_nest` | Load a `.nest` zip file, returns nest summary (plates, drawings, part counts) | +| `import_dxf` | Import a DXF file as a drawing | +| `create_drawing` | Create from built-in shape primitive (rect, circle, L, T) or raw G-code string | + +### Setup +| Tool | Description | +|------|-------------| +| `create_plate` | Define a plate with dimensions, spacing, edge spacing, quadrant | +| `clear_plate` | Remove all parts from a plate | + +### Nesting +| Tool | Description | +|------|-------------| +| `fill_plate` | Fill entire plate with a single drawing (NestEngine.Fill) | +| `fill_area` | Fill a specific box region on a plate | +| `fill_remnants` | Auto-detect remnants via Plate.GetRemnants(), fill each with a drawing | +| `pack_plate` | Multi-drawing bin packing (NestEngine.Pack) | + +### Inspection +| Tool | Description | +|------|-------------| +| `get_plate_info` | Dimensions, part count, utilization %, remnant boxes | +| `get_parts` | List placed parts with location, rotation, bounding box | +| `check_overlaps` | Run overlap detection, return collision points | + +## Example Workflow + +``` +load_nest("N0308-008.zip") +→ 1 plate (36x36), 75 parts, 1 drawing (Converto 3 YRD DUMPER), utilization 80.2% + +get_plate_info(plate: 0) +→ utilization: 80.2%, remnants: [{x:33.5, y:0, w:2.5, h:36}] + +fill_remnants(plate: 0, drawing: "Converto 3 YRD DUMPER") +→ added 3 parts, new utilization: 83.1% + +check_overlaps(plate: 0) +→ no overlaps +``` + +## MCP Server Implementation + +- .NET 8 console app using stdio transport +- Published to `~/.claude/mcp/OpenNest.Mcp/` +- Registered in `~/.claude/settings.local.json` +- In-memory state: holds the current `Nest` object across tool calls diff --git a/docs/plans/2026-03-08-mcp-service-plan.md b/docs/plans/2026-03-08-mcp-service-plan.md new file mode 100644 index 0000000..4c98638 --- /dev/null +++ b/docs/plans/2026-03-08-mcp-service-plan.md @@ -0,0 +1,1047 @@ +# OpenNest MCP Service + IO Library Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Create an MCP server that allows Claude Code to load nest files, run nesting algorithms, and inspect results for rapid iteration on nesting strategies. + +**Architecture:** Extract IO classes from the WinForms project into a new `OpenNest.IO` class library, add `Plate.GetRemnants()` to Core, then build an `OpenNest.Mcp` console app that references Core + Engine + IO and exposes nesting operations as MCP tools over stdio. + +**Tech Stack:** .NET 8, ModelContextProtocol SDK, Microsoft.Extensions.Hosting, ACadSharp 3.1.32 + +--- + +### Task 1: Create the OpenNest.IO class library + +**Files:** +- Create: `OpenNest.IO/OpenNest.IO.csproj` +- Move: `OpenNest/IO/DxfImporter.cs` → `OpenNest.IO/DxfImporter.cs` +- Move: `OpenNest/IO/DxfExporter.cs` → `OpenNest.IO/DxfExporter.cs` +- Move: `OpenNest/IO/NestReader.cs` → `OpenNest.IO/NestReader.cs` +- Move: `OpenNest/IO/NestWriter.cs` → `OpenNest.IO/NestWriter.cs` +- Move: `OpenNest/IO/ProgramReader.cs` → `OpenNest.IO/ProgramReader.cs` +- Move: `OpenNest/IO/Extensions.cs` → `OpenNest.IO/Extensions.cs` +- Modify: `OpenNest/OpenNest.csproj` — replace ACadSharp ref with OpenNest.IO project ref +- Modify: `OpenNest.sln` — add OpenNest.IO project + +**Step 1: Create the IO project** + +```bash +cd C:/Users/AJ/Desktop/Projects/OpenNest +dotnet new classlib -n OpenNest.IO --framework net8.0-windows +``` + +Delete the auto-generated `Class1.cs`. + +**Step 2: Configure the csproj** + +`OpenNest.IO/OpenNest.IO.csproj`: +```xml + + + net8.0-windows + OpenNest.IO + OpenNest.IO + + + + + + +``` + +**Step 3: Move files** + +Move all 6 files from `OpenNest/IO/` to `OpenNest.IO/`: +- `DxfImporter.cs` +- `DxfExporter.cs` +- `NestReader.cs` +- `NestWriter.cs` +- `ProgramReader.cs` +- `Extensions.cs` + +These files already use `namespace OpenNest.IO` so no namespace changes needed. + +**Step 4: Update the WinForms csproj** + +In `OpenNest/OpenNest.csproj`, replace the ACadSharp PackageReference with a project reference to OpenNest.IO: + +Remove: +```xml + +``` + +Add: +```xml + +``` + +**Step 5: Add to solution** + +```bash +dotnet sln OpenNest.sln add OpenNest.IO/OpenNest.IO.csproj +``` + +**Step 6: Build and verify** + +```bash +dotnet build OpenNest.sln +``` + +Expected: clean build, zero errors. The WinForms project's `using OpenNest.IO` statements should resolve via the transitive reference. + +**Step 7: Commit** + +```bash +git add OpenNest.IO/ OpenNest/OpenNest.csproj OpenNest.sln +git add -u OpenNest/IO/ # stages the deletions +git commit -m "refactor: extract OpenNest.IO class library from WinForms project + +Move DxfImporter, DxfExporter, NestReader, NestWriter, ProgramReader, +and Extensions into a new OpenNest.IO class library. The WinForms project +now references OpenNest.IO instead of ACadSharp directly." +``` + +--- + +### Task 2: Add Plate.GetRemnants() + +**Files:** +- Modify: `OpenNest.Core/Plate.cs` + +**Step 1: Read Plate.cs to find the insertion point** + +Read the full `Plate.cs` to understand the existing structure and find the right location for the new method (after `HasOverlappingParts`). + +**Step 2: Implement GetRemnants** + +Add this method to the `Plate` class. The algorithm: +1. Get the work area (plate bounds minus edge spacing). +2. Collect all part bounding boxes, inflated by `PartSpacing`. +3. Find the rightmost part edge — the strip to the right is a remnant. +4. Find the topmost part edge — the strip above is a remnant. +5. Filter out boxes that are too small to be useful (area < 1.0) or overlap existing parts. + +```csharp +/// +/// 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; +} +``` + +**Step 3: Build and verify** + +```bash +dotnet build OpenNest.sln +``` + +Expected: clean build. + +**Step 4: Commit** + +```bash +git add OpenNest.Core/Plate.cs +git commit -m "feat: add Plate.GetRemnants() for finding empty edge strips" +``` + +--- + +### Task 3: Create the OpenNest.Mcp project scaffold + +**Files:** +- Create: `OpenNest.Mcp/OpenNest.Mcp.csproj` +- Create: `OpenNest.Mcp/Program.cs` +- Create: `OpenNest.Mcp/NestSession.cs` +- Modify: `OpenNest.sln` + +**Step 1: Create the console project** + +```bash +cd C:/Users/AJ/Desktop/Projects/OpenNest +dotnet new console -n OpenNest.Mcp --framework net8.0-windows +``` + +**Step 2: Configure the csproj** + +`OpenNest.Mcp/OpenNest.Mcp.csproj`: +```xml + + + Exe + net8.0-windows + OpenNest.Mcp + OpenNest.Mcp + + + + + + + + + +``` + +**Step 3: Create NestSession.cs** + +This holds the in-memory state across tool calls — the current `Nest` object, a list of standalone plates and drawings for synthetic tests. + +```csharp +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; + } + } +} +``` + +**Step 4: Create Program.cs** + +```csharp +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(); +``` + +**Step 5: Add to solution and build** + +```bash +dotnet sln OpenNest.sln add OpenNest.Mcp/OpenNest.Mcp.csproj +dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj +``` + +**Step 6: Commit** + +```bash +git add OpenNest.Mcp/ OpenNest.sln +git commit -m "feat: scaffold OpenNest.Mcp project with session state" +``` + +--- + +### Task 4: Implement input tools (load_nest, import_dxf, create_drawing) + +**Files:** +- Create: `OpenNest.Mcp/Tools/InputTools.cs` + +**Step 1: Create the tools file** + +`OpenNest.Mcp/Tools/InputTools.cs`: + +```csharp +using System.ComponentModel; +using System.Text; +using ModelContextProtocol.Server; +using OpenNest.CNC; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.IO; + +namespace OpenNest.Mcp.Tools +{ + public class InputTools + { + private readonly NestSession _session; + + public InputTools(NestSession session) + { + _session = session; + } + + [McpServerTool(Name = "load_nest")] + [Description("Load a .nest zip file. Returns a summary of plates, drawings, and part counts.")] + public string LoadNest( + [Description("Full path to the .nest zip file")] string path) + { + var nest = NestReader.Read(path); + _session.Nest = nest; + + var sb = new StringBuilder(); + sb.AppendLine($"Loaded: {nest.Name}"); + sb.AppendLine($"Plates: {nest.Plates.Count}"); + sb.AppendLine($"Drawings: {nest.Drawings.Count}"); + + for (var i = 0; i < nest.Plates.Count; i++) + { + var plate = nest.Plates[i]; + sb.AppendLine($" Plate {i}: {plate.Size.Width}x{plate.Size.Height}, " + + $"{plate.Parts.Count} parts, " + + $"utilization: {plate.Utilization():P1}"); + } + + for (var i = 0; i < nest.Drawings.Count; i++) + { + var dwg = nest.Drawings[i]; + var bbox = dwg.Program.BoundingBox(); + sb.AppendLine($" Drawing: \"{dwg.Name}\" ({bbox.Width:F4}x{bbox.Height:F4})"); + } + + return sb.ToString(); + } + + [McpServerTool(Name = "import_dxf")] + [Description("Import a DXF file as a drawing.")] + public string ImportDxf( + [Description("Full path to the DXF file")] string path, + [Description("Name for the drawing (defaults to filename)")] string name = null) + { + var importer = new DxfImporter(); + var geometry = importer.Import(path); + + if (geometry == null || geometry.Count == 0) + return "Error: No geometry found in DXF file."; + + var pgm = ConvertGeometry.ToProgram(geometry); + if (pgm == null) + return "Error: Could not convert DXF geometry to program."; + + var drawingName = name ?? System.IO.Path.GetFileNameWithoutExtension(path); + var drawing = new Drawing(drawingName) { Program = pgm }; + drawing.UpdateArea(); + _session.Drawings.Add(drawing); + + var bbox = pgm.BoundingBox(); + return $"Imported \"{drawingName}\": {bbox.Width:F4}x{bbox.Height:F4}, area: {drawing.Area:F4}"; + } + + [McpServerTool(Name = "create_drawing")] + [Description("Create a drawing from a built-in shape (rectangle, circle, l_shape, t_shape) or raw G-code.")] + public string CreateDrawing( + [Description("Name for the drawing")] string name, + [Description("Shape type: rectangle, circle, l_shape, t_shape, gcode")] string shape, + [Description("Width (for rectangle, l_shape, t_shape)")] double width = 0, + [Description("Height (for rectangle, l_shape, t_shape)")] double height = 0, + [Description("Radius (for circle)")] double radius = 0, + [Description("Secondary width (for l_shape: notch width, t_shape: stem width)")] double width2 = 0, + [Description("Secondary height (for l_shape: notch height, t_shape: stem height)")] double height2 = 0, + [Description("Raw G-code string (for gcode shape)")] string gcode = null) + { + Program pgm; + + switch (shape.ToLowerInvariant()) + { + case "rectangle": + pgm = BuildRectangle(width, height); + break; + case "circle": + pgm = BuildCircle(radius); + break; + case "l_shape": + pgm = BuildLShape(width, height, width2, height2); + break; + case "t_shape": + pgm = BuildTShape(width, height, width2, height2); + break; + case "gcode": + if (string.IsNullOrEmpty(gcode)) + return "Error: gcode parameter required for gcode shape."; + pgm = ProgramReader.Parse(gcode); + break; + default: + return $"Error: Unknown shape '{shape}'. Use: rectangle, circle, l_shape, t_shape, gcode."; + } + + var drawing = new Drawing(name) { Program = pgm }; + drawing.UpdateArea(); + _session.Drawings.Add(drawing); + + var bbox = pgm.BoundingBox(); + return $"Created \"{name}\": {bbox.Width:F4}x{bbox.Height:F4}, area: {drawing.Area:F4}"; + } + + private static Program BuildRectangle(double w, double h) + { + var shape = new Shape(); + shape.Entities.Add(new Line(0, 0, w, 0)); + shape.Entities.Add(new Line(w, 0, w, h)); + shape.Entities.Add(new Line(w, h, 0, h)); + shape.Entities.Add(new Line(0, h, 0, 0)); + return ConvertGeometry.ToProgram(shape); + } + + private static Program BuildCircle(double r) + { + var shape = new Shape(); + shape.Entities.Add(new Circle(0, 0, r)); + return ConvertGeometry.ToProgram(shape); + } + + private static Program BuildLShape(double w, double h, double w2, double h2) + { + // L-shape: full rectangle minus top-right notch + var shape = new Shape(); + shape.Entities.Add(new Line(0, 0, w, 0)); + shape.Entities.Add(new Line(w, 0, w, h - h2)); + shape.Entities.Add(new Line(w, h - h2, w - w2, h - h2)); + shape.Entities.Add(new Line(w - w2, h - h2, w - w2, h)); + shape.Entities.Add(new Line(w - w2, h, 0, h)); + shape.Entities.Add(new Line(0, h, 0, 0)); + return ConvertGeometry.ToProgram(shape); + } + + private static Program BuildTShape(double w, double h, double stemW, double stemH) + { + // T-shape: wide top + centered stem + var stemLeft = (w - stemW) / 2.0; + var stemRight = stemLeft + stemW; + var shape = new Shape(); + shape.Entities.Add(new Line(stemLeft, 0, stemRight, 0)); + shape.Entities.Add(new Line(stemRight, 0, stemRight, stemH)); + shape.Entities.Add(new Line(stemRight, stemH, w, stemH)); + shape.Entities.Add(new Line(w, stemH, w, h)); + shape.Entities.Add(new Line(w, h, 0, h)); + shape.Entities.Add(new Line(0, h, 0, stemH)); + shape.Entities.Add(new Line(0, stemH, stemLeft, stemH)); + shape.Entities.Add(new Line(stemLeft, stemH, stemLeft, 0)); + return ConvertGeometry.ToProgram(shape); + } + } +} +``` + +Note: `DxfImporter.Import()` may have a different signature — check the actual method. It might return `List` or take different parameters. Also check if `ProgramReader.Parse(string)` exists or if it reads from files. Adapt as needed. + +**Step 2: Build and verify** + +```bash +dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj +``` + +Fix any compilation issues from API mismatches (DxfImporter signature, ProgramReader usage). + +**Step 3: Commit** + +```bash +git add OpenNest.Mcp/Tools/InputTools.cs +git commit -m "feat(mcp): add input tools — load_nest, import_dxf, create_drawing" +``` + +--- + +### Task 5: Implement setup tools (create_plate, clear_plate) + +**Files:** +- Create: `OpenNest.Mcp/Tools/SetupTools.cs` + +**Step 1: Create the tools file** + +```csharp +using System.ComponentModel; +using ModelContextProtocol.Server; +using OpenNest.Geometry; + +namespace OpenNest.Mcp.Tools +{ + public class SetupTools + { + private readonly NestSession _session; + + public SetupTools(NestSession session) + { + _session = session; + } + + [McpServerTool(Name = "create_plate")] + [Description("Create a new plate with specified dimensions and spacing.")] + public string CreatePlate( + [Description("Plate width")] double width, + [Description("Plate height")] double height, + [Description("Spacing between parts (default 0.125)")] double partSpacing = 0.125, + [Description("Edge spacing on all sides (default 0.25)")] double edgeSpacing = 0.25, + [Description("Quadrant 1-4 (default 3 = bottom-left origin)")] int quadrant = 3) + { + var plate = new Plate(width, height) + { + PartSpacing = partSpacing, + Quadrant = quadrant, + EdgeSpacing = new Spacing(edgeSpacing) + }; + + _session.Plates.Add(plate); + var index = _session.AllPlates().Count - 1; + var work = plate.WorkArea(); + + return $"Created plate {index}: {width}x{height}, " + + $"work area: {work.Width:F4}x{work.Height:F4}, " + + $"quadrant: {quadrant}, part spacing: {partSpacing}, edge spacing: {edgeSpacing}"; + } + + [McpServerTool(Name = "clear_plate")] + [Description("Remove all parts from a plate.")] + public string ClearPlate( + [Description("Plate index (0-based)")] int plate) + { + var p = _session.GetPlate(plate); + if (p == null) + return $"Error: Plate {plate} not found."; + + var count = p.Parts.Count; + p.Parts.Clear(); + + return $"Cleared plate {plate}: removed {count} parts."; + } + } +} +``` + +Note: Check that `Spacing` has a constructor that takes a single value for all sides. If not, set `Top`, `Bottom`, `Left`, `Right` individually. + +**Step 2: Build and verify** + +```bash +dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj +``` + +**Step 3: Commit** + +```bash +git add OpenNest.Mcp/Tools/SetupTools.cs +git commit -m "feat(mcp): add setup tools — create_plate, clear_plate" +``` + +--- + +### Task 6: Implement nesting tools (fill_plate, fill_area, fill_remnants, pack_plate) + +**Files:** +- Create: `OpenNest.Mcp/Tools/NestingTools.cs` + +**Step 1: Create the tools file** + +```csharp +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using ModelContextProtocol.Server; +using OpenNest.Geometry; + +namespace OpenNest.Mcp.Tools +{ + 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 using NestEngine.Fill.")] + public string FillPlate( + [Description("Plate index (0-based)")] int plate, + [Description("Drawing name")] string drawing, + [Description("Max quantity (0 = unlimited)")] int quantity = 0) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + var d = _session.GetDrawing(drawing); + if (d == null) return $"Error: Drawing '{drawing}' not found."; + + var before = p.Parts.Count; + var engine = new NestEngine(p); + var item = new NestItem { Drawing = d, Quantity = quantity }; + var success = engine.Fill(item); + + var after = p.Parts.Count; + return $"Fill plate {plate}: added {after - before} parts " + + $"(total: {after}), utilization: {p.Utilization():P1}"; + } + + [McpServerTool(Name = "fill_area")] + [Description("Fill a specific rectangular area on a plate with a drawing.")] + public string FillArea( + [Description("Plate index (0-based)")] int plate, + [Description("Drawing name")] string drawing, + [Description("Area left X")] double x, + [Description("Area bottom Y")] double y, + [Description("Area width")] double width, + [Description("Area height")] double height, + [Description("Max quantity (0 = unlimited)")] int quantity = 0) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + var d = _session.GetDrawing(drawing); + if (d == null) return $"Error: Drawing '{drawing}' not found."; + + var before = p.Parts.Count; + var engine = new NestEngine(p); + var area = new Box(x, y, width, height); + var item = new NestItem { Drawing = d, Quantity = quantity }; + var success = engine.Fill(item, area); + + var after = p.Parts.Count; + return $"Fill area: added {after - before} parts " + + $"(total: {after}), utilization: {p.Utilization():P1}"; + } + + [McpServerTool(Name = "fill_remnants")] + [Description("Auto-detect empty remnant strips on a plate and fill each with a drawing.")] + public string FillRemnants( + [Description("Plate index (0-based)")] int plate, + [Description("Drawing name")] string drawing, + [Description("Max quantity per remnant (0 = unlimited)")] int quantity = 0) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + var d = _session.GetDrawing(drawing); + if (d == null) return $"Error: Drawing '{drawing}' not found."; + + var remnants = p.GetRemnants(); + if (remnants.Count == 0) + return "No remnants found on the plate."; + + var sb = new StringBuilder(); + sb.AppendLine($"Found {remnants.Count} remnant(s):"); + + var totalAdded = 0; + var before = p.Parts.Count; + + foreach (var remnant in remnants) + { + var partsBefore = p.Parts.Count; + var engine = new NestEngine(p); + var item = new NestItem { Drawing = d, Quantity = quantity }; + engine.Fill(item, remnant); + var added = p.Parts.Count - partsBefore; + totalAdded += added; + + sb.AppendLine($" Remnant ({remnant.X:F2},{remnant.Y:F2}) " + + $"{remnant.Width:F2}x{remnant.Height:F2}: +{added} parts"); + } + + sb.AppendLine($"Total: +{totalAdded} parts ({p.Parts.Count} total), " + + $"utilization: {p.Utilization():P1}"); + + return sb.ToString(); + } + + [McpServerTool(Name = "pack_plate")] + [Description("Pack multiple drawings onto a plate using bin-packing (PackBottomLeft).")] + public string PackPlate( + [Description("Plate index (0-based)")] int plate, + [Description("Comma-separated list of drawing names")] string drawings, + [Description("Comma-separated quantities for each drawing (default: 1 each)")] string quantities = null) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + var names = drawings.Split(','); + var qtys = quantities?.Split(','); + var items = new List(); + + for (var i = 0; i < names.Length; i++) + { + var d = _session.GetDrawing(names[i].Trim()); + if (d == null) return $"Error: Drawing '{names[i].Trim()}' not found."; + + var qty = 1; + if (qtys != null && i < qtys.Length) + int.TryParse(qtys[i].Trim(), out qty); + + items.Add(new NestItem { Drawing = d, Quantity = qty }); + } + + var before = p.Parts.Count; + var engine = new NestEngine(p); + engine.Pack(items); + + var after = p.Parts.Count; + return $"Pack plate {plate}: added {after - before} parts " + + $"(total: {after}), utilization: {p.Utilization():P1}"; + } + } +} +``` + +**Step 2: Build and verify** + +```bash +dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj +``` + +**Step 3: Commit** + +```bash +git add OpenNest.Mcp/Tools/NestingTools.cs +git commit -m "feat(mcp): add nesting tools — fill_plate, fill_area, fill_remnants, pack_plate" +``` + +--- + +### Task 7: Implement inspection tools (get_plate_info, get_parts, check_overlaps) + +**Files:** +- Create: `OpenNest.Mcp/Tools/InspectionTools.cs` + +**Step 1: Create the tools file** + +```csharp +using System.ComponentModel; +using System.Linq; +using System.Text; +using ModelContextProtocol.Server; +using OpenNest.Geometry; + +namespace OpenNest.Mcp.Tools +{ + public class InspectionTools + { + private readonly NestSession _session; + + public InspectionTools(NestSession session) + { + _session = session; + } + + [McpServerTool(Name = "get_plate_info")] + [Description("Get plate dimensions, part count, utilization, and remnant areas.")] + public string GetPlateInfo( + [Description("Plate index (0-based)")] int plate) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + var sb = new StringBuilder(); + sb.AppendLine($"Plate {plate}:"); + sb.AppendLine($" Size: {p.Size.Width}x{p.Size.Height}"); + sb.AppendLine($" Quadrant: {p.Quadrant}"); + sb.AppendLine($" Part spacing: {p.PartSpacing}"); + sb.AppendLine($" Edge spacing: T={p.EdgeSpacing.Top} B={p.EdgeSpacing.Bottom} " + + $"L={p.EdgeSpacing.Left} R={p.EdgeSpacing.Right}"); + + var work = p.WorkArea(); + sb.AppendLine($" Work area: ({work.X:F4},{work.Y:F4}) {work.Width:F4}x{work.Height:F4}"); + sb.AppendLine($" Parts: {p.Parts.Count}"); + sb.AppendLine($" Area: {p.Area():F4}"); + sb.AppendLine($" Utilization: {p.Utilization():P2}"); + + if (p.Material != null) + sb.AppendLine($" Material: {p.Material.Name}"); + + var remnants = p.GetRemnants(); + sb.AppendLine($" Remnants: {remnants.Count}"); + foreach (var r in remnants) + { + sb.AppendLine($" ({r.X:F2},{r.Y:F2}) {r.Width:F2}x{r.Height:F2} " + + $"(area: {r.Area():F2})"); + } + + // List unique drawings and their counts + var drawingCounts = p.Parts + .GroupBy(part => part.BaseDrawing.Name) + .Select(g => new { Name = g.Key, Count = g.Count() }); + + sb.AppendLine($" Drawing breakdown:"); + foreach (var dc in drawingCounts) + sb.AppendLine($" \"{dc.Name}\": {dc.Count}"); + + return sb.ToString(); + } + + [McpServerTool(Name = "get_parts")] + [Description("List all placed parts on a plate with location, rotation, and bounding box.")] + public string GetParts( + [Description("Plate index (0-based)")] int plate, + [Description("Max parts to return (default 50)")] int limit = 50) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + var sb = new StringBuilder(); + sb.AppendLine($"Plate {plate}: {p.Parts.Count} parts (showing up to {limit})"); + + var count = 0; + foreach (var part in p.Parts) + { + if (count >= limit) break; + + var bbox = part.BoundingBox; + sb.AppendLine($" [{count}] \"{part.BaseDrawing.Name}\" " + + $"loc:({part.Location.X:F4},{part.Location.Y:F4}) " + + $"rot:{OpenNest.Math.Angle.ToDegrees(part.Rotation):F1}° " + + $"bbox:({bbox.X:F4},{bbox.Y:F4} {bbox.Width:F4}x{bbox.Height:F4})"); + + count++; + } + + if (p.Parts.Count > limit) + sb.AppendLine($" ... and {p.Parts.Count - limit} more"); + + return sb.ToString(); + } + + [McpServerTool(Name = "check_overlaps")] + [Description("Run overlap detection on a plate and report any collisions.")] + public string CheckOverlaps( + [Description("Plate index (0-based)")] int plate) + { + var p = _session.GetPlate(plate); + if (p == null) return $"Error: Plate {plate} not found."; + + System.Collections.Generic.List pts; + var hasOverlaps = p.HasOverlappingParts(out pts); + + if (!hasOverlaps) + return $"Plate {plate}: No overlaps detected ({p.Parts.Count} parts)."; + + var sb = new StringBuilder(); + sb.AppendLine($"Plate {plate}: OVERLAPS DETECTED — {pts.Count} intersection point(s)"); + + var limit = System.Math.Min(pts.Count, 20); + for (var i = 0; i < limit; i++) + sb.AppendLine($" Intersection at ({pts[i].X:F4},{pts[i].Y:F4})"); + + if (pts.Count > limit) + sb.AppendLine($" ... and {pts.Count - limit} more"); + + return sb.ToString(); + } + } +} +``` + +**Step 2: Build and verify** + +```bash +dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj +``` + +**Step 3: Commit** + +```bash +git add OpenNest.Mcp/Tools/InspectionTools.cs +git commit -m "feat(mcp): add inspection tools — get_plate_info, get_parts, check_overlaps" +``` + +--- + +### Task 8: Publish and register the MCP server + +**Step 1: Build the full solution** + +```bash +dotnet build OpenNest.sln +``` + +Verify zero errors across all projects. + +**Step 2: Publish the MCP server** + +```bash +dotnet publish OpenNest.Mcp/OpenNest.Mcp.csproj -c Release -o "$USERPROFILE/.claude/mcp/OpenNest.Mcp" +``` + +**Step 3: Register with Claude Code** + +Create or update the project-level `.mcp.json` in the repo root: + +```json +{ + "mcpServers": { + "opennest": { + "command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe", + "args": [] + } + } +} +``` + +Alternatively, register at user level: + +```bash +claude mcp add --transport stdio --scope user opennest -- "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe" +``` + +**Step 4: Commit** + +```bash +git add .mcp.json OpenNest.Mcp/ +git commit -m "feat(mcp): publish OpenNest.Mcp and register in .mcp.json" +``` + +--- + +### Task 9: Smoke test with N0308-008.zip + +After restarting Claude Code, verify the MCP tools work end-to-end: + +1. `load_nest` with `C:/Users/AJ/Desktop/N0308-008.zip` +2. `get_plate_info` for plate 0 — verify 75 parts, 36x36 plate +3. `get_parts` — verify part locations look reasonable +4. `fill_remnants` — fill empty strips with the existing drawing +5. `check_overlaps` — verify no collisions +6. `get_plate_info` again — verify increased utilization + +This is a manual verification step. Fix any runtime issues discovered. + +**Commit any fixes:** + +```bash +git add -u +git commit -m "fix(mcp): address issues found during smoke testing" +``` + +--- + +### Task 10: Update CLAUDE.md and memory + +**Files:** +- Modify: `CLAUDE.md` — update architecture section to include OpenNest.IO and OpenNest.Mcp + +**Step 1: Update project description in CLAUDE.md** + +Add OpenNest.IO and OpenNest.Mcp to the architecture section. Update the dependency description. + +**Step 2: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: update CLAUDE.md with OpenNest.IO and OpenNest.Mcp projects" +```