package pull import ( "context" "fmt" "net/url" "strings" "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/slim" "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) var Tool = tool.New() const ( ListRepoPullRequestsToolName = "list_pull_requests" PullRequestReadToolName = "pull_request_read" PullRequestWriteToolName = "pull_request_write" PullRequestReviewWriteToolName = "pull_request_review_write" ) var ( ListRepoPullRequestsTool = mcp.NewTool( ListRepoPullRequestsToolName, mcp.WithDescription("List pull requests in a repository, filterable by state and milestone, with configurable sort order (e.g. recently updated, most commented)."), mcp.WithToolAnnotation(annotation.ReadOnly("List pull requests")), mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), mcp.WithString("state", mcp.Enum("open", "closed", "all"), mcp.DefaultString("all")), mcp.WithString("sort", mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")), mcp.WithNumber("milestone"), mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), ) PullRequestReadTool = mcp.NewTool( PullRequestReadToolName, mcp.WithDescription("Read pull request: details, diff, changed files, head commit status, reviews."), mcp.WithToolAnnotation(annotation.ReadOnly("Read pull request details")), mcp.WithString("method", mcp.Required(), mcp.Enum("get", "get_diff", "get_files", "get_status", "get_reviews", "get_review", "get_review_comments")), mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), mcp.WithNumber("pull_number", mcp.Required()), mcp.WithNumber("review_id", mcp.Description("for 'get_review'/'get_review_comments'")), mcp.WithBoolean("binary", mcp.Description("include binary diff")), mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), ) PullRequestWriteTool = mcp.NewTool( PullRequestWriteToolName, mcp.WithDescription("Write pull requests: create, update, close, reopen, merge, update branch from base, manage reviewers."), mcp.WithToolAnnotation(annotation.Write("Create, update, close, reopen, or merge pull requests")), mcp.WithString("method", mcp.Required(), mcp.Enum("create", "update", "close", "reopen", "merge", "update_branch", "add_reviewers", "remove_reviewers")), mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), mcp.WithNumber("pull_number", mcp.Description("required except for 'create'")), mcp.WithString("title", mcp.Description("required for 'create'; optional for 'update'/'merge'")), mcp.WithString("body", mcp.Description("required for 'create'; optional for 'update'")), mcp.WithString("head", mcp.Description("head branch (required for 'create')")), mcp.WithString("base", mcp.Description("base branch (required for 'create')")), mcp.WithString("assignee", mcp.Description("for 'update'")), mcp.WithArray("assignees", mcp.Description("for 'update'"), mcp.Items(map[string]any{"type": "string"})), mcp.WithNumber("milestone", mcp.Description("for 'update'")), mcp.WithString("state", mcp.Description("for 'update'"), mcp.Enum("open", "closed")), mcp.WithBoolean("allow_maintainer_edit", mcp.Description("for 'update'")), mcp.WithArray("labels", mcp.Description("label IDs"), mcp.Items(map[string]any{"type": "number"})), mcp.WithString("deadline", mcp.Description("ISO 8601")), mcp.WithBoolean("remove_deadline", mcp.Description("for 'update'")), mcp.WithString("merge_style", mcp.Description("for 'merge'"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")), mcp.WithString("message", mcp.Description("merge commit message or dismissal reason")), mcp.WithBoolean("delete_branch", mcp.Description("for 'merge'")), mcp.WithBoolean("force_merge", mcp.Description("merge even if checks fail")), mcp.WithBoolean("merge_when_checks_succeed", mcp.Description("for 'merge'")), mcp.WithString("head_commit_id", mcp.Description("expected head SHA for conflict detection")), mcp.WithArray("reviewers", mcp.Description("for 'add_reviewers'/'remove_reviewers'"), mcp.Items(map[string]any{"type": "string"})), mcp.WithArray("team_reviewers", mcp.Description("for 'add_reviewers'/'remove_reviewers'"), mcp.Items(map[string]any{"type": "string"})), mcp.WithBoolean("draft", mcp.Description("uses 'WIP: ' title prefix")), ) PullRequestReviewWriteTool = mcp.NewTool( PullRequestReviewWriteToolName, mcp.WithDescription("Write PR reviews: create, submit, delete, dismiss."), mcp.WithToolAnnotation(annotation.Write("Submit a pull request review")), mcp.WithString("method", mcp.Required(), mcp.Enum("create", "submit", "delete", "dismiss")), mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), mcp.WithNumber("pull_number", mcp.Required()), mcp.WithNumber("review_id", mcp.Description("required except for 'create'")), mcp.WithString("state", mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")), mcp.WithString("body"), mcp.WithString("commit_id", mcp.Description("for 'create'")), mcp.WithString("message", mcp.Description("dismissal reason")), mcp.WithArray("comments", mcp.Description("inline comments (for 'create')"), mcp.Items(map[string]any{ "type": "object", "properties": map[string]any{ "path": map[string]any{"type": "string"}, "body": map[string]any{"type": "string"}, "old_line_num": map[string]any{"type": "number", "description": "old-file line (deletions)"}, "new_line_num": map[string]any{"type": "number", "description": "new-file line (additions)"}, }, })), ) ) func init() { Tool.RegisterRead(server.ServerTool{ Tool: ListRepoPullRequestsTool, Handler: listRepoPullRequestsFn, }) Tool.RegisterRead(server.ServerTool{ Tool: PullRequestReadTool, Handler: pullRequestReadFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: PullRequestWriteTool, Handler: pullRequestWriteFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: PullRequestReviewWriteTool, Handler: pullRequestReviewWriteFn, }) } func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { method, err := params.GetString(req.GetArguments(), "method") if err != nil { return to.ErrorResult(err) } switch method { case "get": return getPullRequestByIndexFn(ctx, req) case "get_diff": return getPullRequestDiffFn(ctx, req) case "get_files": return getPullRequestFilesFn(ctx, req) case "get_status": return getPullRequestStatusFn(ctx, req) case "get_reviews": return listPullRequestReviewsFn(ctx, req) case "get_review": return getPullRequestReviewFn(ctx, req) case "get_review_comments": return listPullRequestReviewCommentsFn(ctx, req) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { method, err := params.GetString(req.GetArguments(), "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": return createPullRequestFn(ctx, req) case "update": return editPullRequestFn(ctx, req) case "close": return closePullRequestFn(ctx, req) case "reopen": return reopenPullRequestFn(ctx, req) case "merge": return mergePullRequestFn(ctx, req) case "update_branch": return updatePullRequestBranchFn(ctx, req) case "add_reviewers": return createPullRequestReviewerFn(ctx, req) case "remove_reviewers": return deletePullRequestReviewerFn(ctx, req) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } func closePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(req.GetArguments(), "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "pull_number") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } state := gitea_sdk.StateClosed pr, _, err := client.PullRequests.EditPullRequest(ctx, owner, repo, index, gitea_sdk.EditPullRequestOption{ State: &state, }) if err != nil { return to.ErrorResult(fmt.Errorf("close %v/%v/pr/%v err: %v", owner, repo, index, err)) } return to.TextResult(slimPullRequest(pr)) } func reopenPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(req.GetArguments(), "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "pull_number") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } state := gitea_sdk.StateOpen pr, _, err := client.PullRequests.EditPullRequest(ctx, owner, repo, index, gitea_sdk.EditPullRequestOption{ State: &state, }) if err != nil { return to.ErrorResult(fmt.Errorf("reopen %v/%v/pr/%v err: %v", owner, repo, index, err)) } return to.TextResult(slimPullRequest(pr)) } func pullRequestReviewWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { method, err := params.GetString(req.GetArguments(), "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": return createPullRequestReviewFn(ctx, req) case "submit": return submitPullRequestReviewFn(ctx, req) case "delete": return deletePullRequestReviewFn(ctx, req) case "dismiss": return dismissPullRequestReviewFn(ctx, req) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } func getPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } pr, _, err := client.PullRequests.GetPullRequest(ctx, owner, repo, index) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err)) } // /pulls/{n} omits `assets`; PRs are issues internally, so the issue // assets endpoint surfaces description attachments. var assets []*gitea_sdk.Attachment assetsPath := fmt.Sprintf("repos/%s/%s/issues/%d/assets", url.PathEscape(owner), url.PathEscape(repo), index) if _, err := gitea.DoJSON(ctx, "GET", assetsPath, nil, nil, &assets); err != nil { log.Debugf("fetch %v/%v/issues/%v/assets err: %v", owner, repo, index, err) } m := slimPullRequest(pr) m["body"] = slim.BodyWithAttachments(pr.Body, assets) return to.TextResult(m) } func getPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } binary, _ := args["binary"].(bool) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } diffBytes, _, err := client.PullRequests.GetPullRequestDiff(ctx, owner, repo, index, gitea_sdk.PullRequestDiffOptions{ Binary: binary, }) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err)) } return to.TextResult(string(diffBytes)) } func listRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } 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: page, PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } pullRequests, _, err := client.PullRequests.ListRepoPullRequests(ctx, owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err)) } return to.TextResult(slimPullRequests(pullRequests)) } // defaultWIPPrefixes are the default Gitea title prefixes that mark a PR as // work-in-progress / draft. Gitea matches these case-insensitively. var defaultWIPPrefixes = []string{"WIP:", "[WIP]"} // applyDraftPrefix adds or removes a WIP title prefix that Gitea uses to mark // pull requests as drafts. When the title already carries a recognized prefix // and isDraft is true, the title is returned unchanged to avoid normalization. func applyDraftPrefix(title string, isDraft bool) string { for _, prefix := range defaultWIPPrefixes { if len(title) >= len(prefix) && strings.EqualFold(title[:len(prefix)], prefix) { if isDraft { return title } return strings.TrimLeft(title[len(prefix):], " ") } } if isDraft { return "WIP: " + title } return title } func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } title, err := params.GetString(args, "title") if err != nil { return to.ErrorResult(err) } body, err := params.GetString(args, "body") if err != nil { return to.ErrorResult(err) } head, err := params.GetString(args, "head") if err != nil { return to.ErrorResult(err) } base, err := params.GetString(args, "base") if err != nil { return to.ErrorResult(err) } if draft, ok := args["draft"].(bool); ok { title = applyDraftPrefix(title, draft) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } opt := gitea_sdk.CreatePullRequestOption{ Title: title, Body: body, Head: head, Base: base, } if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil { opt.Labels = labelIDs } opt.Deadline = params.GetOptionalTime(args, "deadline") pr, _, err := client.PullRequests.CreatePullRequest(ctx, owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err)) } return to.TextResult(slimPullRequest(pr)) } type reviewerOp func(client *gitea_sdk.PullRequestsService, ctx context.Context, owner, repo string, index int64, opt gitea_sdk.PullReviewRequestOptions) (*gitea_sdk.Response, error) func pullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest, verb string, op reviewerOp) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } reviewers := params.GetStringSlice(args, "reviewers") teamReviewers := params.GetStringSlice(args, "team_reviewers") client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } if _, err := op(client.PullRequests, ctx, owner, repo, index, gitea_sdk.PullReviewRequestOptions{ Reviewers: reviewers, TeamReviewers: teamReviewers, }); err != nil { return to.ErrorResult(fmt.Errorf("%s review requests for %v/%v/pr/%v err: %v", verb, owner, repo, index, err)) } return to.TextResult(map[string]any{ "message": fmt.Sprintf("Successfully %sd review requests", verb), "reviewers": reviewers, "team_reviewers": teamReviewers, "pr_index": index, "repository": fmt.Sprintf("%s/%s", owner, repo), }) } func createPullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { return pullRequestReviewerFn(ctx, req, "create", (*gitea_sdk.PullRequestsService).CreateReviewRequests) } func deletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { return pullRequestReviewerFn(ctx, req, "delete", (*gitea_sdk.PullRequestsService).DeleteReviewRequests) } func listPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } reviews, _, err := client.PullRequests.ListPullReviews(ctx, owner, repo, index, gitea_sdk.ListPullReviewsOptions{ ListOptions: gitea_sdk.ListOptions{ 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(slimReviews(reviews)) } func getPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } review, _, err := client.PullRequests.GetPullReview(ctx, owner, repo, index, reviewID) if err != nil { return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } return to.TextResult(slimReview(review)) } func listPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } comments, _, err := client.PullRequests.ListPullReviewComments(ctx, owner, repo, index, reviewID) if err != nil { 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(slimReviewComments(comments)) } func createPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } opt := gitea_sdk.CreatePullReviewOptions{} if state, ok := args["state"].(string); ok { opt.State = gitea_sdk.ReviewStateType(state) } if body, ok := args["body"].(string); ok { opt.Body = body } if commitID, ok := args["commit_id"].(string); ok { opt.CommitID = commitID } // Parse inline comments if commentsArg, exists := args["comments"]; exists { if commentsSlice, ok := commentsArg.([]any); ok { for _, comment := range commentsSlice { if commentMap, ok := comment.(map[string]any); ok { reviewComment := gitea_sdk.CreatePullReviewComment{} if path, ok := commentMap["path"].(string); ok { reviewComment.Path = path } if body, ok := commentMap["body"].(string); ok { reviewComment.Body = body } if oldLineNum, ok := params.ToInt64(commentMap["old_line_num"]); ok { reviewComment.OldLineNum = oldLineNum } if newLineNum, ok := params.ToInt64(commentMap["new_line_num"]); ok { reviewComment.NewLineNum = newLineNum } opt.Comments = append(opt.Comments, reviewComment) } } } } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } review, _, err := client.PullRequests.CreatePullReview(ctx, owner, repo, index, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, index, err)) } return to.TextResult(slimReview(review)) } func submitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") 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 := args["body"].(string); ok { opt.Body = body } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } review, _, err := client.PullRequests.SubmitPullReview(ctx, owner, repo, index, reviewID, opt) if err != nil { return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } return to.TextResult(slimReview(review)) } func deletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } _, err = client.PullRequests.DeletePullReview(ctx, owner, repo, index, reviewID) if err != nil { return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } successMsg := map[string]any{ "message": "Successfully deleted review", "review_id": reviewID, "pr_index": index, "repository": fmt.Sprintf("%s/%s", owner, repo), } return to.TextResult(successMsg) } func dismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") 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 := args["message"].(string); ok { opt.Message = message } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } _, err = client.PullRequests.DismissPullReview(ctx, owner, repo, index, reviewID, opt) if err != nil { return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } successMsg := map[string]any{ "message": "Successfully dismissed review", "review_id": reviewID, "pr_index": index, "repository": fmt.Sprintf("%s/%s", owner, repo), } return to.TextResult(successMsg) } func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } 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 { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } forceMerge, _ := args["force_merge"].(bool) mergeWhenChecksSucceed, _ := args["merge_when_checks_succeed"].(bool) headCommitID, _ := args["head_commit_id"].(string) deleteBranchAfterMerge := &deleteBranch opt := gitea_sdk.MergePullRequestOption{ Style: gitea_sdk.MergeStyle(mergeStyle), Title: title, Message: message, DeleteBranchAfterMerge: deleteBranchAfterMerge, ForceMerge: forceMerge, MergeWhenChecksSucceed: mergeWhenChecksSucceed, HeadCommitId: headCommitID, } merged, resp, err := client.PullRequests.MergePullRequest(ctx, owner, repo, index, opt) if err != nil { return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v err: %v", owner, repo, index, err)) } if !merged && resp != nil && resp.StatusCode >= 400 { return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v failed: HTTP %d %s", owner, repo, index, resp.StatusCode, resp.Status)) } if !merged { return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v returned merged=false", owner, repo, index)) } successMsg := map[string]any{ "merged": merged, "pr_index": index, "repository": fmt.Sprintf("%s/%s", owner, repo), "merge_style": mergeStyle, "branch_deleted": deleteBranch, } return to.TextResult(successMsg) } func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } opt := gitea_sdk.EditPullRequestOption{} if title, ok := args["title"].(string); ok { opt.Title = title } if draft, ok := args["draft"].(bool); ok { if opt.Title == "" { // Fetch current title so the caller doesn't have to provide it // just to toggle draft status. client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } pr, _, err := client.PullRequests.GetPullRequest(ctx, owner, repo, index) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err)) } opt.Title = pr.Title } opt.Title = applyDraftPrefix(opt.Title, draft) } opt.Body = params.GetPresentStringPtr(args, "body") opt.AllowMaintainerEdit = params.GetOptionalBoolPtr(args, "allow_maintainer_edit") opt.RemoveDeadline = params.GetOptionalBoolPtr(args, "remove_deadline") opt.Deadline = params.GetOptionalTime(args, "deadline") if base, ok := args["base"].(string); ok { opt.Base = base } if assignee, ok := args["assignee"].(string); ok { opt.Assignee = assignee } if assignees := params.GetStringSlice(args, "assignees"); assignees != nil { opt.Assignees = assignees } if val, exists := args["milestone"]; exists { if milestone, ok := params.ToInt64(val); ok { opt.Milestone = milestone } } if state, ok := args["state"].(string); ok { s := gitea_sdk.StateType(state) opt.State = &s } if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil { opt.Labels = labelIDs } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } pr, _, err := client.PullRequests.EditPullRequest(ctx, owner, repo, index, opt) if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/%v/pr/%v err: %v", owner, repo, index, err)) } return to.TextResult(slimPullRequest(pr)) } func updatePullRequestBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } path := fmt.Sprintf("repos/%s/%s/pulls/%d/update", url.PathEscape(owner), url.PathEscape(repo), index) if _, err := gitea.DoJSON(ctx, "POST", path, nil, nil, nil); err != nil { return to.ErrorResult(fmt.Errorf("update %v/%v/pr/%v branch err: %v", owner, repo, index, err)) } return to.TextResult(map[string]any{"message": "branch updated from base"}) } func getPullRequestFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } files, _, err := client.PullRequests.ListPullRequestFiles(ctx, owner, repo, index, gitea_sdk.ListPullRequestFilesOptions{ ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, }) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v files err: %v", owner, repo, index, err)) } return to.TextResult(files) } func getPullRequestStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } pr, _, err := client.PullRequests.GetPullRequest(ctx, owner, repo, index) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err)) } if pr.Head == nil || pr.Head.Sha == "" { return to.ErrorResult(fmt.Errorf("pr %v/%v/%v has no head SHA", owner, repo, index)) } status, _, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, pr.Head.Sha) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v status err: %v", owner, repo, index, err)) } return to.TextResult(status) }