Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions .claude/features-done/blazor-ui-overhaul.md

This file was deleted.

26 changes: 0 additions & 26 deletions .claude/features-done/cache-load-time.md

This file was deleted.

19 changes: 0 additions & 19 deletions .claude/features-done/gettypes-safe.md

This file was deleted.

20 changes: 0 additions & 20 deletions .claude/features-done/github-actions.md

This file was deleted.

22 changes: 0 additions & 22 deletions .claude/features-done/idempotent-addcache.md

This file was deleted.

18 changes: 0 additions & 18 deletions .claude/features-done/mongodb-clear-cache.md

This file was deleted.

17 changes: 0 additions & 17 deletions .claude/features-done/mongodb-default-config.md

This file was deleted.

21 changes: 0 additions & 21 deletions .claude/features-done/monitor-track-persisted.md

This file was deleted.

2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions Sample/Tharga.Cache.WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -38,6 +40,13 @@

builder.Services.AddHostedService<CacheMonitorBackgroundService>();

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;
Expand All @@ -58,6 +67,8 @@

app.MapControllers();

app.UseThargaMcp();

//app.UseQuilt4NetApi();

app.Run();
Expand Down
1 change: 1 addition & 0 deletions Sample/Tharga.Cache.WebApi/Tharga.Cache.WebApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Tharga.Cache.Mcp\Tharga.Cache.Mcp.csproj" />
<ProjectReference Include="..\..\Tharga.Cache.MongoDB\Tharga.Cache.MongoDB.csproj" />
<ProjectReference Include="..\..\Tharga.Cache.Redis\Tharga.Cache.Redis.csproj" />
<ProjectReference Include="..\..\Tharga.Cache\Tharga.Cache.csproj" />
Expand Down
163 changes: 163 additions & 0 deletions Tharga.Cache.Mcp/CacheResourceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System.Text.Json;
using Tharga.Mcp;

namespace Tharga.Cache.Mcp;

/// <summary>
/// Exposes Tharga.Cache monitoring data as MCP resources on the System scope.
/// </summary>
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<IReadOnlyList<McpResourceDescriptor>> ListResourcesAsync(IMcpContext context, CancellationToken cancellationToken)
{
IReadOnlyList<McpResourceDescriptor> 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<McpResourceContent> 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<McpResourceContent> BuildHealthAsync()
{
var results = new List<object>();
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;
}
}
Loading
Loading