Skip to content

Commit ba78e02

Browse files
authored
Merge branch 'github:main' into main
2 parents f966e30 + bbba3bb commit ba78e02

File tree

6 files changed

+413
-11
lines changed

6 files changed

+413
-11
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,21 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
344344
- `draft`: Create as draft PR (boolean, optional)
345345
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
346346

347+
- **add_pull_request_review_comment** - Add a review comment to a pull request or reply to an existing comment
348+
349+
- `owner`: Repository owner (string, required)
350+
- `repo`: Repository name (string, required)
351+
- `pull_number`: Pull request number (number, required)
352+
- `body`: The text of the review comment (string, required)
353+
- `commit_id`: The SHA of the commit to comment on (string, required unless using in_reply_to)
354+
- `path`: The relative path to the file that necessitates a comment (string, required unless using in_reply_to)
355+
- `line`: The line of the blob in the pull request diff that the comment applies to (number, optional)
356+
- `side`: The side of the diff to comment on (LEFT or RIGHT) (string, optional)
357+
- `start_line`: For multi-line comments, the first line of the range (number, optional)
358+
- `start_side`: For multi-line comments, the starting side of the diff (LEFT or RIGHT) (string, optional)
359+
- `subject_type`: The level at which the comment is targeted (line or file) (string, optional)
360+
- `in_reply_to`: The ID of the review comment to reply to (number, optional). When specified, only body is required and other parameters are ignored.
361+
347362
- **update_pull_request** - Update an existing pull request in a GitHub repository
348363

349364
- `owner`: Repository owner (string, required)

cmd/github-mcp-server/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
iolog "github.com/github/github-mcp-server/pkg/log"
1414
"github.com/github/github-mcp-server/pkg/translations"
1515
gogithub "github.com/google/go-github/v69/github"
16+
"github.com/mark3labs/mcp-go/mcp"
1617
"github.com/mark3labs/mcp-go/server"
1718
log "github.com/sirupsen/logrus"
1819
"github.com/spf13/cobra"
@@ -137,11 +138,19 @@ func runStdioServer(cfg runConfig) error {
137138

138139
t, dumpTranslations := translations.TranslationHelper()
139140

141+
beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) {
142+
ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s (%s/%s)", version, message.Params.ClientInfo.Name, message.Params.ClientInfo.Version)
143+
}
144+
140145
getClient := func(_ context.Context) (*gogithub.Client, error) {
141146
return ghClient, nil // closing over client
142147
}
148+
149+
hooks := &server.Hooks{
150+
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
151+
}
143152
// Create
144-
ghServer := github.NewServer(getClient, version, cfg.readOnly, t)
153+
ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks))
145154
stdioServer := server.NewStdioServer(ghServer)
146155

147156
stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0)

cmd/mcpcurl/main.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"crypto/rand"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -11,8 +12,6 @@ import (
1112
"slices"
1213
"strings"
1314

14-
"crypto/rand"
15-
1615
"github.com/spf13/cobra"
1716
"github.com/spf13/viper"
1817
)
@@ -161,7 +160,7 @@ func main() {
161160
_ = rootCmd.MarkPersistentFlagRequired("stdio-server-cmd")
162161

163162
// Add global flag for pretty printing
164-
rootCmd.PersistentFlags().Bool("pretty", true, "Pretty print MCP response (only for JSON responses)")
163+
rootCmd.PersistentFlags().Bool("pretty", true, "Pretty print MCP response (only for JSON or JSONL responses)")
165164

166165
// Add the tools command to the root command
167166
rootCmd.AddCommand(toolsCmd)
@@ -426,15 +425,26 @@ func printResponse(response string, prettyPrint bool) error {
426425
// Extract text from content items of type "text"
427426
for _, content := range resp.Result.Content {
428427
if content.Type == "text" {
429-
// Unmarshal the text content
430-
var textContent map[string]interface{}
431-
if err := json.Unmarshal([]byte(content.Text), &textContent); err != nil {
432-
return fmt.Errorf("failed to parse text content: %w", err)
428+
var textContentObj map[string]interface{}
429+
err := json.Unmarshal([]byte(content.Text), &textContentObj)
430+
431+
if err == nil {
432+
prettyText, err := json.MarshalIndent(textContentObj, "", " ")
433+
if err != nil {
434+
return fmt.Errorf("failed to pretty print text content: %w", err)
435+
}
436+
fmt.Println(string(prettyText))
437+
continue
438+
}
439+
440+
// Fallback parsing as JSONL
441+
var textContentList []map[string]interface{}
442+
if err := json.Unmarshal([]byte(content.Text), &textContentList); err != nil {
443+
return fmt.Errorf("failed to parse text content as a list: %w", err)
433444
}
434-
// Pretty print the text content
435-
prettyText, err := json.MarshalIndent(textContent, "", " ")
445+
prettyText, err := json.MarshalIndent(textContentList, "", " ")
436446
if err != nil {
437-
return fmt.Errorf("failed to pretty print text content: %w", err)
447+
return fmt.Errorf("failed to pretty print array content: %w", err)
438448
}
439449
fmt.Println(string(prettyText))
440450
}

pkg/github/pullrequests.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,176 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel
644644
}
645645
}
646646

647+
// AddPullRequestReviewComment creates a tool to add a review comment to a pull request.
648+
func AddPullRequestReviewComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
649+
return mcp.NewTool("add_pull_request_review_comment",
650+
mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a review comment to a pull request")),
651+
mcp.WithString("owner",
652+
mcp.Required(),
653+
mcp.Description("Repository owner"),
654+
),
655+
mcp.WithString("repo",
656+
mcp.Required(),
657+
mcp.Description("Repository name"),
658+
),
659+
mcp.WithNumber("pull_number",
660+
mcp.Required(),
661+
mcp.Description("Pull request number"),
662+
),
663+
mcp.WithString("body",
664+
mcp.Required(),
665+
mcp.Description("The text of the review comment"),
666+
),
667+
mcp.WithString("commit_id",
668+
mcp.Description("The SHA of the commit to comment on. Required unless in_reply_to is specified."),
669+
),
670+
mcp.WithString("path",
671+
mcp.Description("The relative path to the file that necessitates a comment. Required unless in_reply_to is specified."),
672+
),
673+
mcp.WithString("subject_type",
674+
mcp.Description("The level at which the comment is targeted, 'line' or 'file'"),
675+
mcp.Enum("line", "file"),
676+
),
677+
mcp.WithNumber("line",
678+
mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"),
679+
),
680+
mcp.WithString("side",
681+
mcp.Description("The side of the diff to comment on. Can be LEFT or RIGHT"),
682+
mcp.Enum("LEFT", "RIGHT"),
683+
),
684+
mcp.WithNumber("start_line",
685+
mcp.Description("For multi-line comments, the first line of the range that the comment applies to"),
686+
),
687+
mcp.WithString("start_side",
688+
mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. Can be LEFT or RIGHT"),
689+
mcp.Enum("LEFT", "RIGHT"),
690+
),
691+
mcp.WithNumber("in_reply_to",
692+
mcp.Description("The ID of the review comment to reply to. When specified, only body is required and all other parameters are ignored"),
693+
),
694+
),
695+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
696+
owner, err := requiredParam[string](request, "owner")
697+
if err != nil {
698+
return mcp.NewToolResultError(err.Error()), nil
699+
}
700+
repo, err := requiredParam[string](request, "repo")
701+
if err != nil {
702+
return mcp.NewToolResultError(err.Error()), nil
703+
}
704+
pullNumber, err := RequiredInt(request, "pull_number")
705+
if err != nil {
706+
return mcp.NewToolResultError(err.Error()), nil
707+
}
708+
body, err := requiredParam[string](request, "body")
709+
if err != nil {
710+
return mcp.NewToolResultError(err.Error()), nil
711+
}
712+
713+
client, err := getClient(ctx)
714+
if err != nil {
715+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
716+
}
717+
718+
// Check if this is a reply to an existing comment
719+
if replyToFloat, ok := request.Params.Arguments["in_reply_to"].(float64); ok {
720+
// Use the specialized method for reply comments due to inconsistency in underlying go-github library: https://github.com/google/go-github/pull/950
721+
commentID := int64(replyToFloat)
722+
createdReply, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, commentID)
723+
if err != nil {
724+
return nil, fmt.Errorf("failed to reply to pull request comment: %w", err)
725+
}
726+
defer func() { _ = resp.Body.Close() }()
727+
728+
if resp.StatusCode != http.StatusCreated {
729+
respBody, err := io.ReadAll(resp.Body)
730+
if err != nil {
731+
return nil, fmt.Errorf("failed to read response body: %w", err)
732+
}
733+
return mcp.NewToolResultError(fmt.Sprintf("failed to reply to pull request comment: %s", string(respBody))), nil
734+
}
735+
736+
r, err := json.Marshal(createdReply)
737+
if err != nil {
738+
return nil, fmt.Errorf("failed to marshal response: %w", err)
739+
}
740+
741+
return mcp.NewToolResultText(string(r)), nil
742+
}
743+
744+
// This is a new comment, not a reply
745+
// Verify required parameters for a new comment
746+
commitID, err := requiredParam[string](request, "commit_id")
747+
if err != nil {
748+
return mcp.NewToolResultError(err.Error()), nil
749+
}
750+
path, err := requiredParam[string](request, "path")
751+
if err != nil {
752+
return mcp.NewToolResultError(err.Error()), nil
753+
}
754+
755+
comment := &github.PullRequestComment{
756+
Body: github.Ptr(body),
757+
CommitID: github.Ptr(commitID),
758+
Path: github.Ptr(path),
759+
}
760+
761+
subjectType, err := OptionalParam[string](request, "subject_type")
762+
if err != nil {
763+
return mcp.NewToolResultError(err.Error()), nil
764+
}
765+
if subjectType != "file" {
766+
line, lineExists := request.Params.Arguments["line"].(float64)
767+
startLine, startLineExists := request.Params.Arguments["start_line"].(float64)
768+
side, sideExists := request.Params.Arguments["side"].(string)
769+
startSide, startSideExists := request.Params.Arguments["start_side"].(string)
770+
771+
if !lineExists {
772+
return mcp.NewToolResultError("line parameter is required unless using subject_type:file"), nil
773+
}
774+
775+
comment.Line = github.Ptr(int(line))
776+
if sideExists {
777+
comment.Side = github.Ptr(side)
778+
}
779+
if startLineExists {
780+
comment.StartLine = github.Ptr(int(startLine))
781+
}
782+
if startSideExists {
783+
comment.StartSide = github.Ptr(startSide)
784+
}
785+
786+
if startLineExists && !lineExists {
787+
return mcp.NewToolResultError("if start_line is provided, line must also be provided"), nil
788+
}
789+
if startSideExists && !sideExists {
790+
return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil
791+
}
792+
}
793+
794+
createdComment, resp, err := client.PullRequests.CreateComment(ctx, owner, repo, pullNumber, comment)
795+
if err != nil {
796+
return nil, fmt.Errorf("failed to create pull request comment: %w", err)
797+
}
798+
defer func() { _ = resp.Body.Close() }()
799+
800+
if resp.StatusCode != http.StatusCreated {
801+
respBody, err := io.ReadAll(resp.Body)
802+
if err != nil {
803+
return nil, fmt.Errorf("failed to read response body: %w", err)
804+
}
805+
return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request comment: %s", string(respBody))), nil
806+
}
807+
808+
r, err := json.Marshal(createdComment)
809+
if err != nil {
810+
return nil, fmt.Errorf("failed to marshal response: %w", err)
811+
}
812+
813+
return mcp.NewToolResultText(string(r)), nil
814+
}
815+
}
816+
647817
// GetPullRequestReviews creates a tool to get the reviews on a pull request.
648818
func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
649819
return mcp.NewTool("get_pull_request_reviews",

0 commit comments

Comments
 (0)