Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,14 @@ The following sets of tools are available (all are on by default):
- `title`: New title (string, optional)
- `type`: New issue type (string, optional)

- **get_latest_issue_comment_user** - Get latest issue comments by a user on a particular issue
- `issue_number`: Issue number (number, required)
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `repo`: Repository name (string, required)
- `user`: Github username of that particular user (string, required)

</details>

<details>
Expand Down
46 changes: 46 additions & 0 deletions pkg/github/__toolsnaps__/get_latest_issue_comment_user.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"annotations": {
"title": "Get latest issue comment by user",
"readOnlyHint": true
},
"description": "Get the latest comments by a specific user on a specific issue in a GitHub repository.",
"inputSchema": {
"properties": {
"issue_number": {
"description": "Issue number",
"type": "number"
},
"owner": {
"description": "Repository owner",
"type": "string"
},
"page": {
"description": "Page number for pagination (min 1)",
"minimum": 1,
"type": "number"
},
"perPage": {
"description": "Results per page for pagination (min 1, max 100)",
"maximum": 100,
"minimum": 1,
"type": "number"
},
"repo": {
"description": "Repository name",
"type": "string"
},
"user": {
"description": "User",
"type": "string"
}
},
"required": [
"owner",
"repo",
"issue_number",
"user"
],
"type": "object"
},
"name": "get_latest_issue_comment_user"
}
173 changes: 173 additions & 0 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,80 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
}
}

// UpdateIssueComment creates a tool to update a previously added comment to an issue.
func UpdateIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("update_issue_comment",
mcp.WithDescription(t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update a previously added comment to a specific issue in a GitHub repository.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_UPDATE_ISSUE_COMMENT_USER_TITLE", "Update a previously added comment to an issue"),
ReadOnlyHint: ToBoolPtr(false),
}),
mcp.WithNumber("comment_id",
mcp.Required(),
mcp.Description("ID of the comment to update"),
),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithString("body",
mcp.Required(),
mcp.Description("Comment content"),
),
),
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
}
body, err := RequiredParam[string](request, "body")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
commentID, err := RequiredInt(request, "comment_id")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

comment := &github.IssueComment{
Body: github.Ptr(body),
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, int64(commentID), comment)
if err != nil {
return nil, fmt.Errorf("failed to update comment: %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 update comment: %s", string(body))), nil
}

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

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

// AddSubIssue creates a tool to add a sub-issue to a parent issue.
func AddSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("add_sub_issue",
Expand Down Expand Up @@ -1511,6 +1585,105 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun
}
}

// GetIssueCommentUser creates a tool to get the latest comment by a user on a specific issue in a GitHub repository.
func GetLatestIssueCommentUser(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_latest_issue_comment_user",
mcp.WithDescription(t("TOOL_GET_LATEST_ISSUE_COMMENT_USER_DESCRIPTION", "Get the latest comments by a specific user on a specific issue in a GitHub repository.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_LATEST_ISSUE_COMMENT_USER_TITLE", "Get latest issue comment by user"),
ReadOnlyHint: ToBoolPtr(true),
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithNumber("issue_number",
mcp.Required(),
mcp.Description("Issue number"),
),
mcp.WithString("user",
mcp.Required(),
mcp.Description("User"),
),
WithPagination(),
),
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
}
issueNumber, err := RequiredInt(request, "issue_number")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
user, err := RequiredParam[string](request, "user")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
Page: pagination.Page,
PerPage: pagination.PerPage,
},
}
var latestcomment *github.IssueComment

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
comments, resp, err := client.Issues.ListComments(ctx, owner, repo, issueNumber, opts)
if err != nil {
return nil, fmt.Errorf("failed to get issue comments: %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 get issue comments: %s", string(body))), nil
}

for _, comment := range comments {
if comment == nil || comment.User == nil || comment.User.Login == nil {
continue
}

if *comment.User.Login == user {
if latestcomment == nil {
latestcomment = comment
} else {
if comment.UpdatedAt.Time.After(latestcomment.UpdatedAt.Time) {
latestcomment = comment
}
}
}
}

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

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

// mvpDescription is an MVP idea for generating tool descriptions from structured data in a shared format.
// It is not intended for widespread usage and is not a complete implementation.
type mvpDescription struct {
Expand Down
Loading