diff --git a/CutList.Mcp/ApiClient.cs b/CutList.Mcp/ApiClient.cs
new file mode 100644
index 0000000..44419d9
--- /dev/null
+++ b/CutList.Mcp/ApiClient.cs
@@ -0,0 +1,214 @@
+using System.Net.Http.Json;
+
+namespace CutList.Mcp;
+
+///
+/// Typed HTTP client for calling the CutList.Web REST API.
+///
+public class ApiClient
+{
+ private readonly HttpClient _http;
+
+ public ApiClient(HttpClient http)
+ {
+ _http = http;
+ }
+
+ #region Suppliers
+
+ public async Task> GetSuppliersAsync(bool includeInactive = false)
+ {
+ var url = $"api/suppliers?includeInactive={includeInactive}";
+ return await _http.GetFromJsonAsync>(url) ?? [];
+ }
+
+ public async Task CreateSupplierAsync(string name, string? contactInfo, string? notes)
+ {
+ var response = await _http.PostAsJsonAsync("api/suppliers", new { Name = name, ContactInfo = contactInfo, Notes = notes });
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadFromJsonAsync();
+ }
+
+ #endregion
+
+ #region Materials
+
+ public async Task> GetMaterialsAsync(string? shape = null, bool includeInactive = false)
+ {
+ var url = $"api/materials?includeInactive={includeInactive}";
+ if (!string.IsNullOrEmpty(shape))
+ url += $"&shape={Uri.EscapeDataString(shape)}";
+ return await _http.GetFromJsonAsync>(url) ?? [];
+ }
+
+ public async Task CreateMaterialAsync(string shape, string? size, string? description,
+ string? type, string? grade, Dictionary? dimensions)
+ {
+ var body = new
+ {
+ Shape = shape,
+ Size = size,
+ Description = description,
+ Type = type,
+ Grade = grade,
+ Dimensions = dimensions
+ };
+ var response = await _http.PostAsJsonAsync("api/materials", body);
+ if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ throw new ApiConflictException(error);
+ }
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadFromJsonAsync();
+ }
+
+ public async Task> SearchMaterialsAsync(string shape, decimal targetValue, decimal tolerance)
+ {
+ var response = await _http.PostAsJsonAsync("api/materials/search", new
+ {
+ Shape = shape,
+ TargetValue = targetValue,
+ Tolerance = tolerance
+ });
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadFromJsonAsync>() ?? [];
+ }
+
+ #endregion
+
+ #region Stock Items
+
+ public async Task> GetStockItemsAsync(int? materialId = null, bool includeInactive = false)
+ {
+ var url = $"api/stock-items?includeInactive={includeInactive}";
+ if (materialId.HasValue)
+ url += $"&materialId={materialId.Value}";
+ return await _http.GetFromJsonAsync>(url) ?? [];
+ }
+
+ public async Task CreateStockItemAsync(int materialId, string length, string? name, int quantityOnHand, string? notes)
+ {
+ var body = new
+ {
+ MaterialId = materialId,
+ Length = length,
+ Name = name,
+ QuantityOnHand = quantityOnHand,
+ Notes = notes
+ };
+ var response = await _http.PostAsJsonAsync("api/stock-items", body);
+ if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ throw new ApiConflictException(error);
+ }
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadFromJsonAsync();
+ }
+
+ #endregion
+
+ #region Offerings
+
+ public async Task> GetOfferingsForSupplierAsync(int supplierId)
+ {
+ return await _http.GetFromJsonAsync>($"api/suppliers/{supplierId}/offerings") ?? [];
+ }
+
+ public async Task> GetOfferingsForStockItemAsync(int stockItemId)
+ {
+ return await _http.GetFromJsonAsync>($"api/stock-items/{stockItemId}/offerings") ?? [];
+ }
+
+ public async Task CreateOfferingAsync(int supplierId, int stockItemId,
+ string? partNumber, string? supplierDescription, decimal? price, string? notes)
+ {
+ var body = new
+ {
+ StockItemId = stockItemId,
+ PartNumber = partNumber,
+ SupplierDescription = supplierDescription,
+ Price = price,
+ Notes = notes
+ };
+ var response = await _http.PostAsJsonAsync($"api/suppliers/{supplierId}/offerings", body);
+ if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ throw new ApiConflictException(error);
+ }
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadFromJsonAsync();
+ }
+
+ #endregion
+}
+
+///
+/// Thrown when the API returns 409 Conflict (duplicate resource).
+///
+public class ApiConflictException : Exception
+{
+ public ApiConflictException(string message) : base(message) { }
+}
+
+#region API Response DTOs
+
+public class ApiSupplierDto
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string? ContactInfo { get; set; }
+ public string? Notes { get; set; }
+ public bool IsActive { get; set; }
+}
+
+public class ApiMaterialDto
+{
+ public int Id { get; set; }
+ public string Shape { get; set; } = string.Empty;
+ public string Type { get; set; } = string.Empty;
+ public string? Grade { get; set; }
+ public string Size { get; set; } = string.Empty;
+ public string? Description { get; set; }
+ public bool IsActive { get; set; }
+ public ApiMaterialDimensionsDto? Dimensions { get; set; }
+}
+
+public class ApiMaterialDimensionsDto
+{
+ public string DimensionType { get; set; } = string.Empty;
+ public Dictionary Values { get; set; } = new();
+}
+
+public class ApiStockItemDto
+{
+ public int Id { get; set; }
+ public int MaterialId { get; set; }
+ public string MaterialName { get; set; } = string.Empty;
+ public decimal LengthInches { get; set; }
+ public string LengthFormatted { get; set; } = string.Empty;
+ public string? Name { get; set; }
+ public int QuantityOnHand { get; set; }
+ public string? Notes { get; set; }
+ public bool IsActive { get; set; }
+}
+
+public class ApiOfferingDto
+{
+ public int Id { get; set; }
+ public int SupplierId { get; set; }
+ public string? SupplierName { get; set; }
+ public int StockItemId { get; set; }
+ public string? MaterialName { get; set; }
+ public decimal? LengthInches { get; set; }
+ public string? LengthFormatted { get; set; }
+ public string? PartNumber { get; set; }
+ public string? SupplierDescription { get; set; }
+ public decimal? Price { get; set; }
+ public string? Notes { get; set; }
+ public bool IsActive { get; set; }
+}
+
+#endregion
diff --git a/CutList.Mcp/CutList.Mcp.csproj b/CutList.Mcp/CutList.Mcp.csproj
index de6e390..b1faad3 100644
--- a/CutList.Mcp/CutList.Mcp.csproj
+++ b/CutList.Mcp/CutList.Mcp.csproj
@@ -2,11 +2,11 @@
-
+
diff --git a/CutList.Mcp/InventoryTools.cs b/CutList.Mcp/InventoryTools.cs
index f4a0715..81f2691 100644
--- a/CutList.Mcp/InventoryTools.cs
+++ b/CutList.Mcp/InventoryTools.cs
@@ -1,23 +1,21 @@
using System.ComponentModel;
using CutList.Core.Formatting;
-using CutList.Web.Data;
-using CutList.Web.Data.Entities;
-using Microsoft.EntityFrameworkCore;
using ModelContextProtocol.Server;
namespace CutList.Mcp;
///
/// MCP tools for inventory management - suppliers, materials, stock items, and offerings.
+/// All calls go through the CutList.Web REST API via ApiClient.
///
[McpServerToolType]
public class InventoryTools
{
- private readonly ApplicationDbContext _context;
+ private readonly ApiClient _api;
- public InventoryTools(ApplicationDbContext context)
+ public InventoryTools(ApiClient api)
{
- _context = context;
+ _api = api;
}
#region Suppliers
@@ -27,11 +25,7 @@ public class InventoryTools
[Description("Include inactive suppliers (default false)")]
bool includeInactive = false)
{
- var query = _context.Suppliers.AsQueryable();
- if (!includeInactive)
- query = query.Where(s => s.IsActive);
-
- var suppliers = await query.OrderBy(s => s.Name).ToListAsync();
+ var suppliers = await _api.GetSuppliersAsync(includeInactive);
return new SupplierListResult
{
@@ -56,29 +50,19 @@ public class InventoryTools
[Description("Notes about the supplier")]
string? notes = null)
{
- var supplier = new Supplier
- {
- Name = name,
- ContactInfo = contactInfo,
- Notes = notes,
- IsActive = true,
- CreatedAt = DateTime.UtcNow
- };
-
- _context.Suppliers.Add(supplier);
- await _context.SaveChangesAsync();
+ var supplier = await _api.CreateSupplierAsync(name, contactInfo, notes);
return new SupplierResult
{
Success = true,
- Supplier = new SupplierDto
+ Supplier = supplier != null ? new SupplierDto
{
Id = supplier.Id,
Name = supplier.Name,
ContactInfo = supplier.ContactInfo,
Notes = supplier.Notes,
IsActive = supplier.IsActive
- }
+ } : null
};
}
@@ -93,47 +77,12 @@ public class InventoryTools
[Description("Include inactive materials (default false)")]
bool includeInactive = false)
{
- var query = _context.Materials
- .Include(m => m.Dimensions)
- .AsQueryable();
-
- if (!includeInactive)
- query = query.Where(m => m.IsActive);
-
- if (!string.IsNullOrEmpty(shape))
- {
- var parsedShape = MaterialShapeExtensions.ParseShape(shape);
- if (parsedShape.HasValue)
- {
- query = query.Where(m => m.Shape == parsedShape.Value);
- }
- else
- {
- // Fallback to string-based display name search
- var shapeLower = shape.ToLower();
- query = query.Where(m => m.Shape.ToString().ToLower().Contains(shapeLower));
- }
- }
-
- var materials = await query
- .OrderBy(m => m.Shape)
- .ThenBy(m => m.SortOrder)
- .ThenBy(m => m.Size)
- .ToListAsync();
+ var materials = await _api.GetMaterialsAsync(shape, includeInactive);
return new MaterialListResult
{
Success = true,
- Materials = materials.Select(m => new MaterialDto
- {
- Id = m.Id,
- Shape = m.Shape.GetDisplayName(),
- Size = m.Size,
- Description = m.Description,
- DisplayName = m.DisplayName,
- IsActive = m.IsActive,
- Dimensions = m.Dimensions != null ? MapDimensions(m.Dimensions) : null
- }).ToList()
+ Materials = materials.Select(MapMaterial).ToList()
};
}
@@ -174,334 +123,65 @@ public class InventoryTools
[Description("Schedule (for Pipe, e.g., '40', '80', 'STD')")]
string? schedule = null)
{
- var parsedShape = MaterialShapeExtensions.ParseShape(shape);
- if (!parsedShape.HasValue)
+ // Build dimensions dictionary from individual parameters
+ var dimensions = new Dictionary();
+ if (diameter.HasValue) dimensions["Diameter"] = (decimal)diameter.Value;
+ if (outerDiameter.HasValue) dimensions["OuterDiameter"] = (decimal)outerDiameter.Value;
+ if (width.HasValue) dimensions["Width"] = (decimal)width.Value;
+ if (height.HasValue) dimensions["Height"] = (decimal)height.Value;
+ if (squareSize.HasValue) dimensions["Size"] = (decimal)squareSize.Value;
+ if (thickness.HasValue) dimensions["Thickness"] = (decimal)thickness.Value;
+ if (wall.HasValue) dimensions["Wall"] = (decimal)wall.Value;
+ if (leg1.HasValue) dimensions["Leg1"] = (decimal)leg1.Value;
+ if (leg2.HasValue) dimensions["Leg2"] = (decimal)leg2.Value;
+ if (flange.HasValue) dimensions["Flange"] = (decimal)flange.Value;
+ if (web.HasValue) dimensions["Web"] = (decimal)web.Value;
+ if (weightPerFoot.HasValue) dimensions["WeightPerFoot"] = (decimal)weightPerFoot.Value;
+ if (nominalSize.HasValue) dimensions["NominalSize"] = (decimal)nominalSize.Value;
+
+ try
{
+ var material = await _api.CreateMaterialAsync(
+ shape, size, description, null, null,
+ dimensions.Count > 0 ? dimensions : null);
+
+ if (material == null)
+ return new MaterialResult { Success = false, Error = "Failed to create material" };
+
return new MaterialResult
{
- Success = false,
- Error = $"Unknown shape: {shape}. Valid shapes are: RoundBar, RoundTube, FlatBar, SquareBar, SquareTube, RectangularTube, Angle, Channel, IBeam, Pipe"
+ Success = true,
+ Material = MapMaterial(material)
};
}
-
- // Create typed dimensions object based on shape
- MaterialDimensions? dimensions = CreateDimensionsForShape(
- parsedShape.Value, diameter, outerDiameter, width, height, squareSize,
- thickness, wall, leg1, leg2, flange, web, weightPerFoot, nominalSize, schedule);
-
- // Auto-generate size string if not provided
- var finalSize = size ?? dimensions?.GenerateSizeString();
- if (string.IsNullOrWhiteSpace(finalSize))
+ catch (ApiConflictException ex)
{
- return new MaterialResult
- {
- Success = false,
- Error = "Size string is required. Either provide a size parameter or dimension values for auto-generation."
- };
+ return new MaterialResult { Success = false, Error = ex.Message };
}
-
- // Check if material already exists
- var existing = await _context.Materials
- .FirstOrDefaultAsync(m => m.Shape == parsedShape.Value && m.Size.ToLower() == finalSize.ToLower());
-
- if (existing != null)
+ catch (HttpRequestException ex)
{
- return new MaterialResult
- {
- Success = false,
- Error = $"Material '{parsedShape.Value.GetDisplayName()} - {finalSize}' already exists with ID {existing.Id}"
- };
+ return new MaterialResult { Success = false, Error = ex.Message };
}
-
- var material = new Material
- {
- Shape = parsedShape.Value,
- Size = finalSize,
- Description = description,
- IsActive = true,
- CreatedAt = DateTime.UtcNow
- };
-
- _context.Materials.Add(material);
- await _context.SaveChangesAsync();
-
- // Add dimensions
- dimensions.MaterialId = material.Id;
- _context.MaterialDimensions.Add(dimensions);
- await _context.SaveChangesAsync();
-
- return new MaterialResult
- {
- Success = true,
- Material = new MaterialDto
- {
- Id = material.Id,
- Shape = material.Shape.GetDisplayName(),
- Size = material.Size,
- Description = material.Description,
- DisplayName = material.DisplayName,
- IsActive = material.IsActive,
- Dimensions = MapDimensions(dimensions)
- }
- };
}
- [McpServerTool(Name = "search_materials"), Description("Search for materials by dimension value with tolerance. Returns materials where the specified dimension is within +/- tolerance of the target value.")]
+ [McpServerTool(Name = "search_materials"), Description("Search for materials by shape with a target dimension value and tolerance. The primary dimension for each shape is searched (e.g., diameter for RoundBar, leg size for Angle).")]
public async Task SearchMaterials(
- [Description("Which dimension to search: diameter, outerDiameter, width, height, size, thickness, wall, leg1, leg2, flange, web, weightPerFoot, nominalSize")]
- string dimensionType,
+ [Description("Shape to search (e.g., 'RoundBar', 'Angle', 'FlatBar')")]
+ string shape,
[Description("Target dimension value in inches (or lbs for weightPerFoot)")]
double targetValue,
- [Description("Tolerance value - returns results within +/- this amount (default 0.01)")]
- double tolerance = 0.01,
- [Description("Optional shape filter (e.g., 'RoundBar', 'Angle')")]
- string? shape = null)
+ [Description("Tolerance value - returns results within +/- this amount (default 0.1)")]
+ double tolerance = 0.1)
{
- var minValue = (decimal)(targetValue - tolerance);
- var maxValue = (decimal)(targetValue + tolerance);
-
- // Search the appropriate typed dimension table based on dimension type
- List materials = dimensionType.ToLowerInvariant() switch
- {
- "diameter" => await SearchByDimension(d => d.Diameter >= minValue && d.Diameter <= maxValue, shape),
- "outerdiameter" or "outer_diameter" or "od" => await SearchByDimension(d => d.OuterDiameter >= minValue && d.OuterDiameter <= maxValue, shape),
- "width" => await SearchWidthDimensions(minValue, maxValue, shape),
- "height" => await SearchHeightDimensions(minValue, maxValue, shape),
- "size" or "squaresize" => await SearchSizeDimensions(minValue, maxValue, shape),
- "thickness" => await SearchThicknessDimensions(minValue, maxValue, shape),
- "wall" => await SearchWallDimensions(minValue, maxValue, shape),
- "leg1" => await SearchByDimension(d => d.Leg1 >= minValue && d.Leg1 <= maxValue, shape),
- "leg2" => await SearchByDimension(d => d.Leg2 >= minValue && d.Leg2 <= maxValue, shape),
- "flange" => await SearchByDimension(d => d.Flange >= minValue && d.Flange <= maxValue, shape),
- "web" => await SearchByDimension(d => d.Web >= minValue && d.Web <= maxValue, shape),
- "weightperfoot" or "weight" => await SearchByDimension(d => d.WeightPerFoot >= minValue && d.WeightPerFoot <= maxValue, shape),
- "nominalsize" or "nominal_size" or "nps" => await SearchByDimension(d => d.NominalSize >= minValue && d.NominalSize <= maxValue, shape),
- _ => throw new ArgumentException($"Unknown dimension type: {dimensionType}")
- };
+ var materials = await _api.SearchMaterialsAsync(shape, (decimal)targetValue, (decimal)tolerance);
return new MaterialListResult
{
Success = true,
- Materials = materials.Select(m => new MaterialDto
- {
- Id = m.Id,
- Shape = m.Shape.GetDisplayName(),
- Size = m.Size,
- Description = m.Description,
- DisplayName = m.DisplayName,
- IsActive = m.IsActive,
- Dimensions = m.Dimensions != null ? MapDimensions(m.Dimensions) : null
- }).ToList()
+ Materials = materials.Select(MapMaterial).ToList()
};
}
- private async Task> SearchByDimension(System.Linq.Expressions.Expression> predicate, string? shape) where T : MaterialDimensions
- {
- var query = _context.Set()
- .Include(d => d.Material)
- .Where(d => d.Material.IsActive)
- .Where(predicate);
-
- if (!string.IsNullOrEmpty(shape))
- {
- var parsedShape = MaterialShapeExtensions.ParseShape(shape);
- if (parsedShape.HasValue)
- {
- query = query.Where(d => d.Material.Shape == parsedShape.Value);
- }
- }
-
- return await query
- .Select(d => d.Material)
- .OrderBy(m => m.Shape)
- .ThenBy(m => m.SortOrder)
- .ThenBy(m => m.Size)
- .ToListAsync();
- }
-
- // Width is used by FlatBar, RectangularTube
- private async Task> SearchWidthDimensions(decimal minValue, decimal maxValue, string? shape)
- {
- var flatBars = await SearchByDimension(d => d.Width >= minValue && d.Width <= maxValue, shape);
- var rectTubes = await SearchByDimension(d => d.Width >= minValue && d.Width <= maxValue, shape);
- return flatBars.Concat(rectTubes).OrderBy(m => m.Shape).ThenBy(m => m.SortOrder).ThenBy(m => m.Size).ToList();
- }
-
- // Height is used by RectangularTube, Channel, IBeam
- private async Task> SearchHeightDimensions(decimal minValue, decimal maxValue, string? shape)
- {
- var rectTubes = await SearchByDimension(d => d.Height >= minValue && d.Height <= maxValue, shape);
- var channels = await SearchByDimension(d => d.Height >= minValue && d.Height <= maxValue, shape);
- var ibeams = await SearchByDimension(d => d.Height >= minValue && d.Height <= maxValue, shape);
- return rectTubes.Concat(channels).Concat(ibeams).OrderBy(m => m.Shape).ThenBy(m => m.SortOrder).ThenBy(m => m.Size).ToList();
- }
-
- // Size is used by SquareBar, SquareTube
- private async Task> SearchSizeDimensions(decimal minValue, decimal maxValue, string? shape)
- {
- var squareBars = await SearchByDimension(d => d.Size >= minValue && d.Size <= maxValue, shape);
- var squareTubes = await SearchByDimension(d => d.Size >= minValue && d.Size <= maxValue, shape);
- return squareBars.Concat(squareTubes).OrderBy(m => m.Shape).ThenBy(m => m.SortOrder).ThenBy(m => m.Size).ToList();
- }
-
- // Thickness is used by FlatBar, Angle
- private async Task> SearchThicknessDimensions(decimal minValue, decimal maxValue, string? shape)
- {
- var flatBars = await SearchByDimension(d => d.Thickness >= minValue && d.Thickness <= maxValue, shape);
- var angles = await SearchByDimension(d => d.Thickness >= minValue && d.Thickness <= maxValue, shape);
- return flatBars.Concat(angles).OrderBy(m => m.Shape).ThenBy(m => m.SortOrder).ThenBy(m => m.Size).ToList();
- }
-
- // Wall is used by RoundTube, SquareTube, RectangularTube, Pipe
- private async Task> SearchWallDimensions(decimal minValue, decimal maxValue, string? shape)
- {
- var roundTubes = await SearchByDimension(d => d.Wall >= minValue && d.Wall <= maxValue, shape);
- var squareTubes = await SearchByDimension(d => d.Wall >= minValue && d.Wall <= maxValue, shape);
- var rectTubes = await SearchByDimension(d => d.Wall >= minValue && d.Wall <= maxValue, shape);
- var pipes = await SearchByDimension(d => d.Wall != null && d.Wall >= minValue && d.Wall <= maxValue, shape);
- return roundTubes.Concat(squareTubes).Concat(rectTubes).Concat(pipes).OrderBy(m => m.Shape).ThenBy(m => m.SortOrder).ThenBy(m => m.Size).ToList();
- }
-
- private static MaterialDimensions? CreateDimensionsForShape(
- MaterialShape shape, double? diameter, double? outerDiameter, double? width, double? height,
- double? squareSize, double? thickness, double? wall, double? leg1, double? leg2,
- double? flange, double? web, double? weightPerFoot, double? nominalSize, string? schedule)
- {
- return shape switch
- {
- MaterialShape.RoundBar when diameter.HasValue => new RoundBarDimensions { Diameter = (decimal)diameter.Value },
- MaterialShape.RoundTube when outerDiameter.HasValue && wall.HasValue => new RoundTubeDimensions { OuterDiameter = (decimal)outerDiameter.Value, Wall = (decimal)wall.Value },
- MaterialShape.FlatBar when width.HasValue && thickness.HasValue => new FlatBarDimensions { Width = (decimal)width.Value, Thickness = (decimal)thickness.Value },
- MaterialShape.SquareBar when squareSize.HasValue => new SquareBarDimensions { Size = (decimal)squareSize.Value },
- MaterialShape.SquareTube when squareSize.HasValue && wall.HasValue => new SquareTubeDimensions { Size = (decimal)squareSize.Value, Wall = (decimal)wall.Value },
- MaterialShape.RectangularTube when width.HasValue && height.HasValue && wall.HasValue => new RectangularTubeDimensions { Width = (decimal)width.Value, Height = (decimal)height.Value, Wall = (decimal)wall.Value },
- MaterialShape.Angle when leg1.HasValue && leg2.HasValue && thickness.HasValue => new AngleDimensions { Leg1 = (decimal)leg1.Value, Leg2 = (decimal)leg2.Value, Thickness = (decimal)thickness.Value },
- MaterialShape.Channel when height.HasValue && flange.HasValue && web.HasValue => new ChannelDimensions { Height = (decimal)height.Value, Flange = (decimal)flange.Value, Web = (decimal)web.Value },
- MaterialShape.IBeam when height.HasValue && weightPerFoot.HasValue => new IBeamDimensions { Height = (decimal)height.Value, WeightPerFoot = (decimal)weightPerFoot.Value },
- MaterialShape.Pipe when nominalSize.HasValue => new PipeDimensions { NominalSize = (decimal)nominalSize.Value, Wall = wall.HasValue ? (decimal)wall.Value : null, Schedule = schedule },
- _ => null
- };
- }
-
- ///
- /// Parses a size string into MaterialDimensions based on shape.
- /// Format: values separated by 'x' (e.g., "1 1/2 x 1/8", "2 x 2 x 1/4")
- ///
- private static (MaterialDimensions? dimensions, int sortOrder) ParseSizeStringToDimensions(MaterialShape shape, string sizeString)
- {
- var p = sizeString.Split('x', StringSplitOptions.TrimEntries)
- .Select(ParseDimension)
- .ToArray();
-
- decimal D(int i) => (decimal)p[i]!.Value;
- int Sort(double v) => (int)(v * 1000);
-
- return shape switch
- {
- MaterialShape.RoundBar when p.Length >= 1 && p[0].HasValue
- => (new RoundBarDimensions { Diameter = D(0) }, Sort(p[0]!.Value)),
-
- MaterialShape.RoundTube when p.Length >= 2 && p[0].HasValue && p[1].HasValue
- => (new RoundTubeDimensions { OuterDiameter = D(0), Wall = D(1) }, Sort(p[0]!.Value)),
-
- MaterialShape.FlatBar when p.Length >= 2 && p[0].HasValue && p[1].HasValue
- => (new FlatBarDimensions { Width = D(0), Thickness = D(1) }, Sort(p[0]!.Value)),
-
- MaterialShape.SquareBar when p.Length >= 1 && p[0].HasValue
- => (new SquareBarDimensions { Size = D(0) }, Sort(p[0]!.Value)),
-
- MaterialShape.SquareTube when p.Length >= 2 && p[0].HasValue && p[1].HasValue
- => (new SquareTubeDimensions { Size = D(0), Wall = D(1) }, Sort(p[0]!.Value)),
-
- MaterialShape.RectangularTube when p.Length >= 3 && p[0].HasValue && p[1].HasValue && p[2].HasValue
- => (new RectangularTubeDimensions { Width = D(0), Height = D(1), Wall = D(2) }, Sort(Math.Max(p[0]!.Value, p[1]!.Value))),
-
- MaterialShape.Angle when p.Length >= 3 && p[0].HasValue && p[1].HasValue && p[2].HasValue
- => (new AngleDimensions { Leg1 = D(0), Leg2 = D(1), Thickness = D(2) }, Sort(Math.Max(p[0]!.Value, p[1]!.Value))),
-
- MaterialShape.Channel when p.Length >= 3 && p[0].HasValue && p[1].HasValue && p[2].HasValue
- => (new ChannelDimensions { Height = D(0), Flange = D(1), Web = D(2) }, Sort(p[0]!.Value)),
-
- MaterialShape.IBeam when p.Length >= 2 && p[0].HasValue && p[1].HasValue
- => (new IBeamDimensions { Height = D(0), WeightPerFoot = D(1) }, Sort(p[0]!.Value)),
-
- MaterialShape.Pipe when p.Length >= 1 && p[0].HasValue
- => (new PipeDimensions { NominalSize = D(0), Schedule = ParsePipeSchedule(sizeString) }, Sort(p[0]!.Value)),
-
- _ => (null, 0)
- };
-
- static double? ParseDimension(string value)
- {
- if (string.IsNullOrWhiteSpace(value)) return null;
- var processed = Fraction.ReplaceFractionsWithDecimals(value.Trim());
- return processed.Split(' ', StringSplitOptions.RemoveEmptyEntries)
- .Sum(part => double.TryParse(part, out var d) ? d : 0) is > 0 and var total ? total : null;
- }
-
- static string? ParsePipeSchedule(string s)
- {
- var idx = s.IndexOf("SCH", StringComparison.OrdinalIgnoreCase);
- if (idx < 0) return null;
- var rest = s[(idx + 3)..].Trim();
- var end = rest.IndexOfAny([' ', 'x', 'X']);
- return end > 0 ? rest[..end] : rest;
- }
- }
-
- private static MaterialDimensionsDto MapDimensions(MaterialDimensions d)
- {
- var dto = new MaterialDimensionsDto();
-
- switch (d)
- {
- case RoundBarDimensions rb:
- dto.Diameter = (double)rb.Diameter;
- break;
- case RoundTubeDimensions rt:
- dto.OuterDiameter = (double)rt.OuterDiameter;
- dto.Wall = (double)rt.Wall;
- break;
- case FlatBarDimensions fb:
- dto.Width = (double)fb.Width;
- dto.Thickness = (double)fb.Thickness;
- break;
- case SquareBarDimensions sb:
- dto.Size = (double)sb.Size;
- break;
- case SquareTubeDimensions st:
- dto.Size = (double)st.Size;
- dto.Wall = (double)st.Wall;
- break;
- case RectangularTubeDimensions rect:
- dto.Width = (double)rect.Width;
- dto.Height = (double)rect.Height;
- dto.Wall = (double)rect.Wall;
- break;
- case AngleDimensions a:
- dto.Leg1 = (double)a.Leg1;
- dto.Leg2 = (double)a.Leg2;
- dto.Thickness = (double)a.Thickness;
- break;
- case ChannelDimensions c:
- dto.Height = (double)c.Height;
- dto.Flange = (double)c.Flange;
- dto.Web = (double)c.Web;
- break;
- case IBeamDimensions i:
- dto.Height = (double)i.Height;
- dto.WeightPerFoot = (double)i.WeightPerFoot;
- break;
- case PipeDimensions p:
- dto.NominalSize = (double)p.NominalSize;
- dto.Wall = p.Wall.HasValue ? (double)p.Wall.Value : null;
- dto.Schedule = p.Schedule;
- break;
- }
-
- return dto;
- }
-
#endregion
#region Stock Items
@@ -515,35 +195,28 @@ public class InventoryTools
[Description("Include inactive stock items (default false)")]
bool includeInactive = false)
{
- var query = _context.StockItems
- .Include(s => s.Material)
- .AsQueryable();
-
- if (!includeInactive)
- query = query.Where(s => s.IsActive);
+ List items;
if (materialId.HasValue)
- query = query.Where(s => s.MaterialId == materialId.Value);
-
- if (!string.IsNullOrEmpty(shape))
{
- var parsedShape = MaterialShapeExtensions.ParseShape(shape);
- if (parsedShape.HasValue)
- {
- query = query.Where(s => s.Material.Shape == parsedShape.Value);
- }
- else
- {
- var shapeLower = shape.ToLower();
- query = query.Where(s => s.Material.Shape.ToString().ToLower().Contains(shapeLower));
- }
+ items = await _api.GetStockItemsAsync(materialId, includeInactive);
+ }
+ else if (!string.IsNullOrEmpty(shape))
+ {
+ // Get materials for this shape, then get stock items for each
+ var materials = await _api.GetMaterialsAsync(shape, includeInactive);
+ var allItems = new List();
+ foreach (var mat in materials)
+ {
+ var matItems = await _api.GetStockItemsAsync(mat.Id, includeInactive);
+ allItems.AddRange(matItems);
+ }
+ items = allItems;
+ }
+ else
+ {
+ items = await _api.GetStockItemsAsync(includeInactive: includeInactive);
}
-
- var items = await query
- .OrderBy(s => s.Material.Shape)
- .ThenBy(s => s.Material.Size)
- .ThenBy(s => s.LengthInches)
- .ToListAsync();
return new StockItemListResult
{
@@ -552,9 +225,9 @@ public class InventoryTools
{
Id = s.Id,
MaterialId = s.MaterialId,
- MaterialName = s.Material.DisplayName,
+ MaterialName = s.MaterialName,
LengthInches = s.LengthInches,
- LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
+ LengthFormatted = s.LengthFormatted,
Name = s.Name,
QuantityOnHand = s.QuantityOnHand,
Notes = s.Notes,
@@ -576,76 +249,38 @@ public class InventoryTools
[Description("Notes")]
string? notes = null)
{
- var material = await _context.Materials.FindAsync(materialId);
- if (material == null)
- {
- return new StockItemResult
- {
- Success = false,
- Error = $"Material with ID {materialId} not found"
- };
- }
-
- // Parse length
- double lengthInches;
try
{
- lengthInches = double.TryParse(length.Trim(), out var plain)
- ? plain
- : ArchUnits.ParseToInches(length);
- }
- catch
- {
+ var stockItem = await _api.CreateStockItemAsync(materialId, length, name, quantityOnHand, notes);
+
+ if (stockItem == null)
+ return new StockItemResult { Success = false, Error = "Failed to create stock item" };
+
return new StockItemResult
{
- Success = false,
- Error = $"Could not parse length: {length}"
+ Success = true,
+ StockItem = new StockItemDto
+ {
+ Id = stockItem.Id,
+ MaterialId = stockItem.MaterialId,
+ MaterialName = stockItem.MaterialName,
+ LengthInches = stockItem.LengthInches,
+ LengthFormatted = stockItem.LengthFormatted,
+ Name = stockItem.Name,
+ QuantityOnHand = stockItem.QuantityOnHand,
+ Notes = stockItem.Notes,
+ IsActive = stockItem.IsActive
+ }
};
}
-
- // Check for duplicate
- var existing = await _context.StockItems
- .FirstOrDefaultAsync(s => s.MaterialId == materialId && s.LengthInches == (decimal)lengthInches && s.IsActive);
-
- if (existing != null)
+ catch (ApiConflictException ex)
{
- return new StockItemResult
- {
- Success = false,
- Error = $"Stock item for {material.DisplayName} at {ArchUnits.FormatFromInches(lengthInches)} already exists with ID {existing.Id}"
- };
+ return new StockItemResult { Success = false, Error = ex.Message };
}
-
- var stockItem = new StockItem
+ catch (HttpRequestException ex)
{
- MaterialId = materialId,
- LengthInches = (decimal)lengthInches,
- Name = name,
- QuantityOnHand = quantityOnHand,
- Notes = notes,
- IsActive = true,
- CreatedAt = DateTime.UtcNow
- };
-
- _context.StockItems.Add(stockItem);
- await _context.SaveChangesAsync();
-
- return new StockItemResult
- {
- Success = true,
- StockItem = new StockItemDto
- {
- Id = stockItem.Id,
- MaterialId = stockItem.MaterialId,
- MaterialName = material.DisplayName,
- LengthInches = stockItem.LengthInches,
- LengthFormatted = ArchUnits.FormatFromInches((double)stockItem.LengthInches),
- Name = stockItem.Name,
- QuantityOnHand = stockItem.QuantityOnHand,
- Notes = stockItem.Notes,
- IsActive = stockItem.IsActive
- }
- };
+ return new StockItemResult { Success = false, Error = ex.Message };
+ }
}
#endregion
@@ -661,27 +296,50 @@ public class InventoryTools
[Description("Filter by material ID")]
int? materialId = null)
{
- var query = _context.SupplierOfferings
- .Include(o => o.Supplier)
- .Include(o => o.StockItem)
- .ThenInclude(s => s.Material)
- .AsQueryable();
+ List offerings;
if (supplierId.HasValue)
- query = query.Where(o => o.SupplierId == supplierId.Value);
-
- if (stockItemId.HasValue)
- query = query.Where(o => o.StockItemId == stockItemId.Value);
-
- if (materialId.HasValue)
- query = query.Where(o => o.StockItem.MaterialId == materialId.Value);
-
- var offerings = await query
- .OrderBy(o => o.Supplier.Name)
- .ThenBy(o => o.StockItem.Material.Shape)
- .ThenBy(o => o.StockItem.Material.Size)
- .ThenBy(o => o.StockItem.LengthInches)
- .ToListAsync();
+ {
+ offerings = await _api.GetOfferingsForSupplierAsync(supplierId.Value);
+ // Apply additional filters client-side
+ if (stockItemId.HasValue)
+ offerings = offerings.Where(o => o.StockItemId == stockItemId.Value).ToList();
+ if (materialId.HasValue)
+ {
+ // Need to get stock items for this material to filter
+ var stockItems = await _api.GetStockItemsAsync(materialId);
+ var stockItemIds = stockItems.Select(s => s.Id).ToHashSet();
+ offerings = offerings.Where(o => stockItemIds.Contains(o.StockItemId)).ToList();
+ }
+ }
+ else if (stockItemId.HasValue)
+ {
+ offerings = await _api.GetOfferingsForStockItemAsync(stockItemId.Value);
+ }
+ else if (materialId.HasValue)
+ {
+ // Get stock items for this material, then aggregate offerings
+ var stockItems = await _api.GetStockItemsAsync(materialId);
+ var allOfferings = new List();
+ foreach (var si in stockItems)
+ {
+ var siOfferings = await _api.GetOfferingsForStockItemAsync(si.Id);
+ allOfferings.AddRange(siOfferings);
+ }
+ offerings = allOfferings;
+ }
+ else
+ {
+ // No filter - get all suppliers then aggregate
+ var suppliers = await _api.GetSuppliersAsync();
+ var allOfferings = new List();
+ foreach (var s in suppliers)
+ {
+ var sOfferings = await _api.GetOfferingsForSupplierAsync(s.Id);
+ allOfferings.AddRange(sOfferings);
+ }
+ offerings = allOfferings;
+ }
return new SupplierOfferingListResult
{
@@ -690,14 +348,14 @@ public class InventoryTools
{
Id = o.Id,
SupplierId = o.SupplierId,
- SupplierName = o.Supplier.Name,
+ SupplierName = o.SupplierName ?? string.Empty,
StockItemId = o.StockItemId,
- MaterialName = o.StockItem.Material.DisplayName,
- LengthFormatted = ArchUnits.FormatFromInches((double)o.StockItem.LengthInches),
+ MaterialName = o.MaterialName ?? string.Empty,
+ LengthFormatted = o.LengthFormatted ?? string.Empty,
PartNumber = o.PartNumber,
SupplierDescription = o.SupplierDescription,
Price = o.Price,
- Notes = o.Notes
+ Notes = o.Notes
}).ToList()
};
}
@@ -717,72 +375,39 @@ public class InventoryTools
[Description("Notes")]
string? notes = null)
{
- var supplier = await _context.Suppliers.FindAsync(supplierId);
- if (supplier == null)
+ try
{
+ var offering = await _api.CreateOfferingAsync(supplierId, stockItemId, partNumber, supplierDescription, price, notes);
+
+ if (offering == null)
+ return new SupplierOfferingResult { Success = false, Error = "Failed to create offering" };
+
return new SupplierOfferingResult
{
- Success = false,
- Error = $"Supplier with ID {supplierId} not found"
+ Success = true,
+ Offering = new SupplierOfferingDto
+ {
+ Id = offering.Id,
+ SupplierId = offering.SupplierId,
+ SupplierName = offering.SupplierName ?? string.Empty,
+ StockItemId = offering.StockItemId,
+ MaterialName = offering.MaterialName ?? string.Empty,
+ LengthFormatted = offering.LengthFormatted ?? string.Empty,
+ PartNumber = offering.PartNumber,
+ SupplierDescription = offering.SupplierDescription,
+ Price = offering.Price,
+ Notes = offering.Notes
+ }
};
}
-
- var stockItem = await _context.StockItems
- .Include(s => s.Material)
- .FirstOrDefaultAsync(s => s.Id == stockItemId);
-
- if (stockItem == null)
+ catch (ApiConflictException ex)
{
- return new SupplierOfferingResult
- {
- Success = false,
- Error = $"Stock item with ID {stockItemId} not found"
- };
+ return new SupplierOfferingResult { Success = false, Error = ex.Message };
}
-
- // Check for duplicate
- var existing = await _context.SupplierOfferings
- .FirstOrDefaultAsync(o => o.SupplierId == supplierId && o.StockItemId == stockItemId);
-
- if (existing != null)
+ catch (HttpRequestException ex)
{
- return new SupplierOfferingResult
- {
- Success = false,
- Error = $"Offering from {supplier.Name} for this stock item already exists with ID {existing.Id}"
- };
+ return new SupplierOfferingResult { Success = false, Error = ex.Message };
}
-
- var offering = new SupplierOffering
- {
- SupplierId = supplierId,
- StockItemId = stockItemId,
- PartNumber = partNumber,
- SupplierDescription = supplierDescription,
- Price = price,
- Notes = notes
- };
-
- _context.SupplierOfferings.Add(offering);
- await _context.SaveChangesAsync();
-
- return new SupplierOfferingResult
- {
- Success = true,
- Offering = new SupplierOfferingDto
- {
- Id = offering.Id,
- SupplierId = offering.SupplierId,
- SupplierName = supplier.Name,
- StockItemId = offering.StockItemId,
- MaterialName = stockItem.Material.DisplayName,
- LengthFormatted = ArchUnits.FormatFromInches((double)stockItem.LengthInches),
- PartNumber = offering.PartNumber,
- SupplierDescription = offering.SupplierDescription,
- Price = offering.Price,
- Notes = offering.Notes
- }
- };
}
[McpServerTool(Name = "add_stock_with_offering"), Description("Convenience method: adds a material (if needed), stock item (if needed), and supplier offering all at once.")]
@@ -806,39 +431,7 @@ public class InventoryTools
[Description("Price per unit")]
decimal? price = null)
{
- var supplier = await _context.Suppliers.FindAsync(supplierId);
- if (supplier == null)
- {
- return new AddStockWithOfferingResult
- {
- Success = false,
- Error = $"Supplier with ID {supplierId} not found"
- };
- }
-
- // Parse shape
- var parsedShape = MaterialShapeExtensions.ParseShape(shape);
- if (!parsedShape.HasValue)
- {
- return new AddStockWithOfferingResult
- {
- Success = false,
- Error = $"Unknown shape: {shape}. Valid shapes are: RoundBar, RoundTube, FlatBar, SquareBar, SquareTube, RectangularTube, Angle, Channel, IBeam, Pipe"
- };
- }
-
- // Parse material type (default to Steel if not provided)
- var typeValue = string.IsNullOrWhiteSpace(type) ? "Steel" : type;
- if (!Enum.TryParse(typeValue, ignoreCase: true, out var parsedType))
- {
- return new AddStockWithOfferingResult
- {
- Success = false,
- Error = $"Unknown material type: {typeValue}. Valid types are: Steel, Aluminum, Stainless, Brass, Copper"
- };
- }
-
- // Parse length
+ // Parse length for formatted display
double lengthInches;
try
{
@@ -855,106 +448,227 @@ public class InventoryTools
};
}
- // Find or create material (match on shape, type, grade, and size)
- var material = await _context.Materials
- .Include(m => m.Dimensions)
- .FirstOrDefaultAsync(m => m.Shape == parsedShape.Value
- && m.Type == parsedType
- && m.Grade == grade
- && m.Size.ToLower() == size.ToLower());
-
+ // Step 1: Find or create material
bool materialCreated = false;
+ ApiMaterialDto? material = null;
+
+ // Search for existing material by shape and size
+ var materials = await _api.GetMaterialsAsync(shape);
+ material = materials.FirstOrDefault(m =>
+ m.Size.Equals(size, StringComparison.OrdinalIgnoreCase) &&
+ m.Type.Equals(type, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(m.Grade, grade, StringComparison.OrdinalIgnoreCase));
+
if (material == null)
{
- // Parse dimensions from size string
- var (dimensions, sortOrder) = ParseSizeStringToDimensions(parsedShape.Value, size);
+ // Parse dimensions from size string for the API
+ var dimensions = ParseSizeStringToDimensions(shape, size);
- material = new Material
+ try
{
- Shape = parsedShape.Value,
- Type = parsedType,
- Grade = grade,
- Size = size,
- SortOrder = sortOrder,
- IsActive = true,
- CreatedAt = DateTime.UtcNow
- };
- _context.Materials.Add(material);
- await _context.SaveChangesAsync();
-
- // Add dimensions if parsed successfully
- if (dimensions != null)
- {
- dimensions.MaterialId = material.Id;
- _context.MaterialDimensions.Add(dimensions);
- await _context.SaveChangesAsync();
+ material = await _api.CreateMaterialAsync(shape, size, null, type, grade, dimensions);
+ materialCreated = true;
+ }
+ catch (ApiConflictException)
+ {
+ // Race condition - material was created between check and create, re-fetch
+ materials = await _api.GetMaterialsAsync(shape);
+ material = materials.FirstOrDefault(m =>
+ m.Size.Equals(size, StringComparison.OrdinalIgnoreCase));
+ }
+ catch (HttpRequestException ex)
+ {
+ return new AddStockWithOfferingResult { Success = false, Error = $"Failed to create material: {ex.Message}" };
}
-
- materialCreated = true;
}
- // Find or create stock item
- var stockItem = await _context.StockItems
- .FirstOrDefaultAsync(s => s.MaterialId == material.Id && s.LengthInches == (decimal)lengthInches && s.IsActive);
+ if (material == null)
+ return new AddStockWithOfferingResult { Success = false, Error = "Failed to find or create material" };
+ // Step 2: Find or create stock item
bool stockItemCreated = false;
+ var stockItems = await _api.GetStockItemsAsync(material.Id);
+ var stockItem = stockItems.FirstOrDefault(s => Math.Abs((double)s.LengthInches - lengthInches) < 0.01);
+
if (stockItem == null)
{
- stockItem = new StockItem
+ try
{
- MaterialId = material.Id,
- LengthInches = (decimal)lengthInches,
- QuantityOnHand = 0,
- IsActive = true,
- CreatedAt = DateTime.UtcNow
- };
- _context.StockItems.Add(stockItem);
- await _context.SaveChangesAsync();
- stockItemCreated = true;
+ stockItem = await _api.CreateStockItemAsync(material.Id, length, null, 0, null);
+ stockItemCreated = true;
+ }
+ catch (ApiConflictException)
+ {
+ // Race condition - re-fetch
+ stockItems = await _api.GetStockItemsAsync(material.Id);
+ stockItem = stockItems.FirstOrDefault(s => Math.Abs((double)s.LengthInches - lengthInches) < 0.01);
+ }
+ catch (HttpRequestException ex)
+ {
+ return new AddStockWithOfferingResult
+ {
+ Success = false,
+ Error = $"Failed to create stock item: {ex.Message}",
+ MaterialCreated = materialCreated
+ };
+ }
}
- // Check if offering already exists
- var existingOffering = await _context.SupplierOfferings
- .FirstOrDefaultAsync(o => o.SupplierId == supplierId && o.StockItemId == stockItem.Id);
+ if (stockItem == null)
+ return new AddStockWithOfferingResult
+ {
+ Success = false,
+ Error = "Failed to find or create stock item",
+ MaterialCreated = materialCreated
+ };
- if (existingOffering != null)
+ // Step 3: Create offering
+ try
+ {
+ var offering = await _api.CreateOfferingAsync(supplierId, stockItem.Id, partNumber, supplierDescription, price, null);
+
+ return new AddStockWithOfferingResult
+ {
+ Success = true,
+ MaterialId = material.Id,
+ MaterialName = $"{material.Shape} - {material.Size}",
+ MaterialCreated = materialCreated,
+ StockItemId = stockItem.Id,
+ StockItemCreated = stockItemCreated,
+ LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
+ OfferingId = offering?.Id ?? 0,
+ PartNumber = partNumber,
+ SupplierDescription = supplierDescription,
+ Price = price
+ };
+ }
+ catch (ApiConflictException)
{
return new AddStockWithOfferingResult
{
Success = false,
- Error = $"Offering from {supplier.Name} for {material.DisplayName} at {ArchUnits.FormatFromInches(lengthInches)} already exists",
+ Error = $"Offering for this supplier and stock item already exists",
MaterialCreated = materialCreated,
StockItemCreated = stockItemCreated
};
}
-
- // Create offering
- var offering = new SupplierOffering
+ catch (HttpRequestException ex)
{
- SupplierId = supplierId,
- StockItemId = stockItem.Id,
- PartNumber = partNumber,
- SupplierDescription = supplierDescription,
- Price = price
+ return new AddStockWithOfferingResult
+ {
+ Success = false,
+ Error = $"Failed to create offering: {ex.Message}",
+ MaterialCreated = materialCreated,
+ StockItemCreated = stockItemCreated
+ };
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static MaterialDto MapMaterial(ApiMaterialDto m)
+ {
+ var dto = new MaterialDto
+ {
+ Id = m.Id,
+ Shape = m.Shape,
+ Size = m.Size,
+ Description = m.Description,
+ DisplayName = $"{m.Shape} - {m.Size}",
+ IsActive = m.IsActive,
};
- _context.SupplierOfferings.Add(offering);
- await _context.SaveChangesAsync();
-
- return new AddStockWithOfferingResult
+ if (m.Dimensions != null)
{
- Success = true,
- MaterialId = material.Id,
- MaterialName = material.DisplayName,
- MaterialCreated = materialCreated,
- StockItemId = stockItem.Id,
- StockItemCreated = stockItemCreated,
- LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
- OfferingId = offering.Id,
- PartNumber = partNumber,
- SupplierDescription = supplierDescription,
- Price = price
+ dto.Dimensions = MapDimensions(m.Dimensions);
+ }
+
+ return dto;
+ }
+
+ private static MaterialDimensionsDto MapDimensions(ApiMaterialDimensionsDto d)
+ {
+ var dto = new MaterialDimensionsDto();
+ var v = d.Values;
+
+ if (v.TryGetValue("Diameter", out var diameter)) dto.Diameter = (double)diameter;
+ if (v.TryGetValue("OuterDiameter", out var od)) dto.OuterDiameter = (double)od;
+ if (v.TryGetValue("Width", out var width)) dto.Width = (double)width;
+ if (v.TryGetValue("Height", out var height)) dto.Height = (double)height;
+ if (v.TryGetValue("Size", out var size)) dto.Size = (double)size;
+ if (v.TryGetValue("Thickness", out var thickness)) dto.Thickness = (double)thickness;
+ if (v.TryGetValue("Wall", out var wall)) dto.Wall = (double)wall;
+ if (v.TryGetValue("Leg1", out var leg1)) dto.Leg1 = (double)leg1;
+ if (v.TryGetValue("Leg2", out var leg2)) dto.Leg2 = (double)leg2;
+ if (v.TryGetValue("Flange", out var flange)) dto.Flange = (double)flange;
+ if (v.TryGetValue("Web", out var web)) dto.Web = (double)web;
+ if (v.TryGetValue("WeightPerFoot", out var wpf)) dto.WeightPerFoot = (double)wpf;
+ if (v.TryGetValue("NominalSize", out var ns)) dto.NominalSize = (double)ns;
+
+ return dto;
+ }
+
+ ///
+ /// Parses a size string into a dimensions dictionary based on shape.
+ /// Format: values separated by 'x' (e.g., "1 1/2 x 1/8", "2 x 2 x 1/4")
+ ///
+ private static Dictionary? ParseSizeStringToDimensions(string shape, string sizeString)
+ {
+ var parts = sizeString.Split('x', StringSplitOptions.TrimEntries)
+ .Select(ParseDimension)
+ .ToArray();
+
+ if (parts.Length == 0 || parts.All(p => !p.HasValue))
+ return null;
+
+ decimal D(int i) => (decimal)parts[i]!.Value;
+
+ var shapeLower = shape.ToLowerInvariant().Replace(" ", "");
+
+ return shapeLower switch
+ {
+ "roundbar" when parts.Length >= 1 && parts[0].HasValue
+ => new Dictionary { ["Diameter"] = D(0) },
+
+ "roundtube" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
+ => new Dictionary { ["OuterDiameter"] = D(0), ["Wall"] = D(1) },
+
+ "flatbar" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
+ => new Dictionary { ["Width"] = D(0), ["Thickness"] = D(1) },
+
+ "squarebar" when parts.Length >= 1 && parts[0].HasValue
+ => new Dictionary { ["Size"] = D(0) },
+
+ "squaretube" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
+ => new Dictionary { ["Size"] = D(0), ["Wall"] = D(1) },
+
+ "rectangulartube" or "recttube" when parts.Length >= 3 && parts[0].HasValue && parts[1].HasValue && parts[2].HasValue
+ => new Dictionary { ["Width"] = D(0), ["Height"] = D(1), ["Wall"] = D(2) },
+
+ "angle" when parts.Length >= 3 && parts[0].HasValue && parts[1].HasValue && parts[2].HasValue
+ => new Dictionary { ["Leg1"] = D(0), ["Leg2"] = D(1), ["Thickness"] = D(2) },
+
+ "channel" when parts.Length >= 3 && parts[0].HasValue && parts[1].HasValue && parts[2].HasValue
+ => new Dictionary { ["Height"] = D(0), ["Flange"] = D(1), ["Web"] = D(2) },
+
+ "ibeam" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
+ => new Dictionary { ["Height"] = D(0), ["WeightPerFoot"] = D(1) },
+
+ "pipe" when parts.Length >= 1 && parts[0].HasValue
+ => new Dictionary { ["NominalSize"] = D(0) },
+
+ _ => null
};
+
+ static double? ParseDimension(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value)) return null;
+ var processed = Fraction.ReplaceFractionsWithDecimals(value.Trim());
+ return processed.Split(' ', StringSplitOptions.RemoveEmptyEntries)
+ .Sum(part => double.TryParse(part, out var d) ? d : 0) is > 0 and var total ? total : null;
+ }
}
#endregion
diff --git a/CutList.Mcp/Program.cs b/CutList.Mcp/Program.cs
index ed8d318..4f9b12e 100644
--- a/CutList.Mcp/Program.cs
+++ b/CutList.Mcp/Program.cs
@@ -1,14 +1,15 @@
-using CutList.Web.Data;
-using Microsoft.EntityFrameworkCore;
+using CutList.Mcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
var builder = Host.CreateApplicationBuilder(args);
-// Add DbContext for inventory tools
-builder.Services.AddDbContext(options =>
- options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"));
+// Register HttpClient for API calls to CutList.Web
+builder.Services.AddHttpClient(client =>
+{
+ client.BaseAddress = new Uri("http://localhost:5009");
+});
builder.Services
.AddMcpServer()