Merge branch 'mai/knuth/phase-5c-itemwrite' (phase 5c slice C: MCP write tools validate)
This commit is contained in:
49
mcp/tools.go
49
mcp/tools.go
@@ -10,9 +10,27 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/m/projax/internal/aggregate"
|
||||
"github.com/m/projax/internal/itemwrite"
|
||||
"github.com/m/projax/store"
|
||||
)
|
||||
|
||||
// itemWriteError serialises an *itemwrite.ValidationError into a Go error
|
||||
// whose Error() string carries the JSON shape MCP clients can parse to
|
||||
// extract kind/path/detail. The JSON-RPC error envelope wraps this in
|
||||
// .error.data so a TypeScript client gets a typed object alongside the
|
||||
// human-readable message.
|
||||
func itemWriteError(ve *itemwrite.ValidationError) error {
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"kind": ve.Kind,
|
||||
"path": ve.Path,
|
||||
"detail": ve.Detail,
|
||||
})
|
||||
if err != nil {
|
||||
return ve
|
||||
}
|
||||
return fmt.Errorf("validation %s: %s [%s]", ve.Kind, ve.Detail, string(body))
|
||||
}
|
||||
|
||||
// TimelineArgs is the MCP-facing input shape for the `timeline` tool — a
|
||||
// JSON-friendly equivalent of the URL query string web/timeline.go consumes.
|
||||
type TimelineArgs struct {
|
||||
@@ -818,9 +836,6 @@ func createItemTool(st *store.Store) ToolHandler {
|
||||
if err := parseInput(raw, &in); err != nil {
|
||||
return nil, fmt.Errorf("bad params: %w", err)
|
||||
}
|
||||
if in.Slug == "" || in.Title == "" {
|
||||
return nil, errors.New("slug and title are required")
|
||||
}
|
||||
parentIDs, err := resolveParentPaths(ctx, st, in.ParentPaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -829,6 +844,18 @@ func createItemTool(st *store.Store) ToolHandler {
|
||||
if len(kind) == 0 {
|
||||
kind = []string{"project"}
|
||||
}
|
||||
// Phase 5c: validate up-front so MCP clients get typed
|
||||
// {kind, path, detail} on rejection instead of an opaque pgErr.
|
||||
if ve := itemwrite.ValidateFormat(itemwrite.Input{
|
||||
Title: in.Title, Slug: in.Slug, Status: in.Status, ParentIDs: parentIDs,
|
||||
}); ve != nil {
|
||||
return nil, itemWriteError(ve)
|
||||
}
|
||||
if ve := itemwrite.ValidateAgainstStore(ctx, st, itemwrite.Input{
|
||||
Title: in.Title, Slug: in.Slug, Status: in.Status, ParentIDs: parentIDs,
|
||||
}); ve != nil {
|
||||
return nil, itemWriteError(ve)
|
||||
}
|
||||
it, err := st.Create(ctx, store.CreateInput{
|
||||
Kind: kind,
|
||||
Title: in.Title,
|
||||
@@ -975,6 +1002,22 @@ func updateItemTool(st *store.Store) ToolHandler {
|
||||
}
|
||||
patch.ParentIDs = pids
|
||||
}
|
||||
// Phase 5c: pre-validate the patched item so MCP clients see typed
|
||||
// {kind, path, detail} rejection instead of an opaque pgErr.
|
||||
validateIn := itemwrite.Input{
|
||||
ID: it.ID,
|
||||
Title: patch.Title,
|
||||
Slug: patch.Slug,
|
||||
Status: patch.Status,
|
||||
ParentIDs: patch.ParentIDs,
|
||||
Path: it.PrimaryPath(),
|
||||
}
|
||||
if ve := itemwrite.ValidateFormat(validateIn); ve != nil {
|
||||
return nil, itemWriteError(ve)
|
||||
}
|
||||
if ve := itemwrite.ValidateAgainstStore(ctx, st, validateIn); ve != nil {
|
||||
return nil, itemWriteError(ve)
|
||||
}
|
||||
updated, err := st.Update(ctx, it.ID, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user