Fix MCP tools/list rejection: wrap list_repos slice in object
The go-sdk infers each tool's outputSchema from its handler result
type, and MCP structured output must be type "object". list_repos
returned []repos.State, yielding outputSchema.type "array", which
Claude Desktop rejects at tools/list — taking the whole server down.
Wrap the slice in listReposOutput{Repos: ...} so the schema is an
object, update the round-trip test, and record the struct-result rule
in AGENT.md §8.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -367,6 +367,13 @@ obeys the safety rules (§1.4).
|
|||||||
`get_pending_switch`, `list_prs`.
|
`get_pending_switch`, `list_prs`.
|
||||||
- Act: `git_status/checkout/commit/push/pull/create_branch`,
|
- Act: `git_status/checkout/commit/push/pull/create_branch`,
|
||||||
`create_pr`, `merge_and_cleanup_pr`, `set_active_project`, `ack_switch`.
|
`create_pr`, `merge_and_cleanup_pr`, `set_active_project`, `ack_switch`.
|
||||||
|
- **A tool's result type must be a struct, never a bare slice/map/scalar.** The
|
||||||
|
go-sdk infers each tool's `outputSchema` from its handler's result type, and MCP
|
||||||
|
structured output must be a JSON **object** (`type: "object"`). A handler that
|
||||||
|
returns `[]T` yields `outputSchema.type: "array"`, which Claude Desktop rejects
|
||||||
|
at `tools/list` — and one bad tool takes the whole server down. Wrap any
|
||||||
|
collection result in a named output struct (e.g. `list_repos` returns
|
||||||
|
`listReposOutput{ Repos []repos.State }`, not `[]repos.State`). Fixed 2026-09-20.
|
||||||
- **Destructive tools carry the §1.4 contract into MCP:** they describe exactly
|
- **Destructive tools carry the §1.4 contract into MCP:** they describe exactly
|
||||||
what they will do and default to the safe variant. The confirmation is the
|
what they will do and default to the safe variant. The confirmation is the
|
||||||
human's — surfaced through Claude and/or the app UI — not something the tool
|
human's — surfaced through Claude and/or the app UI — not something the tool
|
||||||
|
|||||||
+9
-2
@@ -21,6 +21,13 @@ type getRepoInput struct {
|
|||||||
Path string `json:"path" jsonschema:"absolute filesystem path of the repository, exactly as returned by list_repos"`
|
Path string `json:"path" jsonschema:"absolute filesystem path of the repository, exactly as returned by list_repos"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listReposOutput wraps the repository list. MCP structured output must be a JSON
|
||||||
|
// object, so the SDK-inferred outputSchema has to be type "object" — returning a
|
||||||
|
// bare slice yields type "array", which Claude Desktop rejects at tools/list.
|
||||||
|
type listReposOutput struct {
|
||||||
|
Repos []repos.State `json:"repos" jsonschema:"the discovered repositories"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
||||||
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
||||||
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
||||||
@@ -34,8 +41,8 @@ func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
|||||||
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
Name: "list_repos",
|
Name: "list_repos",
|
||||||
Description: "List every Git repository GitManager has discovered, each with its current branch, dirty/clean state, ahead/behind counts, and remote names.",
|
Description: "List every Git repository GitManager has discovered, each with its current branch, dirty/clean state, ahead/behind counts, and remote names.",
|
||||||
}, func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, []repos.State, error) {
|
}, func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listReposOutput, error) {
|
||||||
return nil, svc.ListRepos(), nil
|
return nil, listReposOutput{Repos: svc.ListRepos()}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// get_repo — details for one already-discovered repository.
|
// get_repo — details for one already-discovered repository.
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ func TestMCPRoundTrip(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("list_repos: %v", err)
|
t.Fatalf("list_repos: %v", err)
|
||||||
}
|
}
|
||||||
var states []repos.State
|
var listOut listReposOutput
|
||||||
decodeResult(t, res, &states)
|
decodeResult(t, res, &listOut)
|
||||||
|
states := listOut.Repos
|
||||||
if len(states) != 1 {
|
if len(states) != 1 {
|
||||||
t.Fatalf("expected 1 repo, got %d: %+v", len(states), states)
|
t.Fatalf("expected 1 repo, got %d: %+v", len(states), states)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user