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
}
}