using System.ComponentModel; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ModelContextProtocol.Server; namespace ClaudeDo.Worker.External; public sealed record RunDto( string Id, int RunNumber, string? SessionId, bool IsRetry, string? ResultMarkdown, string? StructuredOutputJson, string? ErrorMarkdown, int? ExitCode, int? TurnCount, int? TokensIn, int? TokensOut, DateTime? StartedAt, DateTime? FinishedAt); [McpServerToolType] public sealed class RunHistoryMcpTools { private readonly TaskRunRepository _runs; public RunHistoryMcpTools(TaskRunRepository runs) => _runs = runs; [McpServerTool, Description("List all execution runs for a task (newest run metadata, tokens, turns, result, error).")] public async Task> ListRuns(string taskId, CancellationToken cancellationToken) { var runs = await _runs.GetByTaskIdAsync(taskId, cancellationToken); return runs.Select(ToDto).ToList(); } [McpServerTool, Description("Get a single execution run by its run id.")] public async Task GetRun(string runId, CancellationToken cancellationToken) { var run = await _runs.GetByIdAsync(runId, cancellationToken) ?? throw new InvalidOperationException($"Run {runId} not found."); return ToDto(run); } private const int MaxLogBytes = 256 * 1024; [McpServerTool, Description("Fetch the raw log output of a task's latest run. Throws if no log is available.")] public async Task GetTaskLog(string taskId, CancellationToken cancellationToken) { var run = await _runs.GetLatestByTaskIdAsync(taskId, cancellationToken) ?? throw new InvalidOperationException($"No runs found for task {taskId}."); if (string.IsNullOrWhiteSpace(run.LogPath) || !File.Exists(run.LogPath)) throw new InvalidOperationException("No log available for the latest run."); var totalBytes = new FileInfo(run.LogPath).Length; if (totalBytes <= MaxLogBytes) return await File.ReadAllTextAsync(run.LogPath, cancellationToken); var buffer = new byte[MaxLogBytes]; await using var fs = new FileStream(run.LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); fs.Seek(totalBytes - MaxLogBytes, SeekOrigin.Begin); var read = await fs.ReadAsync(buffer, cancellationToken); var tail = System.Text.Encoding.UTF8.GetString(buffer, 0, read); return $"[truncated: showing last {MaxLogBytes} of {totalBytes} bytes]\n{tail}"; } private static RunDto ToDto(TaskRunEntity r) => new( r.Id, r.RunNumber, r.SessionId, r.IsRetry, r.ResultMarkdown, r.StructuredOutputJson, r.ErrorMarkdown, r.ExitCode, r.TurnCount, r.TokensIn, r.TokensOut, r.StartedAt, r.FinishedAt); }