Every tool description now leads with what the tool does AND when to reach for it, since MCP clients rank tools by that text. Per-parameter prose moved onto the parameters as [Description], exhaustive result-shape enumerations and design/history rationale dropped, and the repeated boilerplate clauses (lean-task-ref, batch cap, refused-while-Running) pulled into McpToolDocs, which also documents the style for future tools. Tool-level description text: 20494 -> 13605 chars (-34%); combined with the new parameter descriptions 18517 (-10%). Closes gaps that caused wrong calls rather than just verbose ones: - list_task_attachments returns metadata only, no file content - run_task_now shares continue_task's single override slot and throws when busy - list_runs is ordered oldest-first and feeds get_run - workingDir on create_list/update_list is an existing local git repo path, unvalidated until the first task run - get_task_worktree's behind=0 also means the main ref was unreachable Removes get_task_status_values: a whole tool entry for static reference text. GetTask's description is now the canonical place for status meanings.
113 lines
4.4 KiB
C#
113 lines
4.4 KiB
C#
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);
|
|
|
|
public sealed record TaskLogResult(
|
|
bool Available,
|
|
IReadOnlyList<string> Entries,
|
|
int TotalLines,
|
|
bool Truncated);
|
|
|
|
[McpServerToolType]
|
|
public sealed class RunHistoryMcpTools
|
|
{
|
|
private readonly TaskRunRepository _runs;
|
|
|
|
public RunHistoryMcpTools(TaskRunRepository runs) => _runs = runs;
|
|
|
|
[McpServerTool, Description(
|
|
"List all execution runs for a task — metadata, tokens, turns, result, and error per run — ordered " +
|
|
"oldest to newest by run number, so the last entry is the most recent. Use a run's id from here with " +
|
|
"get_run to fetch it individually.")]
|
|
public async Task<IReadOnlyList<RunDto>> ListRuns(string taskId, CancellationToken cancellationToken)
|
|
{
|
|
var runs = await _runs.GetByTaskIdAsync(taskId, cancellationToken);
|
|
return runs.Select(ToDto).ToList();
|
|
}
|
|
|
|
[McpServerTool, Description("Get one execution run's full detail by its run id, obtained from list_runs.")]
|
|
public async Task<RunDto> GetRun(string runId, CancellationToken cancellationToken)
|
|
{
|
|
var run = await _runs.GetByIdAsync(runId, cancellationToken)
|
|
?? throw new InvalidOperationException($"Run {runId} not found.");
|
|
return ToDto(run);
|
|
}
|
|
|
|
[McpServerTool, Description(
|
|
"Fetch NDJSON log lines from a task's latest run — use this to check progress or debug a task without " +
|
|
"opening the log file. Defaults to the last 50 lines. available=false means no log exists yet (queued " +
|
|
"or just started — not an error); truncated=true when fewer entries are returned than totalLines.")]
|
|
public async Task<TaskLogResult> GetTaskLog(
|
|
string taskId,
|
|
[Description("Number of trailing entries to return; ignored if offset or limit is set. Default 50.")]
|
|
int? tail = null,
|
|
[Description("0-based entry index to start from; overrides tail when set. Combine with limit to page through the log.")]
|
|
int? offset = null,
|
|
[Description("Max entries to return starting at offset. Omit to return everything from offset to the end.")]
|
|
int? limit = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var run = await _runs.GetLatestByTaskIdAsync(taskId, cancellationToken);
|
|
if (run is null || string.IsNullOrWhiteSpace(run.LogPath) || !File.Exists(run.LogPath))
|
|
return new TaskLogResult(false, [], 0, false);
|
|
|
|
string allText;
|
|
try
|
|
{
|
|
await using var fs = new FileStream(
|
|
run.LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
using var reader = new StreamReader(fs);
|
|
allText = await reader.ReadToEndAsync(cancellationToken);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return new TaskLogResult(false, [], 0, false);
|
|
}
|
|
|
|
var lines = allText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
var totalLines = lines.Length;
|
|
|
|
IReadOnlyList<string> entries;
|
|
bool truncated;
|
|
|
|
if (offset.HasValue || limit.HasValue)
|
|
{
|
|
var start = Math.Max(0, offset ?? 0);
|
|
var count = limit.HasValue ? Math.Min(limit.Value, totalLines - start) : totalLines - start;
|
|
entries = lines.Skip(start).Take(count).ToArray();
|
|
truncated = start > 0 || (start + count) < totalLines;
|
|
}
|
|
else
|
|
{
|
|
var take = tail ?? 50;
|
|
if (totalLines <= take)
|
|
{
|
|
entries = lines;
|
|
truncated = false;
|
|
}
|
|
else
|
|
{
|
|
entries = lines[^take..];
|
|
truncated = true;
|
|
}
|
|
}
|
|
|
|
return new TaskLogResult(true, entries, totalLines, truncated);
|
|
}
|
|
|
|
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);
|
|
}
|