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 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.", "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": {}}`), 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 return m.listClients(), nil
}, }),
}, },
{ {
Name: "client_bash", Name: "client_bash",
@@ -134,7 +134,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
}, },
"required": ["client", "op"] "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) return m.drivePage(ctx, l, args)
}), }),
}, },
@@ -186,7 +186,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
}, },
"required": ["client", "path"] "required": ["client", "path"]
}`), }`),
Rich: mcp.TypedRich(m.clientRead), Handler: mcp.TypedResult(m.clientRead),
}, },
{ {
Name: "client_write", Name: "client_write",
@@ -264,7 +264,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
}, },
"required": ["client", "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"] "required": ["source"]
}`), }`),
Rich: mcp.TypedRich(func(ctx context.Context, args struct { Handler: mcp.TypedResult(func(ctx context.Context, args struct {
Source string `json:"source"` Source string `json:"source"`
}) mcp.Result { }) mcp.Result {
png, err := renderDiagram(ctx, args.Source) png, err := renderDiagram(ctx, args.Source)
+1 -1
View File
@@ -38,7 +38,7 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
}, },
"required": ["path"] "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) return m.read(l, args)
}), }),
}, },
+1 -1
View File
@@ -32,7 +32,7 @@ func (m *Manager) randomChoiceTool() mcp.Tool {
}, },
"required": ["choices"] "required": ["choices"]
}`), }`),
Rich: mcp.TypedRich(func(ctx context.Context, args struct { Handler: mcp.TypedResult(func(ctx context.Context, args struct {
Choices []struct { Choices []struct {
Choice string `json:"choice"` Choice string `json:"choice"`
Weight float64 `json:"weight"` 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. " + 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.", "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": {}}`), 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, "") err := m.store.ClearReady(ctx, l.taskID, agentActor, "")
if errors.Is(err, store.ErrNotFound) { if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task has no standing ready_for_merge or suggest_discard", ErrConflict) 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 "", err
} }
return "Withdrawn: the human no longer sees this task as ready to merge or suggested for discard.", nil 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", Name: "take_snapshot",
Description: takeSnapshotDescription + " It snapshots your own task's worktree.", Description: takeSnapshotDescription + " It snapshots your own task's worktree.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`), 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) err := m.RequestSnapshot(ctx, l.taskID)
if err != nil { if err != nil {
return "", err 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 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 Name string
Description string Description string
InputSchema json.RawMessage InputSchema json.RawMessage
Handler func(ctx context.Context, args json.RawMessage) (string, error) Handler func(ctx context.Context, args json.RawMessage) Result
Rich func(ctx context.Context, args json.RawMessage) Result
} }
func decode[A any](raw json.RawMessage) (A, error) { func Typed[A any](run func(ctx context.Context, args A) (string, error)) func(ctx context.Context, raw json.RawMessage) Result {
args := *new(A) return TypedResult(func(ctx context.Context, args A) Result {
err := json.Unmarshal(raw, &args) text, err := run(ctx, 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)
if err != nil { if err != nil {
return TextResult(err.Error(), true) 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) return run(ctx, args)
} }
} }
@@ -120,7 +111,7 @@ func (t Tool) Call(ctx context.Context, args json.RawMessage) Result {
defer cancel() defer cancel()
done := make(chan Result, 1) done := make(chan Result, 1)
go func() { go func() {
done <- t.run(cctx, args) done <- t.Handler(cctx, args)
}() }()
late := time.NewTimer(timeout + Grace) late := time.NewTimer(timeout + Grace)
defer late.Stop() 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 { type Server struct {
Name string Name string
Version string Version string