-
Notifications
You must be signed in to change notification settings - Fork 798
Split PR review creation, commenting and submission, and deletion #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Split PR review creation, commenting and submission, and deletion #381
Conversation
8ef3228
to
85e18a9
Compare
Thank you for this @williammartin! I would say that yes, this is worth shipping and getting feedback from. It's better to be a bit more limited (and have a more complex backend api dance) than to break on launch because of JSON schema conflicts imho. |
Ok thanks, I'll move it forward to a place where I think it's acceptable and then ask for any final comments. |
I think the direction sounds great. I'd love to know how well models navigate it. We could have simple I can't wait to try this out. |
@williammartin I think we need a cancel draft pull request review too, so there's a way to get past the fact you can only have one at a time issue. |
9419821
to
a233900
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a test review of a tool in this PR itself.
@toby @SamMorrowDrums please play around with the changes in this PR. The delete review tool has been added. I haven't written unit tests yet, but you should look at the e2e tests to understand the flow. |
@@ -87,11 +89,21 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { | |||
return ghClient, nil // closing over client | |||
} | |||
|
|||
getGQLClient := func(_ context.Context) (*githubv4.Client, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Make enterprise ready.
|
||
type constrainableInt32 int32 | ||
|
||
func (ci *constrainableInt32) Constrain(param any) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'm not sure if this is way overengineered or not, but it facilitates constraining param values: https://github.com/github/github-mcp-server/pull/381/files#diff-c2e2a7250ef597d08ee5d429159bc89c4b862a7ed6cbcf30b8231a3ec599d190R1081
@@ -75,8 +76,123 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) | |||
} | |||
} | |||
|
|||
// CreatePullRequest creates a tool to create a new pull request. | |||
func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just moved from below to a more sensible location in the file, putting the review tools together in one place.
a233900
to
f345fcb
Compare
@@ -65,10 +67,16 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, | |||
AddWriteTools( | |||
toolsets.NewServerTool(MergePullRequest(getClient, t)), | |||
toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), | |||
toolsets.NewServerTool(CreatePullRequestReview(getClient, t)), | |||
toolsets.NewServerTool(CreatePullRequest(getClient, t)), | |||
toolsets.NewServerTool(UpdatePullRequest(getClient, t)), | |||
toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: decide what to do with this standalone tool. I think it can just be removed in favour of create pending + add comment.
mcp.WithString("subjectType", | ||
mcp.Required(), | ||
mcp.Description("Branch to merge into"), | ||
mcp.Description("The level at which the comment is targeted"), | ||
mcp.Enum("FILE", "LINE"), | ||
), | ||
mcp.WithBoolean("draft", | ||
mcp.Description("Create as draft PR"), | ||
mcp.WithNumber("line", | ||
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"), | ||
), | ||
mcp.WithBoolean("maintainer_can_modify", | ||
mcp.Description("Allow maintainer edits"), | ||
mcp.WithString("side", | ||
mcp.Description("The side of the diff to comment on"), | ||
mcp.Enum("LEFT", "RIGHT"), | ||
), | ||
mcp.WithNumber("startLine", | ||
mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), | ||
), | ||
mcp.WithString("startSide", | ||
mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to"), | ||
mcp.Enum("LEFT", "RIGHT"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not really clear to me whether this actually works with Gemini, but it matches the other existing Tool AddPullRequestReviewComment
that doesn't seem to cause gemini to barf.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a test from gemini model
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a test from gemini model
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a test from gemini model
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a test from gemini model
line, err := OptionalParam[constrainableInt32](request, "line") | ||
if err != nil { | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
|
||
maintainerCanModify, err := OptionalParam[bool](request, "maintainer_can_modify") | ||
side, err := OptionalParam[string](request, "side") | ||
if err != nil { | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
|
||
newPR := &github.NewPullRequest{ | ||
Title: github.Ptr(title), | ||
Head: github.Ptr(head), | ||
Base: github.Ptr(base), | ||
startLine, err := OptionalParam[constrainableInt32](request, "startLine") | ||
if err != nil { | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
|
||
if body != "" { | ||
newPR.Body = github.Ptr(body) | ||
startSide, err := OptionalParam[string](request, "startSide") | ||
if err != nil { | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
|
||
newPR.Draft = github.Ptr(draft) | ||
newPR.MaintainerCanModify = github.Ptr(maintainerCanModify) | ||
client, err := getGQLClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: A bit more e2e validation (and probably a manual check) of these working as expected
Description
Relates to: #343
@toby @SamMorrowDrums before I go any further cleaning up the new and old tools, handling edge cases and adding unit, I want to get your feedback on this approach, since there have been quite a few pain points along the way.
The original issue suggested removing
comments
fromcreate_pull_request_review
and depending onadd_pull_request_review_comment
, however,add_pull_request_review_comment
uses the REST API which is only able to create a single comment and only if a review is not already in progress.Thus, we need to lean on GraphQL here to:
mvp_create_pending_pull_request_review
)mvp_add_pull_request_review_comment_to_pending_review
)mvp_submit_pull_request_review
)The implicit requirement here is that a user may only have one pending review at a time, so if we get their most recent review on a PR, and it is pending, we can add a comment to that. This does not allow commenting on other reviews and it does not allow commenting on already submitted reviews, which could be further enhancements in the same or in different tools.
There is an additional challenge since we are now using GraphQL which is that for the most part, queries and mutations operate on GraphQL IDs (
node_id
in the REST API). So for example, when we create a pending review, there is areviewId
that is required for commenting. Within a single session, it would be easy to return theid
in thetextContent
but this:For now, I've decided with these very explicit tools (e.g.
mvp_add_pull_request_review_comment_to_pending_review
) to allow the model to be API implementation agnostic, and that the tool will internally do the lookup. For example, since we know there can only be one review for the user, knowing theowner
,repo
andpr
number seems to be enough to look up the most recent review. The downside is that it's a bit more restrictive and requires more internal API lookups than an alternative which might be to expose aget_pending_review
and allow the model to figure it out. If you asked me I would ship this and wait for feedback on when it sucks or should be enhanced later but I don't have much confidence in my intuition here.Cheers.
Currently passes e2e tests: