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>
This commit is contained in:
@@ -0,0 +1,214 @@
|
|||||||
|
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
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
||||||
<ProjectReference Include="..\CutList.Web\CutList.Web.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||||
<PackageReference Include="ModelContextProtocol" Version="0.7.0-preview.1" />
|
<PackageReference Include="ModelContextProtocol" Version="0.7.0-preview.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
+353
-639
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
|||||||
using CutList.Web.Data;
|
using CutList.Mcp;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
// Add DbContext for inventory tools
|
// Register HttpClient for API calls to CutList.Web
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
builder.Services.AddHttpClient<ApiClient>(client =>
|
||||||
options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"));
|
{
|
||||||
|
client.BaseAddress = new Uri("http://localhost:5009");
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddMcpServer()
|
.AddMcpServer()
|
||||||
|
|||||||
Reference in New Issue
Block a user