From ab1a975ec7465859be1cef424cb7039fc77fa0c4 Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 11:41:59 +0200 Subject: [PATCH 1/7] feat(mcp): add Tharga.Cache.Mcp project skeleton (csproj + README) --- Tharga.Cache.Mcp/README.md | 52 +++++++++++++++++++ Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj | 47 +++++++++++++++++ Tharga.Cache.sln | 14 ++++++ plan/feature.md | 64 ++++++++++++++++++++++++ plan/plan.md | 10 ++++ 5 files changed, 187 insertions(+) create mode 100644 Tharga.Cache.Mcp/README.md create mode 100644 Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj create mode 100644 plan/feature.md create mode 100644 plan/plan.md diff --git a/Tharga.Cache.Mcp/README.md b/Tharga.Cache.Mcp/README.md new file mode 100644 index 0000000..f4d9a2d --- /dev/null +++ b/Tharga.Cache.Mcp/README.md @@ -0,0 +1,52 @@ +# Tharga.Cache.Mcp + +[![NuGet](https://img.shields.io/nuget/v/Tharga.Cache.Mcp)](https://www.nuget.org/packages/Tharga.Cache.Mcp) +![Nuget](https://img.shields.io/nuget/dt/Tharga.Cache.Mcp) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +MCP (Model Context Protocol) provider for [Tharga.Cache](https://www.nuget.org/packages/Tharga.Cache). Lets an MCP-aware AI client browse cached data and run cache maintenance actions. Plugs into [Tharga.Mcp](https://www.nuget.org/packages/Tharga.Mcp). + +## Get Started + +```bash +dotnet add package Tharga.Cache +dotnet add package Tharga.Cache.Mcp +``` + +```csharp +builder.Services.AddCache(); +builder.Services.AddThargaMcp(b => b.AddCache()); + +// ... +app.UseThargaMcp(); +``` + +## Resources + +| URI | Description | +|-----|-------------| +| `cache://types` | All registered cache types with persistence backend, item count, total size, and config flags | +| `cache://items` | Flat list of cached items with key, size, fresh span, expires, last accessed, access count, load duration, stale | +| `cache://health` | Health status of each persistence backend (Memory, Redis, MongoDB, File) | +| `cache://queue` | Current fetch queue depth | + +## Tools + +| Name | Action | +|------|--------| +| `cache.clear_stale` | Evicts all stale items | +| `cache.clear_all` | Evicts everything from all caches | + +## Why expose the cache via MCP? + +- **Visibility** — let an AI assistant inspect what's cached and how much memory it uses without granting it backend access +- **Operations** — clear stale or all cached data through the same channel +- **Diagnostics** — backend health is one resource read away + +## Documentation + +Full documentation, configuration options, and samples are available on the [GitHub project page](https://github.com/Tharga/Cache). + +## License + +MIT diff --git a/Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj b/Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj new file mode 100644 index 0000000..c9b6671 --- /dev/null +++ b/Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj @@ -0,0 +1,47 @@ + + + + net8.0;net9.0;net10.0 + enable + 1.0.0 + Daniel Bohlin + Thargelion AB + Tharga Cache MCP + Exposes Tharga.Cache monitoring data (cache types, items, persistence health, fetch queue) and actions (clear all, clear stale) via MCP (Model Context Protocol). Plugs into Tharga.Mcp. + https://thargelion.se/wp-content/uploads/2025/11/Thargelion-Cache-Icon-150.png + True + https://github.com/Tharga/Cache + README.md + true + true + true + true + portable + true + false + + + + 1701;1702;CS1591;CS0809 + + + + + True + \ + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Tharga.Cache.sln b/Tharga.Cache.sln index 7f61ab9..74debe2 100644 --- a/Tharga.Cache.sln +++ b/Tharga.Cache.sln @@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "File", "File", "{09A0B60C-A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tharga.Cache.File.Tests", "Tharga.Cache.File.Tests\Tharga.Cache.File.Tests.csproj", "{05B3FF19-42EB-4EAE-BAD6-803B2EEDAA71}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tharga.Cache.Mcp", "Tharga.Cache.Mcp\Tharga.Cache.Mcp.csproj", "{08346EDF-D783-4FB0-8430-64618907B1C8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +198,18 @@ Global {05B3FF19-42EB-4EAE-BAD6-803B2EEDAA71}.Release|x64.Build.0 = Release|Any CPU {05B3FF19-42EB-4EAE-BAD6-803B2EEDAA71}.Release|x86.ActiveCfg = Release|Any CPU {05B3FF19-42EB-4EAE-BAD6-803B2EEDAA71}.Release|x86.Build.0 = Release|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Debug|x64.Build.0 = Debug|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Debug|x86.Build.0 = Debug|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Release|Any CPU.Build.0 = Release|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Release|x64.ActiveCfg = Release|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Release|x64.Build.0 = Release|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Release|x86.ActiveCfg = Release|Any CPU + {08346EDF-D783-4FB0-8430-64618907B1C8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/plan/feature.md b/plan/feature.md new file mode 100644 index 0000000..f524533 --- /dev/null +++ b/plan/feature.md @@ -0,0 +1,64 @@ +# Feature: cache-mcp + +## Goal +Add a new package `Tharga.Cache.Mcp` that exposes cache state via MCP, following the established `Tharga.Platform.Mcp` / `Tharga.MongoDB.Mcp` / `Tharga.Communication.Mcp` pattern. Consumers register with `builder.Services.AddThargaMcp(b => b.AddCache())`. + +## Originating request +`Tharga.Cache — MCP / MCP Provider for Cache monitoring` from "All products using Cache" (Quilt4Net Server, PlutusWave, Florida, Eplicta), priority Medium. + +## Decisions (open questions resolved) + +| Question | Decision | +|---|---| +| Method name | `AddCache()` on `IThargaMcpBuilder` — matches Platform/MongoDB/Communication pattern. The original request said `AddMcpCache()`, but established convention wins. | +| Per-key evict (`cache.evict`) | **Skip in v1.** A clean implementation would need a new method on `ICacheMonitor` (e.g. `EvictAsync(Type, Key)`); reflection via `IEternalCache.DropAsync(key)` is hacky. Add as a follow-up if a consumer asks. | +| Hit/miss counters | **Skip.** Today `CacheItemInfo` only tracks `AccessCount` (total reads). True hit/miss requires adding miss tracking to `CacheBase`/`CacheMonitor` — out of scope for the MCP package. Track as a follow-up if needed. | + +## Scope + +### New project: `Tharga.Cache.Mcp/` +- `Tharga.Cache.Mcp.csproj` — `net8.0;net9.0;net10.0`, package metadata matching the other Tharga.Cache packages, dependency on `Tharga.Mcp` v0.1.3, project reference to `Tharga.Cache`. +- `README.md` — concise NuGet sales pitch. +- `ThargaMcpBuilderExtensions.cs` — `AddCache()` extension on `IThargaMcpBuilder`. +- `CacheResourceProvider.cs` — read-only data, scope `McpScope.System`. +- `CacheToolProvider.cs` — actions, scope `McpScope.System`. + +### Resources + +| URI | Source | Payload | +|---|---|---| +| `cache://types` | `ICacheMonitor.GetInfos()` | per type: type name, persist type name (Memory/Redis/MongoDB/File), item count, total size, `StaleWhileRevalidate`, `ReturnDefaultOnFirstLoad` | +| `cache://items` | `ICacheMonitor.GetInfos()` flattened | per item: type, key, size, fresh span, expires, last accessed, access count, load duration, is stale | +| `cache://health` | `ICacheMonitor.GetHealthTypes()` | per persist type: name, success, message | +| `cache://queue` | `ICacheMonitor.GetFetchQueueCount()` | { queueDepth } | + +### Tools + +| Name | Action | +|---|---| +| `cache.clear_stale` | `ICacheMonitor.ClearStale()` | +| `cache.clear_all` | `ICacheMonitor.ClearAll()` | + +### Tests +Basic unit tests in a new `Tharga.Cache.Mcp.Tests` project (or possibly added to `Tharga.Cache.Tests`): +- Resource provider lists all 4 resource descriptors. +- Each resource read returns a non-empty payload against a populated mock `ICacheMonitor`. +- Tool provider lists both tools. +- Each tool invocation calls the corresponding monitor method. + +### Solution + CI +- Add `Tharga.Cache.Mcp.csproj` to `Tharga.Cache.sln`. +- Add a pack line to both stable and pre-release Pack steps in `.github/workflows/build.yml`. + +## Acceptance criteria +- `builder.Services.AddThargaMcp(b => b.AddCache())` registers without error. +- All four resources are discoverable and readable through the MCP endpoint. +- Both tools execute and report success. +- Existing tests still pass; the new test project passes. +- Package `Tharga.Cache.Mcp` is built by CI (stable on master, prerelease on PRs). + +## Done condition +User confirms the package works against an MCP client (or at minimum, the integration test passes). + +## Originating branch +develop diff --git a/plan/plan.md b/plan/plan.md new file mode 100644 index 0000000..cb23b0d --- /dev/null +++ b/plan/plan.md @@ -0,0 +1,10 @@ +# Plan: cache-mcp + +## Steps +- [x] 1. Create `Tharga.Cache.Mcp/` project skeleton (csproj + README) and add to solution — net8/9/10, Tharga.Mcp 0.1.3, ProjectReference to Tharga.Cache, builds cleanly +- [ ] 2. Implement `ThargaMcpBuilderExtensions.AddCache()` extension +- [ ] 3. Implement `CacheResourceProvider` with 4 resources (`cache://types`, `cache://items`, `cache://health`, `cache://queue`) +- [ ] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) +- [ ] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) +- [ ] 6. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` +- [ ] 7. Build, run full test suite, commit From 8905b01c194737c26fe7b30ce42457763abec05d Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 11:47:37 +0200 Subject: [PATCH 2/7] feat(mcp): implement Cache resource and tool providers - AddCache() extension on IThargaMcpBuilder registers both providers. - CacheResourceProvider exposes four System-scope resources: cache://types, cache://items, cache://health, cache://queue. - CacheToolProvider exposes two System-scope tools: cache.clear_stale and cache.clear_all. - Persist type names are surfaced with the leading 'I' stripped (Memory/Redis/MongoDB/File) to match the Blazor UI convention. --- Tharga.Cache.Mcp/CacheResourceProvider.cs | 163 ++++++++++++++++++ Tharga.Cache.Mcp/CacheToolProvider.cs | 95 ++++++++++ .../ThargaMcpBuilderExtensions.cs | 20 +++ plan/plan.md | 11 +- 4 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 Tharga.Cache.Mcp/CacheResourceProvider.cs create mode 100644 Tharga.Cache.Mcp/CacheToolProvider.cs create mode 100644 Tharga.Cache.Mcp/ThargaMcpBuilderExtensions.cs diff --git a/Tharga.Cache.Mcp/CacheResourceProvider.cs b/Tharga.Cache.Mcp/CacheResourceProvider.cs new file mode 100644 index 0000000..5c908f9 --- /dev/null +++ b/Tharga.Cache.Mcp/CacheResourceProvider.cs @@ -0,0 +1,163 @@ +using System.Text.Json; +using Tharga.Mcp; + +namespace Tharga.Cache.Mcp; + +/// +/// Exposes Tharga.Cache monitoring data as MCP resources on the System scope. +/// +public sealed class CacheResourceProvider : IMcpResourceProvider +{ + internal const string TypesUri = "cache://types"; + internal const string ItemsUri = "cache://items"; + internal const string HealthUri = "cache://health"; + internal const string QueueUri = "cache://queue"; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + }; + + private readonly ICacheMonitor _monitor; + + public CacheResourceProvider(ICacheMonitor monitor) + { + _monitor = monitor; + } + + public McpScope Scope => McpScope.System; + + public Task> ListResourcesAsync(IMcpContext context, CancellationToken cancellationToken) + { + IReadOnlyList resources = + [ + new McpResourceDescriptor + { + Uri = TypesUri, + Name = "Cache Types", + Description = "Registered cache types with persistence backend (Memory/Redis/MongoDB/File), item count, total size, and config flags.", + MimeType = "application/json", + }, + new McpResourceDescriptor + { + Uri = ItemsUri, + Name = "Cache Items", + Description = "Flat list of cached items: type, key, size, fresh span, expires, last accessed, access count, load duration, stale.", + MimeType = "application/json", + }, + new McpResourceDescriptor + { + Uri = HealthUri, + Name = "Cache Persistence Health", + Description = "Connectivity status of each persistence backend.", + MimeType = "application/json", + }, + new McpResourceDescriptor + { + Uri = QueueUri, + Name = "Cache Fetch Queue", + Description = "Current depth of the in-flight fetch queue.", + MimeType = "application/json", + }, + ]; + return Task.FromResult(resources); + } + + public async Task ReadResourceAsync(string uri, IMcpContext context, CancellationToken cancellationToken) + { + return uri switch + { + TypesUri => BuildTypes(), + ItemsUri => BuildItems(), + HealthUri => await BuildHealthAsync(), + QueueUri => BuildQueue(), + _ => new McpResourceContent { Uri = uri, Text = $"Unknown resource: {uri}" }, + }; + } + + private McpResourceContent BuildTypes() + { + var types = _monitor.GetInfos().Select(info => new + { + type = info.Type.FullName, + persistType = FormatPersistType(info.PersistType), + count = info.Items.Count, + totalSize = info.Items.Sum(x => x.Value.Size), + staleWhileRevalidate = info.StaleWhileRevalidate, + returnDefaultOnFirstLoad = info.ReturnDefaultOnFirstLoad, + }).ToArray(); + + return new McpResourceContent + { + Uri = TypesUri, + MimeType = "application/json", + Text = JsonSerializer.Serialize(new { types }, JsonOptions), + }; + } + + private McpResourceContent BuildItems() + { + var items = _monitor.GetInfos().SelectMany(info => info.Items.Select(item => new + { + type = info.Type.FullName, + persistType = FormatPersistType(info.PersistType), + key = item.Key, + size = item.Value.Size, + freshSpan = item.Value.FreshSpan, + createTime = item.Value.CreateTime, + updateTime = item.Value.UpdateTime, + expireTime = item.Value.ExpireTime, + lastAccessTime = item.Value.LastAccessTime, + accessCount = item.Value.AccessCount, + loadDuration = item.Value.LoadDuration, + isStale = item.Value.IsStale, + })).ToArray(); + + return new McpResourceContent + { + Uri = ItemsUri, + MimeType = "application/json", + Text = JsonSerializer.Serialize(new { items }, JsonOptions), + }; + } + + private async Task BuildHealthAsync() + { + var results = new List(); + foreach (var healthType in _monitor.GetHealthTypes()) + { + var health = await healthType.GetHealthAsync(); + results.Add(new + { + type = healthType.Type, + success = health.Success, + message = health.Message, + }); + } + + return new McpResourceContent + { + Uri = HealthUri, + MimeType = "application/json", + Text = JsonSerializer.Serialize(new { health = results }, JsonOptions), + }; + } + + private McpResourceContent BuildQueue() + { + var payload = new { queueDepth = _monitor.GetFetchQueueCount() }; + return new McpResourceContent + { + Uri = QueueUri, + MimeType = "application/json", + Text = JsonSerializer.Serialize(payload, JsonOptions), + }; + } + + private static string FormatPersistType(Type persistType) + { + if (persistType == null) return null; + var name = persistType.Name; + return name.StartsWith("I") && name.Length > 1 && char.IsUpper(name[1]) ? name.Substring(1) : name; + } +} diff --git a/Tharga.Cache.Mcp/CacheToolProvider.cs b/Tharga.Cache.Mcp/CacheToolProvider.cs new file mode 100644 index 0000000..837e010 --- /dev/null +++ b/Tharga.Cache.Mcp/CacheToolProvider.cs @@ -0,0 +1,95 @@ +using System.Text.Json; +using Tharga.Mcp; + +namespace Tharga.Cache.Mcp; + +/// +/// Exposes Tharga.Cache maintenance actions as MCP tools on the System scope. +/// +public sealed class CacheToolProvider : IMcpToolProvider +{ + internal const string ClearStaleToolName = "cache.clear_stale"; + internal const string ClearAllToolName = "cache.clear_all"; + + private static readonly JsonElement EmptyArgsSchema = JsonSerializer.Deserialize(""" + { + "type": "object", + "properties": {} + } + """); + + private readonly ICacheMonitor _monitor; + + public CacheToolProvider(ICacheMonitor monitor) + { + _monitor = monitor; + } + + public McpScope Scope => McpScope.System; + + public Task> ListToolsAsync(IMcpContext context, CancellationToken cancellationToken) + { + IReadOnlyList tools = + [ + new McpToolDescriptor + { + Name = ClearStaleToolName, + Description = "Evict all stale cache items across all registered cache types.", + InputSchema = EmptyArgsSchema, + }, + new McpToolDescriptor + { + Name = ClearAllToolName, + Description = "Evict every item from every registered cache type.", + InputSchema = EmptyArgsSchema, + }, + ]; + return Task.FromResult(tools); + } + + public Task CallToolAsync(string toolName, JsonElement arguments, IMcpContext context, CancellationToken cancellationToken) + { + try + { + return Task.FromResult(toolName switch + { + ClearStaleToolName => ClearStale(), + ClearAllToolName => ClearAll(), + _ => Error($"Unknown tool: {toolName}"), + }); + } + catch (Exception e) + { + return Task.FromResult(Error(e.Message)); + } + } + + private McpToolResult ClearStale() + { + _monitor.ClearStale(); + return Ok(new { cleared = "stale" }); + } + + private McpToolResult ClearAll() + { + _monitor.ClearAll(); + return Ok(new { cleared = "all" }); + } + + private static McpToolResult Ok(object payload) + { + return new McpToolResult + { + Content = [new McpContent { Type = "text", Text = JsonSerializer.Serialize(payload) }], + }; + } + + private static McpToolResult Error(string message) + { + return new McpToolResult + { + IsError = true, + Content = [new McpContent { Type = "text", Text = message }], + }; + } +} diff --git a/Tharga.Cache.Mcp/ThargaMcpBuilderExtensions.cs b/Tharga.Cache.Mcp/ThargaMcpBuilderExtensions.cs new file mode 100644 index 0000000..555843e --- /dev/null +++ b/Tharga.Cache.Mcp/ThargaMcpBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Tharga.Mcp; + +namespace Tharga.Cache.Mcp; + +/// +/// Extension methods for that register Tharga.Cache MCP providers. +/// +public static class ThargaMcpBuilderExtensions +{ + /// + /// Registers and , exposing + /// cache types, items, persistence health, fetch queue depth, and clear actions on the System scope. + /// + public static IThargaMcpBuilder AddCache(this IThargaMcpBuilder builder) + { + builder.AddResourceProvider(); + builder.AddToolProvider(); + return builder; + } +} diff --git a/plan/plan.md b/plan/plan.md index cb23b0d..1399330 100644 --- a/plan/plan.md +++ b/plan/plan.md @@ -2,9 +2,10 @@ ## Steps - [x] 1. Create `Tharga.Cache.Mcp/` project skeleton (csproj + README) and add to solution — net8/9/10, Tharga.Mcp 0.1.3, ProjectReference to Tharga.Cache, builds cleanly -- [ ] 2. Implement `ThargaMcpBuilderExtensions.AddCache()` extension -- [ ] 3. Implement `CacheResourceProvider` with 4 resources (`cache://types`, `cache://items`, `cache://health`, `cache://queue`) -- [ ] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) +- [x] 2. Implement `ThargaMcpBuilderExtensions.AddCache()` extension +- [x] 3. Implement `CacheResourceProvider` with 4 resources (`cache://types`, `cache://items`, `cache://health`, `cache://queue`) +- [x] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) - [ ] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) -- [ ] 6. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` -- [ ] 7. Build, run full test suite, commit +- [ ] 6. Wire MCP into the `Tharga.Cache.WebApi` sample (`AddThargaMcp(b => b.AddCache())` + `app.UseThargaMcp()`) +- [ ] 7. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` +- [ ] 8. Build, run full test suite, commit From 83eb348659016e25da1de52a2735c551a260660e Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 11:54:35 +0200 Subject: [PATCH 3/7] test(mcp): unit tests for CacheResourceProvider and CacheToolProvider 10 tests covering: - Resource provider: lists 4 descriptors; each resource read returns well-formed JSON (types, items, health, queue); unknown URI returns explanatory text. - Tool provider: lists 2 tools; clear_stale and clear_all delegate to ICacheMonitor; unknown tool returns IsError=true. Tharga.Cache.Tests now references Tharga.Cache.Mcp directly to keep the MCP tests alongside the rest of the unit tests. --- .../CacheResourceProviderTests.cs | 139 ++++++++++++++++++ Tharga.Cache.Tests/CacheToolProviderTests.cs | 71 +++++++++ Tharga.Cache.Tests/Tharga.Cache.Tests.csproj | 1 + plan/plan.md | 2 +- 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 Tharga.Cache.Tests/CacheResourceProviderTests.cs create mode 100644 Tharga.Cache.Tests/CacheToolProviderTests.cs diff --git a/Tharga.Cache.Tests/CacheResourceProviderTests.cs b/Tharga.Cache.Tests/CacheResourceProviderTests.cs new file mode 100644 index 0000000..ce526f7 --- /dev/null +++ b/Tharga.Cache.Tests/CacheResourceProviderTests.cs @@ -0,0 +1,139 @@ +using System.Text.Json; +using FluentAssertions; +using Moq; +using Tharga.Cache.Core; +using Tharga.Cache.Mcp; +using Tharga.Cache.Persist; +using Xunit; + +namespace Tharga.Cache.Tests; + +public class CacheResourceProviderTests +{ + [Fact] + public async Task ListResources_returns_all_four_descriptors() + { + //Arrange + var monitor = Mock.Of(); + var sut = new CacheResourceProvider(monitor); + + //Act + var resources = await sut.ListResourcesAsync(null, default); + + //Assert + resources.Should().HaveCount(4); + resources.Select(r => r.Uri).Should().BeEquivalentTo( + "cache://types", + "cache://items", + "cache://health", + "cache://queue"); + } + + [Fact] + public async Task Read_types_includes_registered_types() + { + //Arrange + var (monitor, _) = BuildPopulatedMonitor(); + var sut = new CacheResourceProvider(monitor); + + //Act + var content = await sut.ReadResourceAsync("cache://types", null, default); + + //Assert + content.Uri.Should().Be("cache://types"); + content.MimeType.Should().Be("application/json"); + var json = JsonDocument.Parse(content.Text); + var types = json.RootElement.GetProperty("types"); + types.GetArrayLength().Should().Be(1); + types[0].GetProperty("type").GetString().Should().Contain("String"); + types[0].GetProperty("persistType").GetString().Should().Be("Memory"); + types[0].GetProperty("count").GetInt32().Should().Be(1); + } + + [Fact] + public async Task Read_items_returns_individual_entries() + { + //Arrange + var (monitor, _) = BuildPopulatedMonitor(); + var sut = new CacheResourceProvider(monitor); + + //Act + var content = await sut.ReadResourceAsync("cache://items", null, default); + + //Assert + var json = JsonDocument.Parse(content.Text); + var items = json.RootElement.GetProperty("items"); + items.GetArrayLength().Should().Be(1); + items[0].GetProperty("key").GetString().Should().Contain("ItemKey"); + items[0].GetProperty("persistType").GetString().Should().Be("Memory"); + } + + [Fact] + public async Task Read_health_returns_backend_status() + { + //Arrange + var (monitor, _) = BuildPopulatedMonitor(); + var sut = new CacheResourceProvider(monitor); + + //Act + var content = await sut.ReadResourceAsync("cache://health", null, default); + + //Assert + var json = JsonDocument.Parse(content.Text); + var health = json.RootElement.GetProperty("health"); + health.GetArrayLength().Should().Be(1); + health[0].GetProperty("type").GetString().Should().Be("IMemory"); + health[0].GetProperty("success").GetBoolean().Should().BeTrue(); + } + + [Fact] + public async Task Read_queue_returns_queue_depth() + { + //Arrange + var (monitor, _) = BuildPopulatedMonitor(); + var sut = new CacheResourceProvider(monitor); + + //Act + var content = await sut.ReadResourceAsync("cache://queue", null, default); + + //Assert + var json = JsonDocument.Parse(content.Text); + json.RootElement.GetProperty("queueDepth").GetInt32().Should().Be(0); + } + + [Fact] + public async Task Read_unknown_uri_returns_explanatory_text() + { + //Arrange + var monitor = Mock.Of(); + var sut = new CacheResourceProvider(monitor); + + //Act + var content = await sut.ReadResourceAsync("cache://unknown", null, default); + + //Assert + content.Text.Should().Contain("Unknown resource"); + } + + private static (ICacheMonitor Monitor, ICache Cache) BuildPopulatedMonitor() + { + // Build a real CacheMonitor with one item so the resource payloads have substance. + var options = new CacheOptions + { + Default = new CacheTypeOptions { DefaultFreshSpan = TimeSpan.FromMinutes(5) }, + }; + options.RegisterType(s => s.DefaultFreshSpan = TimeSpan.FromMinutes(5)); + + var persistLoader = new Mock(MockBehavior.Strict); + var monitor = new Tharga.Cache.Core.CacheMonitor(persistLoader.Object, options); + var memory = new Memory(monitor); + persistLoader.Setup(x => x.GetPersist(It.IsAny())).Returns(memory); + var fetchQueue = new Tharga.Cache.Core.FetchQueue(monitor, options, null); + var cache = new Tharga.Cache.Core.TimeToLiveCache(monitor, persistLoader.Object, fetchQueue, options); + + // Populate with one item. + cache.GetAsync("ItemKey", () => Task.FromResult("value")).GetAwaiter().GetResult(); + + return (monitor, cache); + } +} diff --git a/Tharga.Cache.Tests/CacheToolProviderTests.cs b/Tharga.Cache.Tests/CacheToolProviderTests.cs new file mode 100644 index 0000000..ce94cec --- /dev/null +++ b/Tharga.Cache.Tests/CacheToolProviderTests.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using FluentAssertions; +using Moq; +using Tharga.Cache.Mcp; +using Xunit; + +namespace Tharga.Cache.Tests; + +public class CacheToolProviderTests +{ + [Fact] + public async Task ListTools_returns_clear_stale_and_clear_all() + { + //Arrange + var monitor = Mock.Of(); + var sut = new CacheToolProvider(monitor); + + //Act + var tools = await sut.ListToolsAsync(null, default); + + //Assert + tools.Select(t => t.Name).Should().BeEquivalentTo("cache.clear_stale", "cache.clear_all"); + } + + [Fact] + public async Task Call_clear_stale_invokes_monitor_ClearStale() + { + //Arrange + var monitor = new Mock(); + var sut = new CacheToolProvider(monitor.Object); + + //Act + var result = await sut.CallToolAsync("cache.clear_stale", default, null, default); + + //Assert + result.IsError.Should().BeFalse(); + monitor.Verify(x => x.ClearStale(), Times.Once); + monitor.Verify(x => x.ClearAll(), Times.Never); + } + + [Fact] + public async Task Call_clear_all_invokes_monitor_ClearAll() + { + //Arrange + var monitor = new Mock(); + var sut = new CacheToolProvider(monitor.Object); + + //Act + var result = await sut.CallToolAsync("cache.clear_all", default, null, default); + + //Assert + result.IsError.Should().BeFalse(); + monitor.Verify(x => x.ClearAll(), Times.Once); + monitor.Verify(x => x.ClearStale(), Times.Never); + } + + [Fact] + public async Task Call_unknown_tool_returns_error_result() + { + //Arrange + var monitor = Mock.Of(); + var sut = new CacheToolProvider(monitor); + + //Act + var result = await sut.CallToolAsync("cache.does_not_exist", default, null, default); + + //Assert + result.IsError.Should().BeTrue(); + result.Content.Single().Text.Should().Contain("Unknown tool"); + } +} diff --git a/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj b/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj index a7001d3..252d0d9 100644 --- a/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj +++ b/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj @@ -34,6 +34,7 @@ + \ No newline at end of file diff --git a/plan/plan.md b/plan/plan.md index 1399330..94e9571 100644 --- a/plan/plan.md +++ b/plan/plan.md @@ -5,7 +5,7 @@ - [x] 2. Implement `ThargaMcpBuilderExtensions.AddCache()` extension - [x] 3. Implement `CacheResourceProvider` with 4 resources (`cache://types`, `cache://items`, `cache://health`, `cache://queue`) - [x] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) -- [ ] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) +- [x] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) — 10 tests, all green - [ ] 6. Wire MCP into the `Tharga.Cache.WebApi` sample (`AddThargaMcp(b => b.AddCache())` + `app.UseThargaMcp()`) - [ ] 7. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` - [ ] 8. Build, run full test suite, commit From e3a34f1dc1d8bcfa7de3ecc59148f15e36fc9735 Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 11:57:10 +0200 Subject: [PATCH 4/7] feat(mcp): wire Tharga.Cache.Mcp into the WebApi sample The sample now exposes the cache via MCP at /mcp: - AddThargaMcp(mcp => { mcp.Options.RequireAuth = false; mcp.AddCache(); }) - app.UseThargaMcp() RequireAuth is set to false so the sample is reachable without auth middleware. Production consumers leave it true and wire their own authorization. --- Sample/Tharga.Cache.WebApi/Program.cs | 11 +++++++++++ Sample/Tharga.Cache.WebApi/Tharga.Cache.WebApi.csproj | 1 + plan/plan.md | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Sample/Tharga.Cache.WebApi/Program.cs b/Sample/Tharga.Cache.WebApi/Program.cs index 6781ce7..e05c7de 100644 --- a/Sample/Tharga.Cache.WebApi/Program.cs +++ b/Sample/Tharga.Cache.WebApi/Program.cs @@ -1,7 +1,9 @@ using Tharga.Cache; +using Tharga.Cache.Mcp; using Tharga.Cache.MongoDB; using Tharga.Cache.Persist; using Tharga.Cache.Web; +using Tharga.Mcp; using Tharga.MongoDB; var builder = WebApplication.CreateBuilder(args); @@ -38,6 +40,13 @@ builder.Services.AddHostedService(); +builder.Services.AddThargaMcp(mcp => +{ + // Sample runs without authentication — production consumers leave RequireAuth=true and wire auth middleware. + mcp.Options.RequireAuth = false; + mcp.AddCache(); +}); + //builder.Services.AddQuilt4NetApi(o => //{ // //o.ShowInOpenApi = !Debugger.IsAttached; @@ -58,6 +67,8 @@ app.MapControllers(); +app.UseThargaMcp(); + //app.UseQuilt4NetApi(); app.Run(); diff --git a/Sample/Tharga.Cache.WebApi/Tharga.Cache.WebApi.csproj b/Sample/Tharga.Cache.WebApi/Tharga.Cache.WebApi.csproj index 6f389a9..b55f84c 100644 --- a/Sample/Tharga.Cache.WebApi/Tharga.Cache.WebApi.csproj +++ b/Sample/Tharga.Cache.WebApi/Tharga.Cache.WebApi.csproj @@ -11,6 +11,7 @@ + diff --git a/plan/plan.md b/plan/plan.md index 94e9571..755cf80 100644 --- a/plan/plan.md +++ b/plan/plan.md @@ -6,6 +6,6 @@ - [x] 3. Implement `CacheResourceProvider` with 4 resources (`cache://types`, `cache://items`, `cache://health`, `cache://queue`) - [x] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) - [x] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) — 10 tests, all green -- [ ] 6. Wire MCP into the `Tharga.Cache.WebApi` sample (`AddThargaMcp(b => b.AddCache())` + `app.UseThargaMcp()`) +- [x] 6. Wire MCP into the `Tharga.Cache.WebApi` sample (`AddThargaMcp(b => b.AddCache())` + `app.UseThargaMcp()`); RequireAuth=false for the sample - [ ] 7. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` - [ ] 8. Build, run full test suite, commit From 76fcf9dafb08f7d7f30a517bda37a6297dca019b Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 11:58:51 +0200 Subject: [PATCH 5/7] ci(mcp): include Tharga.Cache.Mcp in stable + prerelease Pack steps --- .github/workflows/build.yml | 2 ++ plan/plan.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 564892e..1ca8ebc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,6 +103,7 @@ jobs: dotnet pack Tharga.Cache.MongoDB/Tharga.Cache.MongoDB.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }} dotnet pack Tharga.Cache.File/Tharga.Cache.File.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }} dotnet pack Tharga.Cache.Blazor/Tharga.Cache.Blazor.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }} - name: Pack (pre-release — pull request) if: github.event_name == 'pull_request' @@ -112,6 +113,7 @@ jobs: dotnet pack Tharga.Cache.MongoDB/Tharga.Cache.MongoDB.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }} dotnet pack Tharga.Cache.File/Tharga.Cache.File.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }} dotnet pack Tharga.Cache.Blazor/Tharga.Cache.Blazor.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }} + dotnet pack Tharga.Cache.Mcp/Tharga.Cache.Mcp.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }} - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/plan/plan.md b/plan/plan.md index 755cf80..e8129ce 100644 --- a/plan/plan.md +++ b/plan/plan.md @@ -7,5 +7,5 @@ - [x] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) - [x] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) — 10 tests, all green - [x] 6. Wire MCP into the `Tharga.Cache.WebApi` sample (`AddThargaMcp(b => b.AddCache())` + `app.UseThargaMcp()`); RequireAuth=false for the sample -- [ ] 7. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` +- [x] 7. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` - [ ] 8. Build, run full test suite, commit From bc5d8fac30eb2338bf24a3ba3f4e056b05bb18e0 Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 12:00:32 +0200 Subject: [PATCH 6/7] feat: cache-mcp complete --- plan/feature.md => .claude/features-done/cache-mcp.md | 0 plan/plan.md | 11 ----------- 2 files changed, 11 deletions(-) rename plan/feature.md => .claude/features-done/cache-mcp.md (100%) delete mode 100644 plan/plan.md diff --git a/plan/feature.md b/.claude/features-done/cache-mcp.md similarity index 100% rename from plan/feature.md rename to .claude/features-done/cache-mcp.md diff --git a/plan/plan.md b/plan/plan.md deleted file mode 100644 index e8129ce..0000000 --- a/plan/plan.md +++ /dev/null @@ -1,11 +0,0 @@ -# Plan: cache-mcp - -## Steps -- [x] 1. Create `Tharga.Cache.Mcp/` project skeleton (csproj + README) and add to solution — net8/9/10, Tharga.Mcp 0.1.3, ProjectReference to Tharga.Cache, builds cleanly -- [x] 2. Implement `ThargaMcpBuilderExtensions.AddCache()` extension -- [x] 3. Implement `CacheResourceProvider` with 4 resources (`cache://types`, `cache://items`, `cache://health`, `cache://queue`) -- [x] 4. Implement `CacheToolProvider` with 2 tools (`cache.clear_stale`, `cache.clear_all`) -- [x] 5. Add unit tests (resource list/read, tool list/invoke against mock `ICacheMonitor`) — 10 tests, all green -- [x] 6. Wire MCP into the `Tharga.Cache.WebApi` sample (`AddThargaMcp(b => b.AddCache())` + `app.UseThargaMcp()`); RequireAuth=false for the sample -- [x] 7. Add `Tharga.Cache.Mcp.csproj` to both Pack steps in `.github/workflows/build.yml` -- [ ] 8. Build, run full test suite, commit From b4dbc969e78bace8e11ad650c512b4462990030c Mon Sep 17 00:00:00 2001 From: Daniel Bohlin Date: Sun, 3 May 2026 12:03:17 +0200 Subject: [PATCH 7/7] chore: move archived features to external Plan directory Per the updated shared-instructions, completed features live in the project's Plan directory ($DOC_ROOT/Tharga/plans/Toolkit/Cache/done/), not inside the repo's .claude/. Moved all 9 archived feature files out and deleted the now-empty .claude/features-done/ folder. --- .claude/features-done/blazor-ui-overhaul.md | 25 -------- .claude/features-done/cache-load-time.md | 26 -------- .claude/features-done/cache-mcp.md | 64 ------------------- .claude/features-done/gettypes-safe.md | 19 ------ .claude/features-done/github-actions.md | 20 ------ .claude/features-done/idempotent-addcache.md | 22 ------- .claude/features-done/mongodb-clear-cache.md | 18 ------ .../features-done/mongodb-default-config.md | 17 ----- .../features-done/monitor-track-persisted.md | 21 ------ 9 files changed, 232 deletions(-) delete mode 100644 .claude/features-done/blazor-ui-overhaul.md delete mode 100644 .claude/features-done/cache-load-time.md delete mode 100644 .claude/features-done/cache-mcp.md delete mode 100644 .claude/features-done/gettypes-safe.md delete mode 100644 .claude/features-done/github-actions.md delete mode 100644 .claude/features-done/idempotent-addcache.md delete mode 100644 .claude/features-done/mongodb-clear-cache.md delete mode 100644 .claude/features-done/mongodb-default-config.md delete mode 100644 .claude/features-done/monitor-track-persisted.md diff --git a/.claude/features-done/blazor-ui-overhaul.md b/.claude/features-done/blazor-ui-overhaul.md deleted file mode 100644 index 0a634d4..0000000 --- a/.claude/features-done/blazor-ui-overhaul.md +++ /dev/null @@ -1,25 +0,0 @@ -# Feature: blazor-ui-overhaul - -## Goal -Overhaul the Blazor monitoring UI: slim ListView columns, move details to a dialog showing all metadata + JSON content, show persist type per cache type, and make Clear Cache refresh the ListView. - -## Scope -- `CacheTypeInfo` — add `PersistType` field -- `CacheMonitor.Set/Track` — populate `PersistType` -- `ListView.razor` — slim columns to Key, Expires, Size, Load Time, Stale; add persist type to the type-level grid; add chevron button per row; subscribe to monitor events for live refresh -- `SummaryView.razor` — subscribe to monitor events for live refresh -- New `DetailView.razor` — dialog component with all metadata + JSON content via `PeekAsync` + reflection + `CopyButton` - -## Acceptance Criteria -- ListView shows slim columns with chevron to open detail dialog -- Detail dialog shows all metadata (Created, Updated, Fresh Span, Last Accessed, Access Count) plus cached data as JSON -- Copy JSON button works -- Persist type column appears on the type-level grid -- Clear Cache button refreshes the ListView without reload -- All tests still pass - -## Done Condition -User confirms the UI works as expected. - -## Originating Branch -develop diff --git a/.claude/features-done/cache-load-time.md b/.claude/features-done/cache-load-time.md deleted file mode 100644 index 1ab7488..0000000 --- a/.claude/features-done/cache-load-time.md +++ /dev/null @@ -1,26 +0,0 @@ -# Feature: cache-load-time - -## Goal -Track how long the fetch delegate takes and display it in the Blazor monitoring UI. - -## Scope -- `CacheItem` — add `LoadDuration` field -- `CacheItemBuilder` — accept and store `LoadDuration` -- `FetchQueue` — measure fetch time with `Stopwatch` -- `CacheItemInfo` — add `LoadDuration` property, remove TODO comment -- `CacheMonitor.Set/Track` — pass `LoadDuration` to `CacheItemInfo` -- `IManagedCacheMonitor` — update `Set`/`Track` signatures -- `CacheBase` — pass `LoadDuration` through `OnSetAsync` and `TrackIfNeeded` -- `ListView.razor` — add "Load Time" column - -## Acceptance Criteria -- `CacheItemInfo.LoadDuration` reflects the actual fetch delegate execution time -- Blazor ListView shows load duration for each cached item -- Items loaded from persistence (Track path) show null load duration -- All existing tests still pass - -## Done Condition -User confirms the feature is satisfactory. - -## Originating Branch -develop diff --git a/.claude/features-done/cache-mcp.md b/.claude/features-done/cache-mcp.md deleted file mode 100644 index f524533..0000000 --- a/.claude/features-done/cache-mcp.md +++ /dev/null @@ -1,64 +0,0 @@ -# Feature: cache-mcp - -## Goal -Add a new package `Tharga.Cache.Mcp` that exposes cache state via MCP, following the established `Tharga.Platform.Mcp` / `Tharga.MongoDB.Mcp` / `Tharga.Communication.Mcp` pattern. Consumers register with `builder.Services.AddThargaMcp(b => b.AddCache())`. - -## Originating request -`Tharga.Cache — MCP / MCP Provider for Cache monitoring` from "All products using Cache" (Quilt4Net Server, PlutusWave, Florida, Eplicta), priority Medium. - -## Decisions (open questions resolved) - -| Question | Decision | -|---|---| -| Method name | `AddCache()` on `IThargaMcpBuilder` — matches Platform/MongoDB/Communication pattern. The original request said `AddMcpCache()`, but established convention wins. | -| Per-key evict (`cache.evict`) | **Skip in v1.** A clean implementation would need a new method on `ICacheMonitor` (e.g. `EvictAsync(Type, Key)`); reflection via `IEternalCache.DropAsync(key)` is hacky. Add as a follow-up if a consumer asks. | -| Hit/miss counters | **Skip.** Today `CacheItemInfo` only tracks `AccessCount` (total reads). True hit/miss requires adding miss tracking to `CacheBase`/`CacheMonitor` — out of scope for the MCP package. Track as a follow-up if needed. | - -## Scope - -### New project: `Tharga.Cache.Mcp/` -- `Tharga.Cache.Mcp.csproj` — `net8.0;net9.0;net10.0`, package metadata matching the other Tharga.Cache packages, dependency on `Tharga.Mcp` v0.1.3, project reference to `Tharga.Cache`. -- `README.md` — concise NuGet sales pitch. -- `ThargaMcpBuilderExtensions.cs` — `AddCache()` extension on `IThargaMcpBuilder`. -- `CacheResourceProvider.cs` — read-only data, scope `McpScope.System`. -- `CacheToolProvider.cs` — actions, scope `McpScope.System`. - -### Resources - -| URI | Source | Payload | -|---|---|---| -| `cache://types` | `ICacheMonitor.GetInfos()` | per type: type name, persist type name (Memory/Redis/MongoDB/File), item count, total size, `StaleWhileRevalidate`, `ReturnDefaultOnFirstLoad` | -| `cache://items` | `ICacheMonitor.GetInfos()` flattened | per item: type, key, size, fresh span, expires, last accessed, access count, load duration, is stale | -| `cache://health` | `ICacheMonitor.GetHealthTypes()` | per persist type: name, success, message | -| `cache://queue` | `ICacheMonitor.GetFetchQueueCount()` | { queueDepth } | - -### Tools - -| Name | Action | -|---|---| -| `cache.clear_stale` | `ICacheMonitor.ClearStale()` | -| `cache.clear_all` | `ICacheMonitor.ClearAll()` | - -### Tests -Basic unit tests in a new `Tharga.Cache.Mcp.Tests` project (or possibly added to `Tharga.Cache.Tests`): -- Resource provider lists all 4 resource descriptors. -- Each resource read returns a non-empty payload against a populated mock `ICacheMonitor`. -- Tool provider lists both tools. -- Each tool invocation calls the corresponding monitor method. - -### Solution + CI -- Add `Tharga.Cache.Mcp.csproj` to `Tharga.Cache.sln`. -- Add a pack line to both stable and pre-release Pack steps in `.github/workflows/build.yml`. - -## Acceptance criteria -- `builder.Services.AddThargaMcp(b => b.AddCache())` registers without error. -- All four resources are discoverable and readable through the MCP endpoint. -- Both tools execute and report success. -- Existing tests still pass; the new test project passes. -- Package `Tharga.Cache.Mcp` is built by CI (stable on master, prerelease on PRs). - -## Done condition -User confirms the package works against an MCP client (or at minimum, the integration test passes). - -## Originating branch -develop diff --git a/.claude/features-done/gettypes-safe.md b/.claude/features-done/gettypes-safe.md deleted file mode 100644 index dc8f803..0000000 --- a/.claude/features-done/gettypes-safe.md +++ /dev/null @@ -1,19 +0,0 @@ -# Feature: gettypes-safe - -## Goal -Make `AddCache()` resilient to `ReflectionTypeLoadException` thrown by `Assembly.GetTypes()` when an unrelated assembly has unresolvable metadata references. PlutusWave hit a fatal startup crash from a Quilt4Net/ApplicationInsights mismatch in a third-party dll. - -## Scope -- `Tharga.Cache/CacheRegistrationExtensions.cs` — add `GetTypesSafe(assembly, logger)` helper, replace `assembly.GetTypes()` calls in `RegisterIPersistFromAssembly` and `InvokeAllPersistRegistrations`. -- Add a test verifying `AddCache` does not throw when a loaded assembly has an unresolvable type. - -## Acceptance Criteria -- `AddCache` completes when an in-process assembly throws `ReflectionTypeLoadException` from `GetTypes()`. -- A warning is logged (per affected assembly) so the root cause isn't silently swallowed. -- Existing tests still pass. - -## Done Condition -User confirms the fix is satisfactory. - -## Originating Branch -develop diff --git a/.claude/features-done/github-actions.md b/.claude/features-done/github-actions.md deleted file mode 100644 index d7480c2..0000000 --- a/.claude/features-done/github-actions.md +++ /dev/null @@ -1,20 +0,0 @@ -# Feature: github-actions - -## Goal -Add GitHub Actions CI/CD workflow matching the Platform pattern, with a unified publish job that picks release vs prerelease environment based on branch. - -## Scope -- `.github/workflows/build.yml` — build, security, publish jobs - -## Acceptance Criteria -- Build job: restore, build, warning check (threshold 10), test with coverage, codecov, compute version, pack 5 NuGet packages -- Security job: CodeQL analysis -- Publish job: single job with dynamic environment (release on master push, prerelease on PR), push to NuGet, create GitHub release -- Version: `MAJOR_MINOR: '0.4'`, auto-incrementing patch from git tags -- .NET SDKs: 8.0, 9.0, 10.0 - -## Done Condition -User confirms the workflow is satisfactory. - -## Originating Branch -develop diff --git a/.claude/features-done/idempotent-addcache.md b/.claude/features-done/idempotent-addcache.md deleted file mode 100644 index cd2bfb6..0000000 --- a/.claude/features-done/idempotent-addcache.md +++ /dev/null @@ -1,22 +0,0 @@ -# Feature: idempotent-addcache - -## Goal -Make `AddCache()` safe to call multiple times, merging type registrations instead of throwing. - -## Scope -- `CacheRegistrationExtensions.AddCache` — guard against duplicate DI registrations -- `CacheRegistrationExtensions.AppendPreviousRegistrations` — skip duplicate types instead of throwing -- `CacheOptions.AddType` — skip duplicate types instead of throwing -- Tests verifying idempotent behavior - -## Acceptance Criteria -- Calling `AddCache()` twice with the same type registration does not throw -- Calling `AddCache()` twice with different types merges them -- First registration wins when same type is registered with different options -- All existing tests still pass - -## Done Condition -User confirms the fix is satisfactory. - -## Originating Branch -develop diff --git a/.claude/features-done/mongodb-clear-cache.md b/.claude/features-done/mongodb-clear-cache.md deleted file mode 100644 index b428fc6..0000000 --- a/.claude/features-done/mongodb-clear-cache.md +++ /dev/null @@ -1,18 +0,0 @@ -# Feature: mongodb-clear-cache - -## Goal -Fix ClearCache/ClearStale for MongoDB-backed cache types by subscribing to `RequestEvictEvent`. - -## Scope -- `MongoDB.cs` constructor — add `IManagedCacheMonitor` parameter and `RequestEvictEvent` subscription - -## Acceptance Criteria -- `ICacheMonitor.ClearAll()` removes MongoDB-persisted items -- `ICacheMonitor.ClearStale()` removes stale MongoDB-persisted items -- Existing tests still pass - -## Done Condition -User confirms the fix is satisfactory. - -## Originating Branch -develop diff --git a/.claude/features-done/mongodb-default-config.md b/.claude/features-done/mongodb-default-config.md deleted file mode 100644 index af638d7..0000000 --- a/.claude/features-done/mongodb-default-config.md +++ /dev/null @@ -1,17 +0,0 @@ -# Feature: mongodb-default-config - -## Goal -Fix MongoDB setup when not using "default" config by giving `ConfigurationName` a sensible default. - -## Scope -- `MongoDBCacheOptions.ConfigurationName` — set default to `"Default"` - -## Acceptance Criteria -- `AddMongoDBOptions()` without explicit ConfigurationName uses `"Default"` -- Existing tests still pass - -## Done Condition -User confirms the fix is satisfactory. - -## Originating Branch -develop diff --git a/.claude/features-done/monitor-track-persisted.md b/.claude/features-done/monitor-track-persisted.md deleted file mode 100644 index 0d3e7b9..0000000 --- a/.claude/features-done/monitor-track-persisted.md +++ /dev/null @@ -1,21 +0,0 @@ -# Feature: monitor-track-persisted - -## Goal -Make cache items loaded from persistence (MongoDB, Redis, File) visible in the Blazor monitoring UI on first access, not only after re-fetch. - -## Scope -- `IManagedCacheMonitor` — add `Track` method -- `CacheMonitor` — implement `Track` (register in `_caches` without firing `DataSetEvent`) -- `CacheBase.GetCoreAsync` — call `Track` when a fresh item is found from persistence but isn't yet in the monitor -- Tests verifying items are tracked on persistence hit - -## Acceptance Criteria -- Fresh items found in persistence appear in `ICacheMonitor.GetInfos()` after first access -- `DataSetEvent` is NOT fired for items loaded from persistence (only `DataGetEvent`) -- All existing tests still pass - -## Done Condition -User confirms the fix is satisfactory. - -## Originating Branch -develop