Skip to main content
Glama

COA Goldfish MCP

by anortham
StorageService.csโ€ข21.1 kB
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using COA.Goldfish.McpServer.Models; namespace COA.Goldfish.McpServer.Services.Storage; public class StorageService : IStorageService { private readonly GoldfishDbContext _context; private readonly ILogger<StorageService> _logger; public StorageService(GoldfishDbContext context, ILogger<StorageService> logger) { _context = context; _logger = logger; } // Checkpoint operations public async Task<Checkpoint> SaveCheckpointAsync(Checkpoint checkpoint) { var existing = await _context.Checkpoints.FindAsync(checkpoint.Id); if (existing != null) { _context.Entry(existing).CurrentValues.SetValues(checkpoint); } else { _context.Checkpoints.Add(checkpoint); } await _context.SaveChangesAsync(); return checkpoint; } public async Task<Checkpoint?> GetCheckpointAsync(string checkpointId) { return await _context.Checkpoints.FindAsync(checkpointId); } public async Task<List<Checkpoint>> GetCheckpointsAsync(string workspaceId, int limit = 10) { return await _context.Checkpoints .Where(c => c.WorkspaceId == workspaceId) .OrderByDescending(c => c.CreatedAt) .Take(limit) .ToListAsync(); } public async Task<Checkpoint?> GetLatestCheckpointAsync(string workspaceId) { return await _context.Checkpoints .Where(c => c.WorkspaceId == workspaceId) .OrderByDescending(c => c.CreatedAt) .FirstOrDefaultAsync(); } public async Task<bool> DeleteCheckpointAsync(string checkpointId) { var checkpoint = await _context.Checkpoints.FindAsync(checkpointId); if (checkpoint != null) { _context.Checkpoints.Remove(checkpoint); await _context.SaveChangesAsync(); return true; } return false; } // Plan operations public async Task<Plan> SavePlanAsync(Plan plan) { var existing = await _context.Plans.FindAsync(plan.Id); if (existing != null) { plan.UpdatedAt = DateTime.UtcNow; _context.Entry(existing).CurrentValues.SetValues(plan); } else { _context.Plans.Add(plan); } await _context.SaveChangesAsync(); return plan; } public async Task<Plan?> GetPlanAsync(string planId) { return await _context.Plans.FindAsync(planId); } public async Task<List<Plan>> GetPlansAsync(string workspaceId, bool includeCompleted = true) { var query = _context.Plans.Where(p => p.WorkspaceId == workspaceId); if (!includeCompleted) { query = query.Where(p => p.Status != PlanStatus.Complete && p.Status != PlanStatus.Abandoned); } return await query .OrderByDescending(p => p.UpdatedAt) .ToListAsync(); } public async Task<Plan?> GetActivePlanAsync(string workspaceId) { return await _context.Plans .Where(p => p.WorkspaceId == workspaceId && p.Status == PlanStatus.Active) .OrderByDescending(p => p.UpdatedAt) .FirstOrDefaultAsync(); } public async Task<bool> DeletePlanAsync(string planId) { var plan = await _context.Plans.FindAsync(planId); if (plan != null) { _context.Plans.Remove(plan); await _context.SaveChangesAsync(); return true; } return false; } // TODO operations public async Task<TodoList> SaveTodoListAsync(TodoList todoList) { var existing = await _context.TodoLists.FindAsync(todoList.Id); if (existing != null) { todoList.UpdatedAt = DateTime.UtcNow; _context.Entry(existing).CurrentValues.SetValues(todoList); } else { _context.TodoLists.Add(todoList); } await _context.SaveChangesAsync(); return todoList; } public async Task<TodoList?> GetTodoListAsync(string todoListId) { return await _context.TodoLists .Include(tl => tl.Items) .FirstOrDefaultAsync(tl => tl.Id == todoListId); } public async Task<List<TodoList>> GetTodoListsAsync(string workspaceId, bool includeCompleted = true) { var query = _context.TodoLists .Include(tl => tl.Items) .Where(tl => tl.WorkspaceId == workspaceId); return await query .OrderByDescending(tl => tl.UpdatedAt) .ToListAsync(); } public async Task<TodoList?> GetActiveTodoListAsync(string workspaceId) { return await _context.TodoLists .Include(tl => tl.Items) .Where(tl => tl.WorkspaceId == workspaceId && tl.IsActive) .OrderByDescending(tl => tl.UpdatedAt) .FirstOrDefaultAsync(); } public async Task<TodoList?> GetLatestTodoListAsync(string workspaceId) { return await _context.TodoLists .Include(tl => tl.Items) .Where(tl => tl.WorkspaceId == workspaceId) .OrderByDescending(tl => tl.UpdatedAt) .FirstOrDefaultAsync(); } public async Task<bool> DeleteTodoListAsync(string todoListId) { var todoList = await _context.TodoLists.FindAsync(todoListId); if (todoList != null) { _context.TodoLists.Remove(todoList); await _context.SaveChangesAsync(); return true; } return false; } public async Task<TodoItem> SaveTodoItemAsync(TodoItem todoItem) { var existing = await _context.TodoItems.FindAsync(todoItem.Id); if (existing != null) { todoItem.UpdatedAt = DateTime.UtcNow; _context.Entry(existing).CurrentValues.SetValues(todoItem); } else { _context.TodoItems.Add(todoItem); } await _context.SaveChangesAsync(); return todoItem; } public async Task<TodoItem?> GetTodoItemAsync(string todoItemId) { return await _context.TodoItems.FindAsync(todoItemId); } public async Task<bool> DeleteTodoItemAsync(string todoItemId) { var todoItem = await _context.TodoItems.FindAsync(todoItemId); if (todoItem != null) { _context.TodoItems.Remove(todoItem); await _context.SaveChangesAsync(); return true; } return false; } // Chronicle operations public async Task<ChronicleEntry> SaveChronicleEntryAsync(ChronicleEntry entry) { var existing = await _context.ChronicleEntries.FindAsync(entry.Id); if (existing != null) { _context.Entry(existing).CurrentValues.SetValues(entry); } else { _context.ChronicleEntries.Add(entry); } await _context.SaveChangesAsync(); return entry; } public async Task<ChronicleEntry?> GetChronicleEntryAsync(string entryId) { return await _context.ChronicleEntries.FindAsync(entryId); } public async Task<List<ChronicleEntry>> GetChronicleEntriesAsync(string workspaceId, DateTime? since = null, int limit = 50) { var query = _context.ChronicleEntries.Where(e => e.WorkspaceId == workspaceId); if (since.HasValue) { query = query.Where(e => e.Timestamp >= since.Value); } return await query .OrderByDescending(e => e.Timestamp) .Take(limit) .ToListAsync(); } public async Task<bool> DeleteChronicleEntryAsync(string entryId) { var entry = await _context.ChronicleEntries.FindAsync(entryId); if (entry != null) { _context.ChronicleEntries.Remove(entry); await _context.SaveChangesAsync(); return true; } return false; } // Workspace operations public async Task<WorkspaceState> GetWorkspaceStateAsync(string workspaceId) { var state = await _context.WorkspaceStates.FindAsync(workspaceId); if (state == null) { state = new WorkspaceState { WorkspaceId = workspaceId }; _context.WorkspaceStates.Add(state); await _context.SaveChangesAsync(); } return state; } public async Task<WorkspaceState> SaveWorkspaceStateAsync(WorkspaceState workspaceState) { workspaceState.LastActivity = DateTime.UtcNow; var existing = await _context.WorkspaceStates.FindAsync(workspaceState.WorkspaceId); if (existing != null) { _context.Entry(existing).CurrentValues.SetValues(workspaceState); } else { _context.WorkspaceStates.Add(workspaceState); } await _context.SaveChangesAsync(); return workspaceState; } public async Task<List<string>> GetWorkspacesAsync() { return await _context.WorkspaceStates .Select(ws => ws.WorkspaceId) .Distinct() .ToListAsync(); } // Search operations public async Task<List<T>> SearchAsync<T>(string workspaceId, string query, DateTime? since = null, int limit = 20) where T : class { // Basic implementation - can be enhanced with full-text search later var results = new List<T>(); if (typeof(T) == typeof(Checkpoint)) { var checkpoints = await SearchCheckpointsAsync(workspaceId, query, since, limit); results.AddRange(checkpoints.Cast<T>()); } else if (typeof(T) == typeof(Plan)) { var plans = await SearchPlansAsync(workspaceId, query, since, limit); results.AddRange(plans.Cast<T>()); } else if (typeof(T) == typeof(TodoList)) { var todoLists = await SearchTodoListsAsync(workspaceId, query, since, limit); results.AddRange(todoLists.Cast<T>()); } else if (typeof(T) == typeof(ChronicleEntry)) { var chronicles = await SearchChronicleEntriesAsync(workspaceId, query, since, limit); results.AddRange(chronicles.Cast<T>()); } return results; } public async Task<List<Checkpoint>> SearchCheckpointsAsync(string workspaceId, string query, DateTime? since = null, int limit = 20) { var queryLower = query.ToLowerInvariant(); return await _context.Checkpoints .Where(c => c.WorkspaceId == workspaceId && (c.Description.ToLower().Contains(queryLower) || c.WorkContext.ToLower().Contains(queryLower) || c.GitBranch.ToLower().Contains(queryLower))) .Where(c => !since.HasValue || c.CreatedAt >= since.Value) .OrderByDescending(c => c.CreatedAt) .Take(limit) .ToListAsync(); } public async Task<List<Plan>> SearchPlansAsync(string workspaceId, string query, DateTime? since = null, int limit = 20) { var queryLower = query.ToLowerInvariant(); return await _context.Plans .Where(p => p.WorkspaceId == workspaceId && (p.Title.ToLower().Contains(queryLower) || p.Description.ToLower().Contains(queryLower) || p.Category.ToLower().Contains(queryLower))) .Where(p => !since.HasValue || p.CreatedAt >= since.Value) .OrderByDescending(p => p.UpdatedAt) .Take(limit) .ToListAsync(); } public async Task<List<TodoList>> SearchTodoListsAsync(string workspaceId, string query, DateTime? since = null, int limit = 20) { var queryLower = query.ToLowerInvariant(); return await _context.TodoLists .Include(tl => tl.Items) .Where(tl => tl.WorkspaceId == workspaceId && (tl.Title.ToLower().Contains(queryLower) || tl.Description.ToLower().Contains(queryLower))) .Where(tl => !since.HasValue || tl.CreatedAt >= since.Value) .OrderByDescending(tl => tl.UpdatedAt) .Take(limit) .ToListAsync(); } public async Task<List<ChronicleEntry>> SearchChronicleEntriesAsync(string workspaceId, string query, DateTime? since = null, int limit = 20) { var queryLower = query.ToLowerInvariant(); return await _context.ChronicleEntries .Where(ce => ce.WorkspaceId == workspaceId && ce.Description.ToLower().Contains(queryLower)) .Where(ce => !since.HasValue || ce.Timestamp >= since.Value) .OrderByDescending(ce => ce.Timestamp) .Take(limit) .ToListAsync(); } // Advanced queries for keyword resolution (latest, active, etc.) public async Task<TodoList?> ResolveTodoListKeywordAsync(string workspaceId, string keyword) { return keyword.ToLowerInvariant() switch { "latest" or "recent" or "last" => await GetLatestTodoListAsync(workspaceId), "active" or "current" => await GetActiveTodoListAsync(workspaceId), _ => await _context.TodoLists .Include(tl => tl.Items) .Where(tl => tl.WorkspaceId == workspaceId && (tl.Id.EndsWith(keyword) || tl.Id.Contains(keyword))) .OrderByDescending(tl => tl.UpdatedAt) .FirstOrDefaultAsync() }; } public async Task<Plan?> ResolvePlanKeywordAsync(string workspaceId, string keyword) { return keyword.ToLowerInvariant() switch { "latest" or "recent" or "last" => await _context.Plans .Where(p => p.WorkspaceId == workspaceId) .OrderByDescending(p => p.UpdatedAt) .FirstOrDefaultAsync(), "active" or "current" => await GetActivePlanAsync(workspaceId), _ => await _context.Plans .Where(p => p.WorkspaceId == workspaceId && (p.Id.EndsWith(keyword) || p.Id.Contains(keyword))) .OrderByDescending(p => p.UpdatedAt) .FirstOrDefaultAsync() }; } // Cleanup operations public async Task<int> CleanupExpiredItemsAsync() { var now = DateTime.UtcNow; var count = 0; // Clean up expired checkpoints var expiredCheckpoints = await _context.Checkpoints .Where(c => c.TtlExpiry.HasValue && c.TtlExpiry.Value <= now) .ToListAsync(); _context.Checkpoints.RemoveRange(expiredCheckpoints); count += expiredCheckpoints.Count; // Clean up expired plans var expiredPlans = await _context.Plans .Where(p => p.TtlExpiry.HasValue && p.TtlExpiry.Value <= now) .ToListAsync(); _context.Plans.RemoveRange(expiredPlans); count += expiredPlans.Count; // Clean up expired todo lists var expiredTodos = await _context.TodoLists .Where(t => t.TtlExpiry.HasValue && t.TtlExpiry.Value <= now) .ToListAsync(); _context.TodoLists.RemoveRange(expiredTodos); count += expiredTodos.Count; // Clean up expired chronicle entries var expiredChronicles = await _context.ChronicleEntries .Where(c => c.TtlExpiry.HasValue && c.TtlExpiry.Value <= now) .ToListAsync(); _context.ChronicleEntries.RemoveRange(expiredChronicles); count += expiredChronicles.Count; if (count > 0) { await _context.SaveChangesAsync(); _logger.LogInformation("Cleaned up {Count} expired items", count); } return count; } public async Task<int> CleanupWorkspaceAsync(string workspaceId) { var count = 0; // Clean up checkpoints for workspace var checkpoints = await _context.Checkpoints .Where(c => c.WorkspaceId == workspaceId) .ToListAsync(); _context.Checkpoints.RemoveRange(checkpoints); count += checkpoints.Count; // Clean up plans for workspace var plans = await _context.Plans .Where(p => p.WorkspaceId == workspaceId) .ToListAsync(); _context.Plans.RemoveRange(plans); count += plans.Count; // Clean up todo lists for workspace var todoLists = await _context.TodoLists .Where(t => t.WorkspaceId == workspaceId) .ToListAsync(); _context.TodoLists.RemoveRange(todoLists); count += todoLists.Count; // Clean up chronicle entries for workspace var chronicles = await _context.ChronicleEntries .Where(c => c.WorkspaceId == workspaceId) .ToListAsync(); _context.ChronicleEntries.RemoveRange(chronicles); count += chronicles.Count; // Clean up workspace state var workspaceState = await _context.WorkspaceStates.FindAsync(workspaceId); if (workspaceState != null) { _context.WorkspaceStates.Remove(workspaceState); count++; } if (count > 0) { await _context.SaveChangesAsync(); _logger.LogInformation("Cleaned up workspace {WorkspaceId}: {Count} items removed", workspaceId, count); } return count; } // Transaction support public async Task<TResult> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> operation) { using var transaction = await _context.Database.BeginTransactionAsync(); try { var result = await operation(); await transaction.CommitAsync(); return result; } catch { await transaction.RollbackAsync(); throw; } } public async Task ExecuteInTransactionAsync(Func<Task> operation) { using var transaction = await _context.Database.BeginTransactionAsync(); try { await operation(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } // Bulk operations public async Task<int> BulkSaveCheckpointsAsync(IEnumerable<Checkpoint> checkpoints) { var checkpointList = checkpoints.ToList(); var count = 0; foreach (var checkpoint in checkpointList) { var existing = await _context.Checkpoints.FindAsync(checkpoint.Id); if (existing != null) { _context.Entry(existing).CurrentValues.SetValues(checkpoint); } else { _context.Checkpoints.Add(checkpoint); count++; } } await _context.SaveChangesAsync(); return count; } public async Task<int> BulkDeleteExpiredAsync<T>(DateTime expiredBefore) where T : class { var count = 0; if (typeof(T) == typeof(Checkpoint)) { var expired = await _context.Checkpoints .Where(c => c.TtlExpiry.HasValue && c.TtlExpiry.Value <= expiredBefore) .ToListAsync(); _context.Checkpoints.RemoveRange(expired); count = expired.Count; } else if (typeof(T) == typeof(Plan)) { var expired = await _context.Plans .Where(p => p.TtlExpiry.HasValue && p.TtlExpiry.Value <= expiredBefore) .ToListAsync(); _context.Plans.RemoveRange(expired); count = expired.Count; } else if (typeof(T) == typeof(TodoList)) { var expired = await _context.TodoLists .Where(t => t.TtlExpiry.HasValue && t.TtlExpiry.Value <= expiredBefore) .ToListAsync(); _context.TodoLists.RemoveRange(expired); count = expired.Count; } else if (typeof(T) == typeof(ChronicleEntry)) { var expired = await _context.ChronicleEntries .Where(c => c.TtlExpiry.HasValue && c.TtlExpiry.Value <= expiredBefore) .ToListAsync(); _context.ChronicleEntries.RemoveRange(expired); count = expired.Count; } if (count > 0) { await _context.SaveChangesAsync(); _logger.LogInformation("Bulk deleted {Count} expired {Type} items", count, typeof(T).Name); } return count; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/anortham/coa-goldfish-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server