feat(mcp): add save_finding to the external server

This commit is contained in:
mika kuns
2026-08-10 10:19:45 +02:00
parent 9a09dd6186
commit 2d4288abca
4 changed files with 93 additions and 4 deletions
+54
View File
@@ -0,0 +1,54 @@
using System.ComponentModel;
using ClaudeDo.Worker.Findings;
using ClaudeDo.Worker.Git;
using ModelContextProtocol.Server;
namespace ClaudeDo.Worker.External;
public sealed record SaveFindingResult(
bool Saved, string Slug, string List, string Path, int TotalFindings, bool NearCapacity);
[McpServerToolType]
public sealed class FindingsMcpTools
{
private readonly IFindingsStore _store;
private readonly IFindingsStoreLocator _locator;
public FindingsMcpTools(IFindingsStore store, IFindingsStoreLocator locator)
{
_store = store;
_locator = locator;
}
[McpServerTool, Description(
"Record a durable trap or invariant you just learned about this codebase, so future sessions " +
"read it instead of rediscovering it — call it the moment you notice one, not at the end of " +
"the session. Admission bar (all three must hold, or don't save it): lasting (still true next " +
"week), non-obvious (a competent reader would get it wrong), and behaviour-changing (\"X looks " +
"like Y but is Z — do W instead\"). A bug you fixed is not a finding, that is git history; " +
"neither is a design decision or a status update. Findings are a scarce, curated list, not a " +
"log — when unsure, don't save it. Re-using a slug overwrites that finding. This is a dumb " +
"write: it does not read the code or generate the finding for you, you already have the " +
"context. nearCapacity=true in the result means the index is getting too long to stay cheap — " +
"prune stale entries before adding more.")]
public async Task<SaveFindingResult> SaveFinding(
[Description("Stable lowercase kebab-case id, max 60 chars, e.g. 'conpty-arg-quoting'. Same slug overwrites.")] string slug,
[Description("One full sentence stating the trap itself — this is the index line other agents scan.")] string title,
[Description("2-6 sentences: what happens, why it does not look like that, what to do instead.")] string body,
[Description("Repo-relative path or subsystem the finding applies to, e.g. 'src/ClaudeDo.Worker/Planning'.")] string scope = "",
[Description("List id or name. Optional when exactly one list has a working directory.")] string list = "",
CancellationToken cancellationToken = default)
{
var target = await _locator.ResolveForListAsync(list, cancellationToken);
var head = await GitHead.ShortAsync(target.WorkingDir, cancellationToken);
var outcome = await _store.SaveAsync(
target.WorkingDir,
new FindingInput(slug, title, body, scope, SourceTaskId: "", VerifiedAgainst: head),
cancellationToken,
target.Tracked);
return new SaveFindingResult(
true, outcome.Slug, target.ListName, outcome.Path, outcome.TotalFindings, outcome.NearCapacity);
}
}