33 KiB
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
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:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>OpenNest.IO</RootNamespace>
<AssemblyName>OpenNest.IO</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
<PackageReference Include="ACadSharp" Version="3.1.32" />
</ItemGroup>
</Project>
Step 3: Move files
Move all 6 files from OpenNest/IO/ to OpenNest.IO/:
DxfImporter.csDxfExporter.csNestReader.csNestWriter.csProgramReader.csExtensions.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:
<PackageReference Include="ACadSharp" Version="3.1.32" />
Add:
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
Step 5: Add to solution
dotnet sln OpenNest.sln add OpenNest.IO/OpenNest.IO.csproj
Step 6: Build and verify
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
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:
- Get the work area (plate bounds minus edge spacing).
- Collect all part bounding boxes, inflated by
PartSpacing. - Find the rightmost part edge — the strip to the right is a remnant.
- Find the topmost part edge — the strip above is a remnant.
- Filter out boxes that are too small to be useful (area < 1.0) or overlap existing parts.
/// <summary>
/// Finds rectangular remnant (empty) regions on the plate.
/// Returns strips along edges that are clear of parts.
/// </summary>
public List<Box> GetRemnants()
{
var work = WorkArea();
var results = new List<Box>();
if (Parts.Count == 0)
{
results.Add(work);
return results;
}
var obstacles = new List<Box>();
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
dotnet build OpenNest.sln
Expected: clean build.
Step 4: Commit
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
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:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>OpenNest.Mcp</RootNamespace>
<AssemblyName>OpenNest.Mcp</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
<PackageReference Include="ModelContextProtocol" Version="0.*-*" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.*" />
</ItemGroup>
</Project>
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.
using System.Collections.Generic;
namespace OpenNest.Mcp
{
public class NestSession
{
public Nest Nest { get; set; }
public List<Plate> Plates { get; } = new();
public List<Drawing> 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<Plate> AllPlates()
{
var all = new List<Plate>();
if (Nest != null)
all.AddRange(Nest.Plates);
all.AddRange(Plates);
return all;
}
public List<Drawing> AllDrawings()
{
var all = new List<Drawing>();
if (Nest != null)
all.AddRange(Nest.Drawings);
all.AddRange(Drawings);
return all;
}
}
}
Step 4: Create Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
using OpenNest.Mcp;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<NestSession>();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly(typeof(Program).Assembly);
var app = builder.Build();
await app.RunAsync();
Step 5: Add to solution and build
dotnet sln OpenNest.sln add OpenNest.Mcp/OpenNest.Mcp.csproj
dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj
Step 6: Commit
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:
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<Entity> 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
dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj
Fix any compilation issues from API mismatches (DxfImporter signature, ProgramReader usage).
Step 3: Commit
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
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
dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj
Step 3: Commit
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
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<NestItem>();
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
dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj
Step 3: Commit
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
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<Vector> 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
dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj
Step 3: Commit
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
dotnet build OpenNest.sln
Verify zero errors across all projects.
Step 2: Publish the MCP server
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:
{
"mcpServers": {
"opennest": {
"command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe",
"args": []
}
}
}
Alternatively, register at user level:
claude mcp add --transport stdio --scope user opennest -- "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe"
Step 4: Commit
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:
load_nestwithC:/Users/AJ/Desktop/N0308-008.zipget_plate_infofor plate 0 — verify 75 parts, 36x36 plateget_parts— verify part locations look reasonablefill_remnants— fill empty strips with the existing drawingcheck_overlaps— verify no collisionsget_plate_infoagain — verify increased utilization
This is a manual verification step. Fix any runtime issues discovered.
Commit any fixes:
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
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with OpenNest.IO and OpenNest.Mcp projects"