# 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"
```