diff --git a/CutList.Mcp/CutList.Mcp.csproj b/CutList.Mcp/CutList.Mcp.csproj
new file mode 100644
index 0000000..87ab5d8
--- /dev/null
+++ b/CutList.Mcp/CutList.Mcp.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/CutList.Mcp/CutListTools.cs b/CutList.Mcp/CutListTools.cs
new file mode 100644
index 0000000..285e176
--- /dev/null
+++ b/CutList.Mcp/CutListTools.cs
@@ -0,0 +1,388 @@
+using System.ComponentModel;
+using CutList.Core;
+using CutList.Core.Formatting;
+using CutList.Core.Nesting;
+using ModelContextProtocol.Server;
+
+namespace CutList.Mcp;
+
+///
+/// MCP tools for cut list optimization.
+///
+[McpServerToolType]
+public static class CutListTools
+{
+ ///
+ /// Creates an optimized cut list by packing parts into stock bins using a first-fit decreasing algorithm.
+ /// Returns the optimal arrangement of cuts to minimize waste.
+ ///
+ /// List of parts to cut. Each part has a name, length (in inches or architectural format like 12' 6"), and quantity.
+ /// List of available stock material. Each has a length, quantity (-1 for unlimited), and priority (lower = used first).
+ /// Blade/cutting width in inches. Default is 0.125 (1/8"). This accounts for material lost to the cut itself.
+ /// Optimized cut list showing which parts go in which bins, utilization percentages, and any parts that couldn't fit.
+ [McpServerTool(Name = "create_cutlist"), Description("Creates an optimized cut list by packing parts into stock bins to minimize waste.")]
+ public static CutListResult CreateCutList(
+ [Description("Parts to cut. Each object needs: name (string), length (string like \"12'\" or \"36\\\"\" or \"12.5\"), quantity (int)")]
+ PartInput[] parts,
+ [Description("Stock bins available. Each needs: length (string), quantity (int, use -1 for unlimited), priority (int, lower = used first, default 25)")]
+ StockBinInput[] stockBins,
+ [Description("Blade kerf/width in inches (default 0.125)")]
+ double kerf = 0.125)
+ {
+ try
+ {
+ // Convert parts to BinItems
+ var binItems = new List();
+ foreach (var part in parts)
+ {
+ double length = ParseLength(part.Length);
+ if (length <= 0)
+ {
+ return new CutListResult
+ {
+ Success = false,
+ Error = $"Invalid part length: {part.Length} for part '{part.Name}'"
+ };
+ }
+
+ for (int i = 0; i < part.Quantity; i++)
+ {
+ binItems.Add(new BinItem(part.Name, length));
+ }
+ }
+
+ // Convert stock bins to MultiBins
+ var multiBins = new List();
+ foreach (var bin in stockBins)
+ {
+ double length = ParseLength(bin.Length);
+ if (length <= 0)
+ {
+ return new CutListResult
+ {
+ Success = false,
+ Error = $"Invalid bin length: {bin.Length}"
+ };
+ }
+
+ multiBins.Add(new MultiBin(length, bin.Quantity, bin.Priority));
+ }
+
+ // Run the packing algorithm
+ var engine = new MultiBinEngine();
+ engine.SetBins(multiBins);
+ engine.Spacing = kerf;
+
+ var packResult = engine.Pack(binItems);
+
+ // Convert results
+ var resultBins = new List();
+ foreach (var bin in packResult.Bins)
+ {
+ var resultBin = new ResultBin
+ {
+ Length = FormatLength(bin.Length),
+ LengthInches = bin.Length,
+ UsedLength = FormatLength(bin.UsedLength),
+ UsedLengthInches = bin.UsedLength,
+ RemainingLength = FormatLength(bin.RemainingLength),
+ RemainingLengthInches = bin.RemainingLength,
+ Utilization = Math.Round(bin.Utilization * 100, 2),
+ Items = bin.Items.Select(item => new ResultItem
+ {
+ Name = item.Name,
+ Length = FormatLength(item.Length),
+ LengthInches = item.Length
+ }).ToList()
+ };
+ resultBins.Add(resultBin);
+ }
+
+ var unusedItems = packResult.ItemsNotUsed.Select(item => new ResultItem
+ {
+ Name = item.Name,
+ Length = FormatLength(item.Length),
+ LengthInches = item.Length
+ }).ToList();
+
+ // Calculate summary statistics
+ double totalStockUsed = packResult.Bins.Sum(b => b.Length);
+ double totalMaterialUsed = packResult.Bins.Sum(b => b.UsedLength);
+ double totalWaste = packResult.Bins.Sum(b => b.RemainingLength);
+ double overallUtilization = totalStockUsed > 0 ? (totalMaterialUsed / totalStockUsed) * 100 : 0;
+
+ return new CutListResult
+ {
+ Success = true,
+ Bins = resultBins,
+ UnusedItems = unusedItems,
+ Summary = new CutListSummary
+ {
+ TotalBinsUsed = packResult.Bins.Count,
+ TotalPartsPlaced = binItems.Count - packResult.ItemsNotUsed.Count,
+ TotalPartsNotPlaced = packResult.ItemsNotUsed.Count,
+ TotalStockLength = FormatLength(totalStockUsed),
+ TotalStockLengthInches = totalStockUsed,
+ TotalWaste = FormatLength(totalWaste),
+ TotalWasteInches = totalWaste,
+ OverallUtilization = Math.Round(overallUtilization, 2)
+ }
+ };
+ }
+ catch (Exception ex)
+ {
+ return new CutListResult
+ {
+ Success = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ ///
+ /// Parses a length string in architectural format (feet/inches/fractions) to inches.
+ ///
+ /// Length string like "12'", "6\"", "12' 6\"", "12.5", "6 1/2\"", etc.
+ /// The length in inches, or an error message if parsing fails.
+ [McpServerTool(Name = "parse_length"), Description("Parses an architectural length string (feet/inches/fractions) to decimal inches.")]
+ public static ParseLengthResult ParseLengthString(
+ [Description("Length string like \"12'\", \"6\\\"\", \"12' 6\\\"\", \"12.5\", \"6 1/2\\\"\"")]
+ string input)
+ {
+ try
+ {
+ double inches = ParseLength(input);
+ return new ParseLengthResult
+ {
+ Success = true,
+ Inches = inches,
+ Formatted = FormatLength(inches)
+ };
+ }
+ catch (Exception ex)
+ {
+ return new ParseLengthResult
+ {
+ Success = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ ///
+ /// Formats a length in inches to a human-readable string with feet and fractional inches.
+ ///
+ /// Length in inches.
+ /// Formatted string like "12' 6 1/2\"".
+ [McpServerTool(Name = "format_length"), Description("Formats a length in inches to feet and fractional inches.")]
+ public static FormatLengthResult FormatLengthString(
+ [Description("Length in inches")]
+ double inches)
+ {
+ try
+ {
+ return new FormatLengthResult
+ {
+ Success = true,
+ Formatted = FormatLength(inches),
+ Inches = inches
+ };
+ }
+ catch (Exception ex)
+ {
+ return new FormatLengthResult
+ {
+ Success = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ ///
+ /// Creates an optimized cut list and saves a formatted text report to a file.
+ ///
+ [McpServerTool(Name = "create_cutlist_report"), Description("Creates an optimized cut list and saves a formatted printable text report to a file. Returns the file path.")]
+ public static CutListReportResult CreateCutListReport(
+ [Description("Parts to cut. Each object needs: name (string), length (string like \"12'\" or \"36\\\"\" or \"12.5\"), quantity (int)")]
+ PartInput[] parts,
+ [Description("Stock bins available. Each needs: length (string), quantity (int, use -1 for unlimited), priority (int, lower = used first, default 25)")]
+ StockBinInput[] stockBins,
+ [Description("Blade kerf/width in inches (default 0.125)")]
+ double kerf = 0.125,
+ [Description("File path to save the report. If not provided, saves to a temp file.")]
+ string? filePath = null)
+ {
+ try
+ {
+ // Convert parts to BinItems
+ var binItems = new List();
+ foreach (var part in parts)
+ {
+ double length = ParseLength(part.Length);
+ if (length <= 0)
+ {
+ return new CutListReportResult
+ {
+ Success = false,
+ Error = $"Invalid part length: {part.Length} for part '{part.Name}'"
+ };
+ }
+
+ for (int i = 0; i < part.Quantity; i++)
+ {
+ binItems.Add(new BinItem(part.Name, length));
+ }
+ }
+
+ // Convert stock bins to MultiBins
+ var multiBins = new List();
+ foreach (var bin in stockBins)
+ {
+ double length = ParseLength(bin.Length);
+ if (length <= 0)
+ {
+ return new CutListReportResult
+ {
+ Success = false,
+ Error = $"Invalid bin length: {bin.Length}"
+ };
+ }
+
+ multiBins.Add(new MultiBin(length, bin.Quantity, bin.Priority));
+ }
+
+ // Run the packing algorithm
+ var engine = new MultiBinEngine();
+ engine.SetBins(multiBins);
+ engine.Spacing = kerf;
+
+ var packResult = engine.Pack(binItems);
+
+ // Determine file path
+ var outputPath = string.IsNullOrWhiteSpace(filePath)
+ ? Path.Combine(Path.GetTempPath(), $"cutlist_{DateTime.Now:yyyyMMdd_HHmmss}.txt")
+ : filePath;
+
+ // Save using BinFileSaver
+ var saver = new BinFileSaver(packResult.Bins);
+ saver.SaveBinsToFile(outputPath);
+
+ return new CutListReportResult
+ {
+ Success = true,
+ FilePath = Path.GetFullPath(outputPath),
+ TotalBins = packResult.Bins.Count,
+ TotalParts = binItems.Count - packResult.ItemsNotUsed.Count,
+ PartsNotPlaced = packResult.ItemsNotUsed.Count
+ };
+ }
+ catch (Exception ex)
+ {
+ return new CutListReportResult
+ {
+ Success = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ private static double ParseLength(string input)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ return 0;
+
+ // Try parsing as a plain number first
+ if (double.TryParse(input.Trim(), out double plainNumber))
+ return plainNumber;
+
+ // Try architectural format
+ return ArchUnits.ParseToInches(input);
+ }
+
+ private static string FormatLength(double inches)
+ {
+ return ArchUnits.FormatFromInches(inches);
+ }
+}
+
+// Input models
+public class PartInput
+{
+ public string Name { get; set; } = string.Empty;
+ public string Length { get; set; } = string.Empty;
+ public int Quantity { get; set; } = 1;
+}
+
+public class StockBinInput
+{
+ public string Length { get; set; } = string.Empty;
+ public int Quantity { get; set; } = -1; // -1 = unlimited
+ public int Priority { get; set; } = 25; // Lower = used first
+}
+
+// Output models
+public class CutListResult
+{
+ public bool Success { get; set; }
+ public string? Error { get; set; }
+ public List Bins { get; set; } = new();
+ public List UnusedItems { get; set; } = new();
+ public CutListSummary? Summary { get; set; }
+}
+
+public class ResultBin
+{
+ public string Length { get; set; } = string.Empty;
+ public double LengthInches { get; set; }
+ public string UsedLength { get; set; } = string.Empty;
+ public double UsedLengthInches { get; set; }
+ public string RemainingLength { get; set; } = string.Empty;
+ public double RemainingLengthInches { get; set; }
+ public double Utilization { get; set; }
+ public List Items { get; set; } = new();
+}
+
+public class ResultItem
+{
+ public string Name { get; set; } = string.Empty;
+ public string Length { get; set; } = string.Empty;
+ public double LengthInches { get; set; }
+}
+
+public class CutListSummary
+{
+ public int TotalBinsUsed { get; set; }
+ public int TotalPartsPlaced { get; set; }
+ public int TotalPartsNotPlaced { get; set; }
+ public string TotalStockLength { get; set; } = string.Empty;
+ public double TotalStockLengthInches { get; set; }
+ public string TotalWaste { get; set; } = string.Empty;
+ public double TotalWasteInches { get; set; }
+ public double OverallUtilization { get; set; }
+}
+
+public class ParseLengthResult
+{
+ public bool Success { get; set; }
+ public string? Error { get; set; }
+ public double Inches { get; set; }
+ public string? Formatted { get; set; }
+}
+
+public class FormatLengthResult
+{
+ public bool Success { get; set; }
+ public string? Error { get; set; }
+ public string? Formatted { get; set; }
+ public double Inches { get; set; }
+}
+
+public class CutListReportResult
+{
+ public bool Success { get; set; }
+ public string? Error { get; set; }
+ public string? FilePath { get; set; }
+ public int TotalBins { get; set; }
+ public int TotalParts { get; set; }
+ public int PartsNotPlaced { get; set; }
+}
diff --git a/CutList.Mcp/Program.cs b/CutList.Mcp/Program.cs
new file mode 100644
index 0000000..458c10b
--- /dev/null
+++ b/CutList.Mcp/Program.cs
@@ -0,0 +1,13 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using ModelContextProtocol.Server;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.Services
+ .AddMcpServer()
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly(typeof(Program).Assembly);
+
+var app = builder.Build();
+await app.RunAsync();
diff --git a/CutList.sln b/CutList.sln
index adf2323..3a9379e 100644
--- a/CutList.sln
+++ b/CutList.sln
@@ -7,20 +7,54 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList", "CutList\CutList.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Core", "CutList.Core\CutList.Core.csproj", "{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Mcp", "CutList.Mcp\CutList.Mcp.csproj", "{3B53377F-E012-42BA-82C8-322815D661B3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.Build.0 = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x64.Build.0 = Debug|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x86.Build.0 = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x64.ActiveCfg = Release|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x64.Build.0 = Release|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x86.ActiveCfg = Release|Any CPU
+ {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x86.Build.0 = Release|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x64.Build.0 = Debug|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x86.Build.0 = Debug|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x64.ActiveCfg = Release|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x64.Build.0 = Release|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x86.ActiveCfg = Release|Any CPU
+ {3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x86.Build.0 = Release|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x64.Build.0 = Debug|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x86.Build.0 = Debug|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Release|x64.ActiveCfg = Release|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Release|x64.Build.0 = Release|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Release|x86.ActiveCfg = Release|Any CPU
+ {3B53377F-E012-42BA-82C8-322815D661B3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE