MCP clients (e.g. Claude Code) may serialize number parameters as JSON strings instead of JSON numbers. The Go type assertion .(float64) silently returns 0 for string values, causing "issue does not exist [index: 0]" errors. Add to.Float64() helper that handles float64, int, int64, and string types via strconv.ParseFloat. Replace all 62 bare .(float64) assertions across 9 operation files with to.Float64() calls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
239 lines
9.2 KiB
Go
239 lines
9.2 KiB
Go
package pull
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/params"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/log"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/to"
|
|
|
|
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
const (
|
|
CreatePullReviewToolName = "create_pull_review"
|
|
SubmitPullReviewToolName = "submit_pull_review"
|
|
DismissPullReviewToolName = "dismiss_pull_review"
|
|
DeletePullReviewToolName = "delete_pull_review"
|
|
CreateReviewRequestsToolName = "create_review_requests"
|
|
DeleteReviewRequestsToolName = "delete_review_requests"
|
|
)
|
|
|
|
var (
|
|
CreatePullReviewTool = mcp.NewTool(
|
|
CreatePullReviewToolName,
|
|
mcp.WithDescription("Create a pull request review with optional inline comments"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
|
|
mcp.WithString("body", mcp.Description(params.ReviewBody)),
|
|
mcp.WithString("state", mcp.Required(), mcp.Description(params.ReviewState)),
|
|
mcp.WithString("comments", mcp.Description(params.ReviewComments)),
|
|
)
|
|
|
|
SubmitPullReviewTool = mcp.NewTool(
|
|
SubmitPullReviewToolName,
|
|
mcp.WithDescription("Submit a pending pull request review"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
|
|
mcp.WithNumber("id", mcp.Required(), mcp.Description(params.ReviewID)),
|
|
mcp.WithString("body", mcp.Description(params.ReviewBody)),
|
|
mcp.WithString("state", mcp.Required(), mcp.Description(params.ReviewState)),
|
|
)
|
|
|
|
DismissPullReviewTool = mcp.NewTool(
|
|
DismissPullReviewToolName,
|
|
mcp.WithDescription("Dismiss a pull request review"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
|
|
mcp.WithNumber("id", mcp.Required(), mcp.Description(params.ReviewID)),
|
|
mcp.WithString("message", mcp.Required(), mcp.Description(params.DismissMessage)),
|
|
)
|
|
|
|
DeletePullReviewTool = mcp.NewTool(
|
|
DeletePullReviewToolName,
|
|
mcp.WithDescription("Delete a pending pull request review"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
|
|
mcp.WithNumber("id", mcp.Required(), mcp.Description(params.ReviewID)),
|
|
)
|
|
|
|
CreateReviewRequestsTool = mcp.NewTool(
|
|
CreateReviewRequestsToolName,
|
|
mcp.WithDescription("Request reviews from specific users or teams"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
|
|
mcp.WithString("reviewers", mcp.Description(params.Reviewers)),
|
|
mcp.WithString("team_reviewers", mcp.Description(params.TeamReviewers)),
|
|
)
|
|
|
|
DeleteReviewRequestsTool = mcp.NewTool(
|
|
DeleteReviewRequestsToolName,
|
|
mcp.WithDescription("Cancel pending review requests"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description(params.PRIndex)),
|
|
mcp.WithString("reviewers", mcp.Description(params.Reviewers)),
|
|
mcp.WithString("team_reviewers", mcp.Description(params.TeamReviewers)),
|
|
)
|
|
)
|
|
|
|
func RegisterReviewTools(s *server.MCPServer) {
|
|
s.AddTool(CreatePullReviewTool, CreatePullReviewFn)
|
|
s.AddTool(SubmitPullReviewTool, SubmitPullReviewFn)
|
|
s.AddTool(DismissPullReviewTool, DismissPullReviewFn)
|
|
s.AddTool(DeletePullReviewTool, DeletePullReviewFn)
|
|
s.AddTool(CreateReviewRequestsTool, CreateReviewRequestsFn)
|
|
s.AddTool(DeleteReviewRequestsTool, DeleteReviewRequestsFn)
|
|
}
|
|
|
|
func CreatePullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called CreatePullReviewFn")
|
|
owner, _ := req.GetArguments()["owner"].(string)
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
index, _ := to.Float64(req.GetArguments()["index"])
|
|
body, _ := req.GetArguments()["body"].(string)
|
|
state, _ := req.GetArguments()["state"].(string)
|
|
commentsJSON, _ := req.GetArguments()["comments"].(string)
|
|
|
|
opt := forgejo_sdk.CreatePullReviewOptions{
|
|
State: forgejo_sdk.ReviewStateType(state),
|
|
Body: body,
|
|
}
|
|
|
|
if commentsJSON != "" {
|
|
var comments []forgejo_sdk.CreatePullReviewComment
|
|
if err := json.Unmarshal([]byte(commentsJSON), &comments); err != nil {
|
|
return to.ErrorResult(fmt.Errorf("invalid comments JSON: %v", err))
|
|
}
|
|
opt.Comments = comments
|
|
}
|
|
|
|
review, _, err := forgejo.Client().CreatePullReview(owner, repo, int64(index), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create pull review err: %v", err))
|
|
}
|
|
return to.TextResult(review)
|
|
}
|
|
|
|
func SubmitPullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called SubmitPullReviewFn")
|
|
owner, _ := req.GetArguments()["owner"].(string)
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
index, _ := to.Float64(req.GetArguments()["index"])
|
|
id, _ := to.Float64(req.GetArguments()["id"])
|
|
body, _ := req.GetArguments()["body"].(string)
|
|
state, _ := req.GetArguments()["state"].(string)
|
|
|
|
opt := forgejo_sdk.SubmitPullReviewOptions{
|
|
State: forgejo_sdk.ReviewStateType(state),
|
|
Body: body,
|
|
}
|
|
|
|
review, _, err := forgejo.Client().SubmitPullReview(owner, repo, int64(index), int64(id), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("submit pull review err: %v", err))
|
|
}
|
|
return to.TextResult(review)
|
|
}
|
|
|
|
func DismissPullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called DismissPullReviewFn")
|
|
owner, _ := req.GetArguments()["owner"].(string)
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
index, _ := to.Float64(req.GetArguments()["index"])
|
|
id, _ := to.Float64(req.GetArguments()["id"])
|
|
message, _ := req.GetArguments()["message"].(string)
|
|
|
|
opt := forgejo_sdk.DismissPullReviewOptions{
|
|
Message: message,
|
|
}
|
|
|
|
_, err := forgejo.Client().DismissPullReview(owner, repo, int64(index), int64(id), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("dismiss pull review err: %v", err))
|
|
}
|
|
return mcp.NewToolResultText(`{"Result":"review dismissed successfully"}`), nil
|
|
}
|
|
|
|
func DeletePullReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called DeletePullReviewFn")
|
|
owner, _ := req.GetArguments()["owner"].(string)
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
index, _ := to.Float64(req.GetArguments()["index"])
|
|
id, _ := to.Float64(req.GetArguments()["id"])
|
|
|
|
_, err := forgejo.Client().DeletePullReview(owner, repo, int64(index), int64(id))
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("delete pull review err: %v", err))
|
|
}
|
|
return mcp.NewToolResultText(`{"Result":"review deleted successfully"}`), nil
|
|
}
|
|
|
|
func CreateReviewRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called CreateReviewRequestsFn")
|
|
owner, _ := req.GetArguments()["owner"].(string)
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
index, _ := to.Float64(req.GetArguments()["index"])
|
|
reviewers, _ := req.GetArguments()["reviewers"].(string)
|
|
teamReviewers, _ := req.GetArguments()["team_reviewers"].(string)
|
|
|
|
opt := forgejo_sdk.PullReviewRequestOptions{}
|
|
if reviewers != "" {
|
|
opt.Reviewers = splitCSV(reviewers)
|
|
}
|
|
if teamReviewers != "" {
|
|
opt.TeamReviewers = splitCSV(teamReviewers)
|
|
}
|
|
|
|
_, err := forgejo.Client().CreateReviewRequests(owner, repo, int64(index), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create review requests err: %v", err))
|
|
}
|
|
return mcp.NewToolResultText(`{"Result":"review requests created successfully"}`), nil
|
|
}
|
|
|
|
func DeleteReviewRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called DeleteReviewRequestsFn")
|
|
owner, _ := req.GetArguments()["owner"].(string)
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
index, _ := to.Float64(req.GetArguments()["index"])
|
|
reviewers, _ := req.GetArguments()["reviewers"].(string)
|
|
teamReviewers, _ := req.GetArguments()["team_reviewers"].(string)
|
|
|
|
opt := forgejo_sdk.PullReviewRequestOptions{}
|
|
if reviewers != "" {
|
|
opt.Reviewers = splitCSV(reviewers)
|
|
}
|
|
if teamReviewers != "" {
|
|
opt.TeamReviewers = splitCSV(teamReviewers)
|
|
}
|
|
|
|
_, err := forgejo.Client().DeleteReviewRequests(owner, repo, int64(index), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("delete review requests err: %v", err))
|
|
}
|
|
return mcp.NewToolResultText(`{"Result":"review requests deleted successfully"}`), nil
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
parts := strings.Split(s, ",")
|
|
result := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
trimmed := strings.TrimSpace(p)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|