func CreatePullRequestReview()

in pkg/github/pullrequests.go [920:1133]


func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
	return mcp.NewTool("create_pull_request_review",
			mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a review for a pull request.")),
			mcp.WithToolAnnotation(mcp.ToolAnnotation{
				Title:        t("TOOL_CREATE_PULL_REQUEST_REVIEW_USER_TITLE", "Submit pull request review"),
				ReadOnlyHint: false,
			}),
			mcp.WithString("owner",
				mcp.Required(),
				mcp.Description("Repository owner"),
			),
			mcp.WithString("repo",
				mcp.Required(),
				mcp.Description("Repository name"),
			),
			mcp.WithNumber("pullNumber",
				mcp.Required(),
				mcp.Description("Pull request number"),
			),
			mcp.WithString("body",
				mcp.Description("Review comment text"),
			),
			mcp.WithString("event",
				mcp.Required(),
				mcp.Description("Review action to perform"),
				mcp.Enum("APPROVE", "REQUEST_CHANGES", "COMMENT"),
			),
			mcp.WithString("commitId",
				mcp.Description("SHA of commit to review"),
			),
			mcp.WithArray("comments",
				mcp.Items(
					map[string]interface{}{
						"type":                 "object",
						"additionalProperties": false,
						"required":             []string{"path", "body", "position", "line", "side", "start_line", "start_side"},
						"properties": map[string]interface{}{
							"path": map[string]interface{}{
								"type":        "string",
								"description": "path to the file",
							},
							"position": map[string]interface{}{
								"anyOf": []interface{}{
									map[string]string{"type": "number"},
									map[string]string{"type": "null"},
								},
								"description": "position of the comment in the diff",
							},
							"line": map[string]interface{}{
								"anyOf": []interface{}{
									map[string]string{"type": "number"},
									map[string]string{"type": "null"},
								},
								"description": "line number in the file to comment on. For multi-line comments, the end of the line range",
							},
							"side": map[string]interface{}{
								"anyOf": []interface{}{
									map[string]string{"type": "string"},
									map[string]string{"type": "null"},
								},
								"description": "The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. (LEFT or RIGHT)",
							},
							"start_line": map[string]interface{}{
								"anyOf": []interface{}{
									map[string]string{"type": "number"},
									map[string]string{"type": "null"},
								},
								"description": "The first line of the range to which the comment refers. Required for multi-line comments.",
							},
							"start_side": map[string]interface{}{
								"anyOf": []interface{}{
									map[string]string{"type": "string"},
									map[string]string{"type": "null"},
								},
								"description": "The side of the diff on which the start line resides for multi-line comments. (LEFT or RIGHT)",
							},
							"body": map[string]interface{}{
								"type":        "string",
								"description": "comment body",
							},
						},
					},
				),
				mcp.Description("Line-specific comments array of objects to place comments on pull request changes. Requires path and body. For line comments use line or position. For multi-line comments use start_line and line with optional side parameters."),
			),
		),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			owner, err := requiredParam[string](request, "owner")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			repo, err := requiredParam[string](request, "repo")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			pullNumber, err := RequiredInt(request, "pullNumber")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			event, err := requiredParam[string](request, "event")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}

			// Create review request
			reviewRequest := &github.PullRequestReviewRequest{
				Event: github.Ptr(event),
			}

			// Add body if provided
			body, err := OptionalParam[string](request, "body")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			if body != "" {
				reviewRequest.Body = github.Ptr(body)
			}

			// Add commit ID if provided
			commitID, err := OptionalParam[string](request, "commitId")
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			if commitID != "" {
				reviewRequest.CommitID = github.Ptr(commitID)
			}

			// Add comments if provided
			if commentsObj, ok := request.Params.Arguments["comments"].([]interface{}); ok && len(commentsObj) > 0 {
				comments := []*github.DraftReviewComment{}

				for _, c := range commentsObj {
					commentMap, ok := c.(map[string]interface{})
					if !ok {
						return mcp.NewToolResultError("each comment must be an object with path and body"), nil
					}

					path, ok := commentMap["path"].(string)
					if !ok || path == "" {
						return mcp.NewToolResultError("each comment must have a path"), nil
					}

					body, ok := commentMap["body"].(string)
					if !ok || body == "" {
						return mcp.NewToolResultError("each comment must have a body"), nil
					}

					_, hasPosition := commentMap["position"].(float64)
					_, hasLine := commentMap["line"].(float64)
					_, hasSide := commentMap["side"].(string)
					_, hasStartLine := commentMap["start_line"].(float64)
					_, hasStartSide := commentMap["start_side"].(string)

					switch {
					case !hasPosition && !hasLine:
						return mcp.NewToolResultError("each comment must have either position or line"), nil
					case hasPosition && (hasLine || hasSide || hasStartLine || hasStartSide):
						return mcp.NewToolResultError("position cannot be combined with line, side, start_line, or start_side"), nil
					case hasStartSide && !hasSide:
						return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil
					}

					comment := &github.DraftReviewComment{
						Path: github.Ptr(path),
						Body: github.Ptr(body),
					}

					if positionFloat, ok := commentMap["position"].(float64); ok {
						comment.Position = github.Ptr(int(positionFloat))
					} else if lineFloat, ok := commentMap["line"].(float64); ok {
						comment.Line = github.Ptr(int(lineFloat))
					}
					if side, ok := commentMap["side"].(string); ok {
						comment.Side = github.Ptr(side)
					}
					if startLineFloat, ok := commentMap["start_line"].(float64); ok {
						comment.StartLine = github.Ptr(int(startLineFloat))
					}
					if startSide, ok := commentMap["start_side"].(string); ok {
						comment.StartSide = github.Ptr(startSide)
					}

					comments = append(comments, comment)
				}

				reviewRequest.Comments = comments
			}

			client, err := getClient(ctx)
			if err != nil {
				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
			}
			review, resp, err := client.PullRequests.CreateReview(ctx, owner, repo, pullNumber, reviewRequest)
			if err != nil {
				return nil, fmt.Errorf("failed to create pull request review: %w", err)
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusOK {
				body, err := io.ReadAll(resp.Body)
				if err != nil {
					return nil, fmt.Errorf("failed to read response body: %w", err)
				}
				return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request review: %s", string(body))), nil
			}

			r, err := json.Marshal(review)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal response: %w", err)
			}

			return mcp.NewToolResultText(string(r)), nil
		}
}