Unify MCP tool handlers around one result type

This commit is contained in:
Ian Gulliver
2026-10-23 17:38:34 -07:00
parent 6c51457a06
commit 758aa7f893
7 changed files with 28 additions and 48 deletions
+5 -5
View File
@@ -35,9 +35,9 @@ func (m *Manager) clientShellTools() []mcp.Tool {
"The most recently active one is listed first. " +
"The human's iPhone is listed too, when byre's app is open on it; it takes only show_page, show_file and drive_page, and the other client tools fail there.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
Handler: mcp.Typed(func(ctx context.Context, args struct{}) (string, error) {
return m.listClients(), nil
},
}),
},
{
Name: "client_bash",
@@ -134,7 +134,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "op"]
}`),
Rich: mcp.TypedRich(func(ctx context.Context, args drivePageArgs) mcp.Result {
Handler: mcp.TypedResult(func(ctx context.Context, args drivePageArgs) mcp.Result {
return m.drivePage(ctx, l, args)
}),
},
@@ -186,7 +186,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path"]
}`),
Rich: mcp.TypedRich(m.clientRead),
Handler: mcp.TypedResult(m.clientRead),
},
{
Name: "client_write",
@@ -264,7 +264,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "tool"]
}`),
Rich: mcp.TypedRich(m.clientHands),
Handler: mcp.TypedResult(m.clientHands),
},
}...)
}
+1 -1
View File
@@ -27,7 +27,7 @@ func (m *Manager) renderDiagramTool() mcp.Tool {
},
"required": ["source"]
}`),
Rich: mcp.TypedRich(func(ctx context.Context, args struct {
Handler: mcp.TypedResult(func(ctx context.Context, args struct {
Source string `json:"source"`
}) mcp.Result {
png, err := renderDiagram(ctx, args.Source)
+1 -1
View File
@@ -38,7 +38,7 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
},
"required": ["path"]
}`),
Rich: mcp.TypedRich(func(ctx context.Context, args readArgs) mcp.Result {
Handler: mcp.TypedResult(func(ctx context.Context, args readArgs) mcp.Result {
return m.read(l, args)
}),
},
+1 -1
View File
@@ -32,7 +32,7 @@ func (m *Manager) randomChoiceTool() mcp.Tool {
},
"required": ["choices"]
}`),
Rich: mcp.TypedRich(func(ctx context.Context, args struct {
Handler: mcp.TypedResult(func(ctx context.Context, args struct {
Choices []struct {
Choice string `json:"choice"`
Weight float64 `json:"weight"`
+2 -2
View File
@@ -121,7 +121,7 @@ func (m *Manager) cancelReadyTool(l *live) mcp.Tool {
Description: "Withdraw your standing ready_for_merge or suggest_discard, when it no longer holds: for example the human raised a problem, or you found more work. " +
"The human then no longer sees the task as ready to merge or as suggested for discard; call ready_for_merge or suggest_discard again once it holds.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
Handler: mcp.Typed(func(ctx context.Context, args struct{}) (string, error) {
err := m.store.ClearReady(ctx, l.taskID, agentActor, "")
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task has no standing ready_for_merge or suggest_discard", ErrConflict)
@@ -130,7 +130,7 @@ func (m *Manager) cancelReadyTool(l *live) mcp.Tool {
return "", err
}
return "Withdrawn: the human no longer sees this task as ready to merge or suggested for discard.", nil
},
}),
}
}
+2 -2
View File
@@ -264,13 +264,13 @@ func (m *Manager) takeSnapshotTool(l *live) mcp.Tool {
Name: "take_snapshot",
Description: takeSnapshotDescription + " It snapshots your own task's worktree.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`),
Handler: func(ctx context.Context, args json.RawMessage) (string, error) {
Handler: mcp.Typed(func(ctx context.Context, args struct{}) (string, error) {
err := m.RequestSnapshot(ctx, l.taskID)
if err != nil {
return "", err
}
return "Worktree committed to the task's branch, and snapshot requested. If the project has a review workflow, its reviewers check it against the default branch (a subtask's against its parent's branch); their findings come to you together once the workflow finishes.", nil
},
}),
}
}
+16 -36
View File
@@ -13,35 +13,26 @@ type Tool struct {
Name string
Description string
InputSchema json.RawMessage
Handler func(ctx context.Context, args json.RawMessage) (string, error)
Rich func(ctx context.Context, args json.RawMessage) Result
Handler func(ctx context.Context, args json.RawMessage) Result
}
func decode[A any](raw json.RawMessage) (A, error) {
args := *new(A)
err := json.Unmarshal(raw, &args)
if err != nil {
return args, fmt.Errorf("malformed call: %w", err)
}
return args, nil
}
func Typed[A any](run func(ctx context.Context, args A) (string, error)) func(ctx context.Context, raw json.RawMessage) (string, error) {
return func(ctx context.Context, raw json.RawMessage) (string, error) {
args, err := decode[A](raw)
if err != nil {
return "", err
}
return run(ctx, args)
}
}
func TypedRich[A any](run func(ctx context.Context, args A) Result) func(ctx context.Context, raw json.RawMessage) Result {
return func(ctx context.Context, raw json.RawMessage) Result {
args, err := decode[A](raw)
func Typed[A any](run func(ctx context.Context, args A) (string, error)) func(ctx context.Context, raw json.RawMessage) Result {
return TypedResult(func(ctx context.Context, args A) Result {
text, err := run(ctx, args)
if err != nil {
return TextResult(err.Error(), true)
}
return TextResult(text, false)
})
}
func TypedResult[A any](run func(ctx context.Context, args A) Result) func(ctx context.Context, raw json.RawMessage) Result {
return func(ctx context.Context, raw json.RawMessage) Result {
var args A
err := json.Unmarshal(raw, &args)
if err != nil {
return TextResult(fmt.Sprintf("malformed call: %s", err), true)
}
return run(ctx, args)
}
}
@@ -120,7 +111,7 @@ func (t Tool) Call(ctx context.Context, args json.RawMessage) Result {
defer cancel()
done := make(chan Result, 1)
go func() {
done <- t.run(cctx, args)
done <- t.Handler(cctx, args)
}()
late := time.NewTimer(timeout + Grace)
defer late.Stop()
@@ -134,17 +125,6 @@ func (t Tool) Call(ctx context.Context, args json.RawMessage) Result {
}
}
func (t Tool) run(ctx context.Context, args json.RawMessage) Result {
if t.Rich != nil {
return t.Rich(ctx, args)
}
text, err := t.Handler(ctx, args)
if err != nil {
return TextResult(err.Error(), true)
}
return TextResult(text, false)
}
type Server struct {
Name string
Version string