Files
CutList/CutList.Mcp/ApiClient.cs
AJ Isaacs 177affabf0 refactor: Decouple MCP server from direct DB access
Replace direct EF Core/DbContext usage in MCP tools with HTTP calls
to the CutList.Web REST API via new ApiClient. Removes CutList.Web
project reference from MCP, adds Microsoft.Extensions.Http instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 16:54:05 -05:00

215 lines
7.0 KiB
C#

using System.Net.Http.Json;
namespace CutList.Mcp;
/// <summary>
/// Typed HTTP client for calling the CutList.Web REST API.
/// </summary>
public class ApiClient
{
private readonly HttpClient _http;
public ApiClient(HttpClient http)
{
_http = http;
}
#region Suppliers
public async Task<List<ApiSupplierDto>> GetSuppliersAsync(bool includeInactive = false)
{
var url = $"api/suppliers?includeInactive={includeInactive}";
return await _http.GetFromJsonAsync<List<ApiSupplierDto>>(url) ?? [];
}
public async Task<ApiSupplierDto?> 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<ApiSupplierDto>();
}
#endregion
#region Materials
public async Task<List<ApiMaterialDto>> 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<List<ApiMaterialDto>>(url) ?? [];
}
public async Task<ApiMaterialDto?> CreateMaterialAsync(string shape, string? size, string? description,
string? type, string? grade, Dictionary<string, decimal>? 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<ApiMaterialDto>();
}
public async Task<List<ApiMaterialDto>> 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<List<ApiMaterialDto>>() ?? [];
}
#endregion
#region Stock Items
public async Task<List<ApiStockItemDto>> 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<List<ApiStockItemDto>>(url) ?? [];
}
public async Task<ApiStockItemDto?> 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<ApiStockItemDto>();
}
#endregion
#region Offerings
public async Task<List<ApiOfferingDto>> GetOfferingsForSupplierAsync(int supplierId)
{
return await _http.GetFromJsonAsync<List<ApiOfferingDto>>($"api/suppliers/{supplierId}/offerings") ?? [];
}
public async Task<List<ApiOfferingDto>> GetOfferingsForStockItemAsync(int stockItemId)
{
return await _http.GetFromJsonAsync<List<ApiOfferingDto>>($"api/stock-items/{stockItemId}/offerings") ?? [];
}
public async Task<ApiOfferingDto?> 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<ApiOfferingDto>();
}
#endregion
}
/// <summary>
/// Thrown when the API returns 409 Conflict (duplicate resource).
/// </summary>
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<string, decimal> 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