Skip to content

Commit 98a8246

Browse files
committed
add transition_change tool and transition group bundle
transition_change moves a change's state behind one action enum: submit (NEW -> MERGED), abandon (NEW -> ABANDONED), restore (ABANDONED -> NEW), wip, and ready. An optional message accompanies every action except submit -- Gerrit's submit endpoint takes none, so a message there is refused rather than dropped. The ack carries the resulting status when the endpoint returns one; Gerrit refusals (blocked submit) are reported verbatim. The transition capability group bundles get_change, set_vote, and transition_change -- the minimal read subset the flow needs to stand on its own. Refs: #17, #18
1 parent 8ee0db4 commit 98a8246

6 files changed

Lines changed: 373 additions & 4 deletions

File tree

internal/registry/registry.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ func groupTools() map[config.Group][]string {
3636
tools.NameGetChangeComments,
3737
tools.NamePostComments,
3838
},
39-
config.GroupTransition: {}, // arrives with the transition group implementation
39+
// transition bundles the minimal read subset it needs: understanding
40+
// the change whose state it moves.
41+
config.GroupTransition: {
42+
tools.NameGetChange,
43+
tools.NameSetVote,
44+
tools.NameTransitionChange,
45+
},
4046
}
4147
}
4248

internal/registry/registry_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,28 @@ func Test_Resolve(t *testing.T) {
8484
want: append(allReadTools(), tools.NamePostComments),
8585
},
8686
{
87-
name: "transition group exposes nothing yet",
87+
name: "transition group bundles its read subset",
8888
giveGroups: []config.Group{config.GroupTransition},
8989
giveInclude: nil,
9090
giveExclude: nil,
91-
want: nil,
91+
want: []string{
92+
tools.NameGetChange,
93+
tools.NameSetVote,
94+
tools.NameTransitionChange,
95+
},
96+
},
97+
{
98+
name: "comment and transition union deduplicates",
99+
giveGroups: []config.Group{config.GroupComment, config.GroupTransition},
100+
giveInclude: nil,
101+
giveExclude: nil,
102+
want: []string{
103+
tools.NameGetChange,
104+
tools.NameGetChangeComments,
105+
tools.NamePostComments,
106+
tools.NameSetVote,
107+
tools.NameTransitionChange,
108+
},
92109
},
93110
{
94111
name: "exclude removes tools",

internal/tools/get-change_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func Test_GetChange(t *testing.T) {
104104

105105
assert.ElementsMatch(t, []string{
106106
"search_changes", "get_change", "list_change_files", "get_file_diff", "get_change_comments",
107-
"post_comments", "set_vote",
107+
"post_comments", "set_vote", "transition_change",
108108
}, names)
109109
})
110110

internal/tools/tools.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
NameGetChangeComments = "get_change_comments"
1919
NamePostComments = "post_comments"
2020
NameSetVote = "set_vote"
21+
NameTransitionChange = "transition_change"
2122
)
2223

2324
// Tool binds a tool name to its MCP registration.
@@ -37,6 +38,7 @@ func All(c *gerritclient.Client) []Tool {
3738
getChangeComments(c),
3839
postComments(c),
3940
setVote(c),
41+
transitionChange(c),
4042
}
4143
}
4244

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"dev.gaijin.team/go/golib/e"
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
10+
"dev.gaijin.team/go/go-gerrit-mcp/internal/gerritclient"
11+
"dev.gaijin.team/go/go-gerrit-mcp/internal/llmxml"
12+
)
13+
14+
var (
15+
errUnknownAction = e.New("unknown action, expected submit, abandon, restore, wip, or ready")
16+
errSubmitMessage = e.New("submit does not accept a message; vote or comment separately")
17+
)
18+
19+
type transitionChangeInput struct {
20+
Change string `json:"change" jsonschema:"Change identifier: numeric ID, project~number, or Change-Id"`
21+
Action string `json:"action" jsonschema:"One of: submit, abandon, restore, wip, ready"`
22+
Message string `json:"message,omitempty" jsonschema:"Optional message; not accepted for submit"`
23+
}
24+
25+
func transitionChange(c *gerritclient.Client) Tool {
26+
return Tool{
27+
Name: NameTransitionChange,
28+
Register: func(s *mcp.Server) {
29+
mcp.AddTool(s, &mcp.Tool{
30+
Name: NameTransitionChange,
31+
Description: "Transition a Gerrit change's state. Actions and the states they move " +
32+
"between: submit (NEW -> MERGED, requires satisfied submit requirements), " +
33+
"abandon (NEW -> ABANDONED), restore (ABANDONED -> NEW), wip (marks NEW change " +
34+
"work-in-progress), ready (work-in-progress -> ready for review). An optional " +
35+
"message accompanies every action except submit. Gerrit's refusal (blocked " +
36+
"submit, restore of a merged change) is reported verbatim. Refused on changes " +
37+
"not owned by the authenticated account unless the operator disabled the " +
38+
"own-changes restriction.",
39+
}, func(ctx context.Context, _ *mcp.CallToolRequest, in transitionChangeInput,
40+
) (*mcp.CallToolResult, any, error) {
41+
action := strings.ToLower(strings.TrimSpace(in.Action))
42+
43+
status, err := runTransition(ctx, c, action, in)
44+
if err != nil {
45+
return nil, nil, err
46+
}
47+
48+
attrs := []llmxml.Attribute{
49+
llmxml.Attr("change", in.Change),
50+
llmxml.Attr("action", action),
51+
}
52+
if status != "" {
53+
attrs = append(attrs, llmxml.Attr("status", status))
54+
}
55+
56+
return textResult(llmxml.NewElement("change_transitioned", attrs...).String()), nil, nil
57+
})
58+
},
59+
}
60+
}
61+
62+
// runTransition maps the action enum onto the client operation and reports
63+
// the change status when the endpoint returns one.
64+
func runTransition(ctx context.Context, c *gerritclient.Client, action string, in transitionChangeInput,
65+
) (string, error) {
66+
switch action {
67+
case "submit":
68+
if strings.TrimSpace(in.Message) != "" {
69+
return "", errSubmitMessage
70+
}
71+
72+
info, err := c.SubmitChange(ctx, in.Change)
73+
if err != nil {
74+
return "", err
75+
}
76+
77+
return info.Status, nil
78+
79+
case "abandon":
80+
info, err := c.AbandonChange(ctx, in.Change, in.Message)
81+
if err != nil {
82+
return "", err
83+
}
84+
85+
return info.Status, nil
86+
87+
case "restore":
88+
info, err := c.RestoreChange(ctx, in.Change, in.Message)
89+
if err != nil {
90+
return "", err
91+
}
92+
93+
return info.Status, nil
94+
95+
case "wip":
96+
return "", c.SetWorkInProgress(ctx, in.Change, in.Message)
97+
case "ready":
98+
return "", c.SetReadyForReview(ctx, in.Change, in.Message)
99+
default:
100+
return "", errUnknownAction.WithField("action", in.Action)
101+
}
102+
}

0 commit comments

Comments
 (0)