feat: slim tool responses (#141)

Reduce token usage by slimming tool responses. Instead of returning full Gitea SDK objects (with nested user/repo objects, avatars, permissions, etc.), each operation now has a colocated `slim.go` that extracts only the fields an LLM needs. List endpoints return even fewer fields than single-item endpoints.

Other changes:
- Add `params` helpers to DRY parameter extraction across 40+ handlers
- Remove `{"Result": ...}` wrapper for flatter responses
- Reduce default pageSize from 100 to 30

Fixes: https://gitea.com/gitea/gitea-mcp/issues/128

*Created by Claude on behalf of @silverwind*

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/141
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-03-05 05:56:23 +00:00
committed by silverwind
parent 9ce5604e4c
commit c3db4fb65f
35 changed files with 2274 additions and 1156 deletions

View File

@@ -2,7 +2,6 @@ package pull
import (
"context"
"errors"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -63,7 +62,7 @@ var (
mcp.WithString("sort", mcp.Description("sort"), mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")),
mcp.WithNumber("milestone", mcp.Description("milestone")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)),
)
CreatePullRequestTool = mcp.NewTool(
@@ -104,7 +103,7 @@ var (
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)),
)
GetPullRequestReviewTool = mcp.NewTool(
@@ -269,15 +268,16 @@ func init() {
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestByIndexFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
@@ -290,24 +290,25 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err))
}
return to.TextResult(pr)
return to.TextResult(slimPullRequest(pr))
}
func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestDiffFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
binary, _ := req.GetArguments()["binary"].(bool)
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
binary, _ := args["binary"].(bool)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -320,41 +321,31 @@ func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err))
}
result := map[string]any{
"diff": string(diffBytes),
"binary": binary,
"index": index,
"repo": repo,
"owner": owner,
}
return to.TextResult(result)
return to.TextResult(string(diffBytes))
}
func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoPullRequests")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
state, _ := req.GetArguments()["state"].(string)
sort, ok := req.GetArguments()["sort"].(string)
if !ok {
sort = "recentupdate"
}
milestone := params.GetOptionalInt(req.GetArguments(), "milestone", 0)
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
state, _ := args["state"].(string)
sort := params.GetOptionalString(args, "sort", "recentupdate")
milestone := params.GetOptionalInt(args, "milestone", 0)
page, pageSize := params.GetPagination(args, 30)
opt := gitea_sdk.ListPullRequestsOptions{
State: gitea_sdk.StateType(state),
Sort: sort,
Milestone: milestone,
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
Page: page,
PageSize: pageSize,
},
}
client, err := gitea.ClientFromContext(ctx)
@@ -366,34 +357,35 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err))
}
return to.TextResult(pullRequests)
return to.TextResult(slimPullRequests(pullRequests))
}
func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
title, ok := req.GetArguments()["title"].(string)
if !ok {
return to.ErrorResult(errors.New("title is required"))
title, err := params.GetString(args, "title")
if err != nil {
return to.ErrorResult(err)
}
body, ok := req.GetArguments()["body"].(string)
if !ok {
return to.ErrorResult(errors.New("body is required"))
body, err := params.GetString(args, "body")
if err != nil {
return to.ErrorResult(err)
}
head, ok := req.GetArguments()["head"].(string)
if !ok {
return to.ErrorResult(errors.New("head is required"))
head, err := params.GetString(args, "head")
if err != nil {
return to.ErrorResult(err)
}
base, ok := req.GetArguments()["base"].(string)
if !ok {
return to.ErrorResult(errors.New("base is required"))
base, err := params.GetString(args, "base")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -409,45 +401,27 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err))
}
return to.TextResult(pr)
return to.TextResult(slimPullRequest(pr))
}
func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestReviewerFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]any); ok {
for _, reviewer := range reviewersSlice {
if reviewerStr, ok := reviewer.(string); ok {
reviewers = append(reviewers, reviewerStr)
}
}
}
}
var teamReviewers []string
if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists {
if teamReviewersSlice, ok := teamReviewersArg.([]any); ok {
for _, teamReviewer := range teamReviewersSlice {
if teamReviewerStr, ok := teamReviewer.(string); ok {
teamReviewers = append(teamReviewers, teamReviewerStr)
}
}
}
}
reviewers := params.GetStringSlice(args, "reviewers")
teamReviewers := params.GetStringSlice(args, "team_reviewers")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -476,40 +450,22 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePullRequestReviewerFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]any); ok {
for _, reviewer := range reviewersSlice {
if reviewerStr, ok := reviewer.(string); ok {
reviewers = append(reviewers, reviewerStr)
}
}
}
}
var teamReviewers []string
if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists {
if teamReviewersSlice, ok := teamReviewersArg.([]any); ok {
for _, teamReviewer := range teamReviewersSlice {
if teamReviewerStr, ok := teamReviewer.(string); ok {
teamReviewers = append(teamReviewers, teamReviewerStr)
}
}
}
}
reviewers := params.GetStringSlice(args, "reviewers")
teamReviewers := params.GetStringSlice(args, "team_reviewers")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -537,20 +493,20 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPullRequestReviewsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -559,32 +515,33 @@ func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mc
reviews, _, err := client.ListPullReviews(owner, repo, index, gitea_sdk.ListPullReviewsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
Page: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list reviews for %v/%v/pr/%v err: %v", owner, repo, index, err))
}
return to.TextResult(reviews)
return to.TextResult(slimReviews(reviews))
}
func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(args, "review_id")
if err != nil {
return to.ErrorResult(err)
}
@@ -599,24 +556,25 @@ func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
}
return to.TextResult(review)
return to.TextResult(slimReview(review))
}
func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPullRequestReviewCommentsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(args, "review_id")
if err != nil {
return to.ErrorResult(err)
}
@@ -631,38 +589,39 @@ func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques
return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
}
return to.TextResult(comments)
return to.TextResult(slimReviewComments(comments))
}
func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.CreatePullReviewOptions{}
if state, ok := req.GetArguments()["state"].(string); ok {
if state, ok := args["state"].(string); ok {
opt.State = gitea_sdk.ReviewStateType(state)
}
if body, ok := req.GetArguments()["body"].(string); ok {
if body, ok := args["body"].(string); ok {
opt.Body = body
}
if commitID, ok := req.GetArguments()["commit_id"].(string); ok {
if commitID, ok := args["commit_id"].(string); ok {
opt.CommitID = commitID
}
// Parse inline comments
if commentsArg, exists := req.GetArguments()["comments"]; exists {
if commentsArg, exists := args["comments"]; exists {
if commentsSlice, ok := commentsArg.([]any); ok {
for _, comment := range commentsSlice {
if commentMap, ok := comment.(map[string]any); ok {
@@ -695,36 +654,37 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, index, err))
}
return to.TextResult(review)
return to.TextResult(slimReview(review))
}
func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SubmitPullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
state, ok := req.GetArguments()["state"].(string)
if !ok {
return to.ErrorResult(errors.New("state is required"))
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(args, "review_id")
if err != nil {
return to.ErrorResult(err)
}
state, err := params.GetString(args, "state")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.SubmitPullReviewOptions{
State: gitea_sdk.ReviewStateType(state),
}
if body, ok := req.GetArguments()["body"].(string); ok {
if body, ok := args["body"].(string); ok {
opt.Body = body
}
@@ -738,24 +698,25 @@ func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
}
return to.TextResult(review)
return to.TextResult(slimReview(review))
}
func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(args, "review_id")
if err != nil {
return to.ErrorResult(err)
}
@@ -782,25 +743,26 @@ func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DismissPullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
reviewID, err := params.GetIndex(args, "review_id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.DismissPullReviewOptions{}
if message, ok := req.GetArguments()["message"].(string); ok {
if message, ok := args["message"].(string); ok {
opt.Message = message
}
@@ -826,38 +788,24 @@ func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*
func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called MergePullRequestFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
mergeStyle := "merge"
if style, exists := req.GetArguments()["merge_style"].(string); exists && style != "" {
mergeStyle = style
}
title := ""
if t, exists := req.GetArguments()["title"].(string); exists {
title = t
}
message := ""
if msg, exists := req.GetArguments()["message"].(string); exists {
message = msg
}
deleteBranch := false
if del, exists := req.GetArguments()["delete_branch"].(bool); exists {
deleteBranch = del
}
mergeStyle := params.GetOptionalString(args, "merge_style", "merge")
title, _ := args["title"].(string)
message, _ := args["message"].(string)
deleteBranch, _ := args["delete_branch"].(bool)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -897,53 +845,46 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditPullRequestFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditPullRequestOption{}
if title, ok := req.GetArguments()["title"].(string); ok {
if title, ok := args["title"].(string); ok {
opt.Title = title
}
if body, ok := req.GetArguments()["body"].(string); ok {
if body, ok := args["body"].(string); ok {
opt.Body = new(body)
}
if base, ok := req.GetArguments()["base"].(string); ok {
if base, ok := args["base"].(string); ok {
opt.Base = base
}
if assignee, ok := req.GetArguments()["assignee"].(string); ok {
if assignee, ok := args["assignee"].(string); ok {
opt.Assignee = assignee
}
if assigneesArg, exists := req.GetArguments()["assignees"]; exists {
if assigneesSlice, ok := assigneesArg.([]any); ok {
var assignees []string
for _, a := range assigneesSlice {
if s, ok := a.(string); ok {
assignees = append(assignees, s)
}
}
opt.Assignees = assignees
}
if assignees := params.GetStringSlice(args, "assignees"); assignees != nil {
opt.Assignees = assignees
}
if val, exists := req.GetArguments()["milestone"]; exists {
if val, exists := args["milestone"]; exists {
if milestone, ok := params.ToInt64(val); ok {
opt.Milestone = milestone
}
}
if state, ok := req.GetArguments()["state"].(string); ok {
if state, ok := args["state"].(string); ok {
opt.State = new(gitea_sdk.StateType(state))
}
if allowMaintainerEdit, ok := req.GetArguments()["allow_maintainer_edit"].(bool); ok {
if allowMaintainerEdit, ok := args["allow_maintainer_edit"].(bool); ok {
opt.AllowMaintainerEdit = new(allowMaintainerEdit)
}
@@ -957,5 +898,5 @@ func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
return to.ErrorResult(fmt.Errorf("edit %v/%v/pr/%v err: %v", owner, repo, index, err))
}
return to.TextResult(pr)
return to.TextResult(slimPullRequest(pr))
}

View File

@@ -116,13 +116,11 @@ func TestEditPullRequestFn(t *testing.T) {
t.Fatalf("expected text content, got %T", result.Content[0])
}
var parsed struct {
Result map[string]any `json:"Result"`
}
var parsed map[string]any
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
t.Fatalf("unmarshal result text: %v", err)
}
if got := parsed.Result["title"].(string); got != "WIP: my feature" {
if got := parsed["title"].(string); got != "WIP: my feature" {
t.Fatalf("result title = %q, want %q", got, "WIP: my feature")
}
})
@@ -239,20 +237,18 @@ func TestMergePullRequestFn(t *testing.T) {
t.Fatalf("expected text content, got %T", result.Content[0])
}
var parsed struct {
Result map[string]any `json:"Result"`
}
var parsed map[string]any
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
t.Fatalf("unmarshal result text: %v", err)
}
if parsed.Result["merged"] != true {
t.Fatalf("expected merged=true, got %v", parsed.Result["merged"])
if parsed["merged"] != true {
t.Fatalf("expected merged=true, got %v", parsed["merged"])
}
if parsed.Result["merge_style"] != "squash" {
t.Fatalf("expected merge_style 'squash', got %v", parsed.Result["merge_style"])
if parsed["merge_style"] != "squash" {
t.Fatalf("expected merge_style 'squash', got %v", parsed["merge_style"])
}
if parsed.Result["branch_deleted"] != true {
t.Fatalf("expected branch_deleted=true, got %v", parsed.Result["branch_deleted"])
if parsed["branch_deleted"] != true {
t.Fatalf("expected branch_deleted=true, got %v", parsed["branch_deleted"])
}
})
}
@@ -370,27 +366,13 @@ func TestGetPullRequestDiffFn(t *testing.T) {
t.Fatalf("expected text content, got %T", result.Content[0])
}
var parsed struct {
Result map[string]any `json:"Result"`
}
// The diff response is now a plain string
var parsed string
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
t.Fatalf("unmarshal result text: %v", err)
}
if got, ok := parsed.Result["diff"].(string); !ok || got != diffRaw {
t.Fatalf("diff = %q, want %q", got, diffRaw)
}
if got, ok := parsed.Result["binary"].(bool); !ok || got != true {
t.Fatalf("binary = %v, want true", got)
}
if got, ok := parsed.Result["index"].(float64); !ok || int64(got) != int64(index) {
t.Fatalf("index = %v, want %d", got, index)
}
if got, ok := parsed.Result["owner"].(string); !ok || got != owner {
t.Fatalf("owner = %q, want %q", got, owner)
}
if got, ok := parsed.Result["repo"].(string); !ok || got != repo {
t.Fatalf("repo = %q, want %q", got, repo)
if parsed != diffRaw {
t.Fatalf("diff = %q, want %q", parsed, diffRaw)
}
})
}

191
operation/pull/slim.go Normal file
View File

@@ -0,0 +1,191 @@
package pull
import (
gitea_sdk "code.gitea.io/sdk/gitea"
)
func userLogin(u *gitea_sdk.User) string {
if u == nil {
return ""
}
return u.UserName
}
func userLogins(users []*gitea_sdk.User) []string {
if len(users) == 0 {
return nil
}
out := make([]string, 0, len(users))
for _, u := range users {
if u != nil {
out = append(out, u.UserName)
}
}
return out
}
func labelNames(labels []*gitea_sdk.Label) []string {
if len(labels) == 0 {
return nil
}
out := make([]string, 0, len(labels))
for _, l := range labels {
if l != nil {
out = append(out, l.Name)
}
}
return out
}
func repoRef(r *gitea_sdk.Repository) map[string]any {
if r == nil {
return nil
}
return map[string]any{
"full_name": r.FullName,
"description": r.Description,
}
}
func slimPullRequest(pr *gitea_sdk.PullRequest) map[string]any {
if pr == nil {
return nil
}
m := map[string]any{
"number": pr.Index,
"title": pr.Title,
"body": pr.Body,
"state": pr.State,
"draft": pr.Draft,
"merged": pr.HasMerged,
"mergeable": pr.Mergeable,
"html_url": pr.HTMLURL,
"user": userLogin(pr.Poster),
"labels": labelNames(pr.Labels),
"comments": pr.Comments,
"created_at": pr.Created,
"updated_at": pr.Updated,
"closed_at": pr.Closed,
}
if pr.HasMerged {
m["merged_at"] = pr.Merged
m["merge_commit_sha"] = pr.MergedCommitID
m["merged_by"] = userLogin(pr.MergedBy)
}
if pr.Head != nil {
head := map[string]any{"ref": pr.Head.Ref, "sha": pr.Head.Sha}
if pr.Head.Repository != nil {
head["repo"] = repoRef(pr.Head.Repository)
}
m["head"] = head
}
if pr.Base != nil {
base := map[string]any{"ref": pr.Base.Ref, "sha": pr.Base.Sha}
if pr.Base.Repository != nil {
base["repo"] = repoRef(pr.Base.Repository)
}
m["base"] = base
}
if pr.Additions != nil {
m["additions"] = *pr.Additions
}
if pr.Deletions != nil {
m["deletions"] = *pr.Deletions
}
if pr.ChangedFiles != nil {
m["changed_files"] = *pr.ChangedFiles
}
if len(pr.Assignees) > 0 {
m["assignees"] = userLogins(pr.Assignees)
}
if pr.Milestone != nil {
m["milestone"] = pr.Milestone.Title
}
if pr.ReviewComments > 0 {
m["review_comments"] = pr.ReviewComments
}
return m
}
func slimPullRequests(prs []*gitea_sdk.PullRequest) []map[string]any {
out := make([]map[string]any, 0, len(prs))
for _, pr := range prs {
if pr == nil {
continue
}
m := map[string]any{
"number": pr.Index,
"title": pr.Title,
"state": pr.State,
"draft": pr.Draft,
"merged": pr.HasMerged,
"html_url": pr.HTMLURL,
"user": userLogin(pr.Poster),
"created_at": pr.Created,
"updated_at": pr.Updated,
}
if pr.Head != nil {
m["head"] = pr.Head.Ref
}
if pr.Base != nil {
m["base"] = pr.Base.Ref
}
if len(pr.Labels) > 0 {
m["labels"] = labelNames(pr.Labels)
}
out = append(out, m)
}
return out
}
func slimReview(r *gitea_sdk.PullReview) map[string]any {
if r == nil {
return nil
}
return map[string]any{
"id": r.ID,
"state": r.State,
"body": r.Body,
"user": userLogin(r.Reviewer),
"comments_count": r.CodeCommentsCount,
"submitted_at": r.Submitted,
"html_url": r.HTMLURL,
"stale": r.Stale,
"official": r.Official,
"dismissed": r.Dismissed,
}
}
func slimReviews(reviews []*gitea_sdk.PullReview) []map[string]any {
out := make([]map[string]any, 0, len(reviews))
for _, r := range reviews {
out = append(out, slimReview(r))
}
return out
}
func slimReviewComment(c *gitea_sdk.PullReviewComment) map[string]any {
if c == nil {
return nil
}
return map[string]any{
"id": c.ID,
"body": c.Body,
"path": c.Path,
"position": c.LineNum,
"old_position": c.OldLineNum,
"diff_hunk": c.DiffHunk,
"user": userLogin(c.Reviewer),
"html_url": c.HTMLURL,
"created_at": c.Created,
"updated_at": c.Updated,
}
}
func slimReviewComments(comments []*gitea_sdk.PullReviewComment) []map[string]any {
out := make([]map[string]any, 0, len(comments))
for _, c := range comments {
out = append(out, slimReviewComment(c))
}
return out
}

124
operation/pull/slim_test.go Normal file
View File

@@ -0,0 +1,124 @@
package pull
import (
"testing"
"time"
gitea_sdk "code.gitea.io/sdk/gitea"
)
func TestSlimPullRequest(t *testing.T) {
now := time.Now()
additions := 10
deletions := 5
changedFiles := 3
pr := &gitea_sdk.PullRequest{
Index: 1,
Title: "Fix bug",
Body: "Fixes #123",
State: "open",
Draft: false,
HasMerged: false,
Mergeable: true,
HTMLURL: "https://gitea.com/org/repo/pulls/1",
Poster: &gitea_sdk.User{UserName: "bob"},
Labels: []*gitea_sdk.Label{
{Name: "bug"},
{Name: "priority"},
},
Comments: 2,
Created: &now,
Updated: &now,
Additions: &additions,
Deletions: &deletions,
ChangedFiles: &changedFiles,
Head: &gitea_sdk.PRBranchInfo{
Ref: "fix-branch",
Sha: "abc123",
},
Base: &gitea_sdk.PRBranchInfo{
Ref: "main",
Sha: "def456",
},
Assignees: []*gitea_sdk.User{
{UserName: "alice"},
},
Milestone: &gitea_sdk.Milestone{Title: "v1.0"},
}
m := slimPullRequest(pr)
if m["number"] != int64(1) {
t.Errorf("expected number 1, got %v", m["number"])
}
if m["title"] != "Fix bug" {
t.Errorf("expected title Fix bug, got %v", m["title"])
}
if m["user"] != "bob" {
t.Errorf("expected user bob, got %v", m["user"])
}
if m["additions"] != 10 {
t.Errorf("expected additions 10, got %v", m["additions"])
}
if m["milestone"] != "v1.0" {
t.Errorf("expected milestone v1.0, got %v", m["milestone"])
}
labels := m["labels"].([]string)
if len(labels) != 2 || labels[0] != "bug" {
t.Errorf("expected labels [bug priority], got %v", labels)
}
head := m["head"].(map[string]any)
if head["ref"] != "fix-branch" {
t.Errorf("expected head ref fix-branch, got %v", head["ref"])
}
assignees := m["assignees"].([]string)
if len(assignees) != 1 || assignees[0] != "alice" {
t.Errorf("expected assignees [alice], got %v", assignees)
}
// merged fields should not be present for unmerged PR
if _, ok := m["merged_at"]; ok {
t.Error("merged_at should not be present for unmerged PR")
}
}
func TestSlimPullRequests_ListIsSlimmer(t *testing.T) {
pr := &gitea_sdk.PullRequest{
Index: 1,
Title: "PR title",
State: "open",
HTMLURL: "https://gitea.com/org/repo/pulls/1",
Poster: &gitea_sdk.User{UserName: "bob"},
Body: "Full body text here",
Head: &gitea_sdk.PRBranchInfo{Ref: "feature"},
Base: &gitea_sdk.PRBranchInfo{Ref: "main"},
}
single := slimPullRequest(pr)
list := slimPullRequests([]*gitea_sdk.PullRequest{pr})
// Single has body, list does not
if _, ok := single["body"]; !ok {
t.Error("single PR should have body")
}
if _, ok := list[0]["body"]; ok {
t.Error("list PR should not have body")
}
// List has head as string ref, single has head as map
if _, ok := single["head"].(map[string]any); !ok {
t.Error("single PR head should be a map")
}
if list[0]["head"] != "feature" {
t.Errorf("list PR head should be string ref, got %v", list[0]["head"])
}
}
func TestSlimPullRequests_Nil(t *testing.T) {
if r := slimPullRequests(nil); len(r) != 0 {
t.Errorf("expected empty slice, got %v", r)
}
}