Skip to content

Commit 07ddd7c

Browse files
Merge pull request #5 from yevheniidehtiar/feat/milestone-commands
feat: Implement list_milestones and search_milestones commands
2 parents eb35d67 + d76a8d5 commit 07ddd7c

File tree

6 files changed

+591
-5
lines changed

6 files changed

+591
-5
lines changed

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,11 @@ The following sets of tools are available (all are on by default):
544544
- `state`: Milestone state (string, required)
545545
- `title`: Milestone title (string, required)
546546

547+
- **delete_milestone** - Delete milestone
548+
- `milestone_number`: The number of the milestone to delete (number, required)
549+
- `owner`: Repository owner (string, required)
550+
- `repo`: Repository name (string, required)
551+
547552
- **edit_milestone** - Edit milestone
548553
- `description`: New milestone description (string, optional)
549554
- `due_on`: New milestone due date in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) (string, optional)
@@ -553,11 +558,6 @@ The following sets of tools are available (all are on by default):
553558
- `state`: New milestone state (string, optional)
554559
- `title`: New milestone title (string, optional)
555560

556-
- **delete_milestone** - Delete milestone
557-
- `milestone_number`: The number of the milestone to delete (number, required)
558-
- `owner`: Repository owner (string, required)
559-
- `repo`: Repository name (string, required)
560-
561561
- **get_issue** - Get issue details
562562
- `issue_number`: The number of the issue (number, required)
563563
- `owner`: The owner of the repository (string, required)
@@ -584,6 +584,15 @@ The following sets of tools are available (all are on by default):
584584
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
585585
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
586586

587+
- **list_milestones** - List milestones
588+
- `direction`: Sort direction (string, optional)
589+
- `owner`: Repository owner (string, required)
590+
- `page`: Page number for pagination (min 1) (number, optional)
591+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
592+
- `repo`: Repository name (string, required)
593+
- `sort`: Sort field (string, optional)
594+
- `state`: Filter by state (string, optional)
595+
587596
- **list_sub_issues** - List sub-issues
588597
- `issue_number`: Issue number (number, required)
589598
- `owner`: Repository owner (string, required)
@@ -614,6 +623,12 @@ The following sets of tools are available (all are on by default):
614623
- `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional)
615624
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)
616625

626+
- **search_milestones** - Search milestones
627+
- `owner`: Repository owner (string, required)
628+
- `query`: Search query to filter milestones by title or description (string, required)
629+
- `repo`: Repository name (string, required)
630+
- `state`: Filter by state (string, optional)
631+
617632
- **update_issue** - Edit issue
618633
- `assignees`: New assignees (string[], optional)
619634
- `body`: New description (string, optional)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"annotations": {
3+
"title": "List milestones",
4+
"readOnlyHint": true
5+
},
6+
"description": "List milestones for a repository.",
7+
"inputSchema": {
8+
"properties": {
9+
"direction": {
10+
"description": "Sort direction",
11+
"enum": [
12+
"asc",
13+
"desc"
14+
],
15+
"type": "string"
16+
},
17+
"owner": {
18+
"description": "Repository owner",
19+
"type": "string"
20+
},
21+
"page": {
22+
"description": "Page number for pagination (min 1)",
23+
"minimum": 1,
24+
"type": "number"
25+
},
26+
"perPage": {
27+
"description": "Results per page for pagination (min 1, max 100)",
28+
"maximum": 100,
29+
"minimum": 1,
30+
"type": "number"
31+
},
32+
"repo": {
33+
"description": "Repository name",
34+
"type": "string"
35+
},
36+
"sort": {
37+
"description": "Sort field",
38+
"enum": [
39+
"due_on",
40+
"completeness"
41+
],
42+
"type": "string"
43+
},
44+
"state": {
45+
"description": "Filter by state",
46+
"enum": [
47+
"open",
48+
"closed",
49+
"all"
50+
],
51+
"type": "string"
52+
}
53+
},
54+
"required": [
55+
"owner",
56+
"repo"
57+
],
58+
"type": "object"
59+
},
60+
"name": "list_milestones"
61+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"annotations": {
3+
"title": "Search milestones",
4+
"readOnlyHint": true
5+
},
6+
"description": "Search for milestones in a repository.",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "Repository owner",
11+
"type": "string"
12+
},
13+
"query": {
14+
"description": "Search query to filter milestones by title or description",
15+
"type": "string"
16+
},
17+
"repo": {
18+
"description": "Repository name",
19+
"type": "string"
20+
},
21+
"state": {
22+
"description": "Filter by state",
23+
"enum": [
24+
"open",
25+
"closed",
26+
"all"
27+
],
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"owner",
33+
"repo",
34+
"query"
35+
],
36+
"type": "object"
37+
},
38+
"name": "search_milestones"
39+
}

pkg/github/issues.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,84 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
206206
}
207207
}
208208

209+
// SearchMilestones creates a tool to search for milestones in a repository.
210+
func SearchMilestones(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
211+
return mcp.NewTool("search_milestones",
212+
mcp.WithDescription(t("TOOL_SEARCH_MILESTONES_DESCRIPTION", "Search for milestones in a repository.")),
213+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
214+
Title: t("TOOL_SEARCH_MILESTONES_USER_TITLE", "Search milestones"),
215+
ReadOnlyHint: ToBoolPtr(true),
216+
}),
217+
mcp.WithString("owner",
218+
mcp.Required(),
219+
mcp.Description("Repository owner"),
220+
),
221+
mcp.WithString("repo",
222+
mcp.Required(),
223+
mcp.Description("Repository name"),
224+
),
225+
mcp.WithString("query",
226+
mcp.Required(),
227+
mcp.Description("Search query to filter milestones by title or description"),
228+
),
229+
mcp.WithString("state",
230+
mcp.Description("Filter by state"),
231+
mcp.Enum("open", "closed", "all"),
232+
),
233+
),
234+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
235+
owner, err := RequiredParam[string](request, "owner")
236+
if err != nil {
237+
return mcp.NewToolResultError(err.Error()), nil
238+
}
239+
repo, err := RequiredParam[string](request, "repo")
240+
if err != nil {
241+
return mcp.NewToolResultError(err.Error()), nil
242+
}
243+
query, err := RequiredParam[string](request, "query")
244+
if err != nil {
245+
return mcp.NewToolResultError(err.Error()), nil
246+
}
247+
state, err := OptionalParam[string](request, "state")
248+
if err != nil {
249+
return mcp.NewToolResultError(err.Error()), nil
250+
}
251+
252+
if state == "" {
253+
state = "open"
254+
}
255+
256+
opts := &github.MilestoneListOptions{
257+
State: state,
258+
}
259+
260+
client, err := getClient(ctx)
261+
if err != nil {
262+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
263+
}
264+
265+
allMilestones, _, err := client.Issues.ListMilestones(ctx, owner, repo, opts)
266+
if err != nil {
267+
return nil, fmt.Errorf("failed to list milestones: %w", err)
268+
}
269+
270+
var filteredMilestones []*github.Milestone
271+
for _, milestone := range allMilestones {
272+
if (milestone.Title != nil && strings.Contains(strings.ToLower(*milestone.Title), strings.ToLower(query))) ||
273+
(milestone.Description != nil && strings.Contains(strings.ToLower(*milestone.Description), strings.ToLower(query))) {
274+
filteredMilestones = append(filteredMilestones, milestone)
275+
}
276+
}
277+
278+
r, err := json.Marshal(filteredMilestones)
279+
if err != nil {
280+
return nil, fmt.Errorf("failed to marshal response: %w", err)
281+
}
282+
283+
return mcp.NewToolResultText(string(r)), nil
284+
}
285+
}
286+
209287
// EditMilestone creates a tool to edit an existing milestone in a GitHub repository.
210288
func EditMilestone(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
211289
return mcp.NewTool("edit_milestone",
@@ -474,6 +552,100 @@ func DeleteMilestone(getClient GetClientFn, t translations.TranslationHelperFunc
474552
}
475553
}
476554

555+
// ListMilestones creates a tool to list milestones for a repository.
556+
func ListMilestones(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
557+
return mcp.NewTool("list_milestones",
558+
mcp.WithDescription(t("TOOL_LIST_MILESTONES_DESCRIPTION", "List milestones for a repository.")),
559+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
560+
Title: t("TOOL_LIST_MILESTONES_USER_TITLE", "List milestones"),
561+
ReadOnlyHint: ToBoolPtr(true),
562+
}),
563+
mcp.WithString("owner",
564+
mcp.Required(),
565+
mcp.Description("Repository owner"),
566+
),
567+
mcp.WithString("repo",
568+
mcp.Required(),
569+
mcp.Description("Repository name"),
570+
),
571+
mcp.WithString("state",
572+
mcp.Description("Filter by state"),
573+
mcp.Enum("open", "closed", "all"),
574+
),
575+
mcp.WithString("sort",
576+
mcp.Description("Sort field"),
577+
mcp.Enum("due_on", "completeness"),
578+
),
579+
mcp.WithString("direction",
580+
mcp.Description("Sort direction"),
581+
mcp.Enum("asc", "desc"),
582+
),
583+
WithPagination(),
584+
),
585+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
586+
owner, err := RequiredParam[string](request, "owner")
587+
if err != nil {
588+
return mcp.NewToolResultError(err.Error()), nil
589+
}
590+
repo, err := RequiredParam[string](request, "repo")
591+
if err != nil {
592+
return mcp.NewToolResultError(err.Error()), nil
593+
}
594+
state, err := OptionalParam[string](request, "state")
595+
if err != nil {
596+
return mcp.NewToolResultError(err.Error()), nil
597+
}
598+
sort, err := OptionalParam[string](request, "sort")
599+
if err != nil {
600+
return mcp.NewToolResultError(err.Error()), nil
601+
}
602+
direction, err := OptionalParam[string](request, "direction")
603+
if err != nil {
604+
return mcp.NewToolResultError(err.Error()), nil
605+
}
606+
pagination, err := OptionalPaginationParams(request)
607+
if err != nil {
608+
return mcp.NewToolResultError(err.Error()), nil
609+
}
610+
611+
opts := &github.MilestoneListOptions{
612+
State: state,
613+
Sort: sort,
614+
Direction: direction,
615+
ListOptions: github.ListOptions{
616+
Page: pagination.Page,
617+
PerPage: pagination.PerPage,
618+
},
619+
}
620+
621+
client, err := getClient(ctx)
622+
if err != nil {
623+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
624+
}
625+
626+
milestones, resp, err := client.Issues.ListMilestones(ctx, owner, repo, opts)
627+
if err != nil {
628+
return nil, fmt.Errorf("failed to list milestones: %w", err)
629+
}
630+
defer func() { _ = resp.Body.Close() }()
631+
632+
if resp.StatusCode != http.StatusOK {
633+
body, err := io.ReadAll(resp.Body)
634+
if err != nil {
635+
return nil, fmt.Errorf("failed to read response body: %w", err)
636+
}
637+
return mcp.NewToolResultError(fmt.Sprintf("failed to list milestones: %s", string(body))), nil
638+
}
639+
640+
r, err := json.Marshal(milestones)
641+
if err != nil {
642+
return nil, fmt.Errorf("failed to marshal response: %w", err)
643+
}
644+
645+
return mcp.NewToolResultText(string(r)), nil
646+
}
647+
}
648+
477649
// ListIssueTypes creates a tool to list defined issue types for an organization. This can be used to understand supported issue type values for creating or updating issues.
478650
func ListIssueTypes(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
479651

0 commit comments

Comments
 (0)