diff --git a/operation/issue/issue.go b/operation/issue/issue.go index 0f48b52..4c023a5 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -44,7 +44,9 @@ var ( mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), mcp.WithString("state", mcp.DefaultString("all")), + mcp.WithString("type", mcp.Description("issues or pulls"), mcp.Enum("issues", "pulls")), mcp.WithArray("labels", mcp.Description("label name filter"), mcp.Items(map[string]any{"type": "string"})), + mcp.WithArray("milestones", mcp.Description("milestone name or ID filter"), mcp.Items(map[string]any{"type": "string"})), mcp.WithString("since", mcp.Description("updated after ISO 8601")), mcp.WithString("before", mcp.Description("updated before ISO 8601")), mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), @@ -181,15 +183,23 @@ func listRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo state = "all" } labels := params.GetStringSlice(req.GetArguments(), "labels") + milestones := params.GetStringSlice(req.GetArguments(), "milestones") page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.ListIssueOption{ - State: gitea_sdk.StateType(state), - Labels: labels, + State: gitea_sdk.StateType(state), + Labels: labels, + Milestones: milestones, ListOptions: gitea_sdk.ListOptions{ Page: page, PageSize: pageSize, }, } + switch req.GetArguments()["type"] { + case "issues": + opt.Type = gitea_sdk.IssueTypeIssue + case "pulls": + opt.Type = gitea_sdk.IssueTypePull + } if t := params.GetOptionalTime(req.GetArguments(), "since"); t != nil { opt.Since = *t } diff --git a/operation/issue/issue_test.go b/operation/issue/issue_test.go index 26b0827..767235d 100644 --- a/operation/issue/issue_test.go +++ b/operation/issue/issue_test.go @@ -63,10 +63,12 @@ func Test_listRepoIssuesFn_filters(t *testing.T) { req := mcp.CallToolRequest{ Params: mcp.CallToolParams{ Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "labels": []any{"bug", "enhancement"}, - "since": "2026-01-01T00:00:00Z", + "owner": owner, + "repo": repo, + "type": "issues", + "labels": []any{"bug", "enhancement"}, + "milestones": []any{"v1.0", "2"}, + "since": "2026-01-01T00:00:00Z", }, }, } @@ -85,6 +87,59 @@ func Test_listRepoIssuesFn_filters(t *testing.T) { if !strings.Contains(gotQuery, "since=2026-01-01") { t.Fatalf("expected since query param, got %s", gotQuery) } + if !strings.Contains(gotQuery, "milestones=v1.0%2C2") { + t.Fatalf("expected milestones query param, got %s", gotQuery) + } + if !strings.Contains(gotQuery, "type=issues") { + t.Fatalf("expected type query param, got %s", gotQuery) + } +} + +func Test_listRepoIssuesFn_includesMilestone(t *testing.T) { + const ( + owner = "octo" + repo = "demo" + ) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/version": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":"1.12.0"}`)) + case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo): + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"private":false}`)) + case fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner, repo): + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ + {"number": 1, "title": "with milestone", "state": "closed", "milestone": {"id": 5, "title": "v1.0"}}, + {"number": 2, "title": "without milestone", "state": "open"} + ]`)) + default: + http.NotFound(w, r) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + origHost, origToken, origVersion := flag.Host, flag.Token, flag.Version + flag.Host, flag.Token, flag.Version = server.URL, "", "test" + defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() + + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + "owner": owner, "repo": repo, + }}} + res, err := listRepoIssuesFn(context.Background(), req) + if err != nil { + t.Fatalf("listRepoIssuesFn() error = %v", err) + } + if res.IsError { + t.Fatalf("unexpected error result: %v", res.Content) + } + body := res.Content[0].(mcp.TextContent).Text + if !strings.Contains(body, `"milestone"`) || !strings.Contains(body, `"v1.0"`) { + t.Fatalf("expected milestone in list output, got: %s", body) + } } func Test_createIssueFn_labels(t *testing.T) { diff --git a/operation/issue/slim.go b/operation/issue/slim.go index 00e78e6..84105e4 100644 --- a/operation/issue/slim.go +++ b/operation/issue/slim.go @@ -63,6 +63,12 @@ func slimIssues(issues []*gitea_sdk.Issue) []map[string]any { if len(i.Labels) > 0 { m["labels"] = slim.LabelNames(i.Labels) } + if i.Milestone != nil { + m["milestone"] = map[string]any{ + "id": i.Milestone.ID, + "title": i.Milestone.Title, + } + } if i.Ref != "" { m["ref"] = i.Ref }