From 3e093c10b98606e84f6f2b8ed8b6553d59c19a10 Mon Sep 17 00:00:00 2001 From: gronod Date: Wed, 16 Sep 2026 08:03:23 +0000 Subject: [PATCH 1/2] feat(issue): support cross-repo dependencies and check_blocked Send blocked_owner/blocked_repo and blocking_owner/blocking_repo in the IssueMeta body so Gitea can resolve cross-repository links. Default those fields to the path issue's owner/repo. Add issue_read method check_blocked to summarize open blockers, and document that dependency endpoints 404 when the repo has issue dependencies disabled. --- README.md | 4 +-- operation/issue/blocking.go | 50 ++++++++++++++++++++++++-- operation/issue/blocking_test.go | 62 +++++++++++++++++++++++++++++--- operation/issue/issue.go | 12 +++++-- 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 49e4634..6d9cfe8 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,8 @@ Once configured, try `list all my repositories` in the chat box. | package_write | packages | Write | Delete a package version (irreversible) | | list_issues | issue | Read | List repository issues | | attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content | -| issue_read | issue | Read | Read issue: details, comments, labels, or blocking relationships | -| issue_write | issue | Write | Write issues: create, update, manage comments, labels, and blocking relationships | +| issue_read | issue | Read | Read issue: details, comments, labels, or dependency/blocking links | +| issue_write | issue | Write | Write issues: create, update, manage comments, labels, and dependency/blocking links | | list_pull_requests | pull_request | Read | List repository pull requests | | pull_request_read | pull_request | Read | Read pull request: details, diff, files, status, reviews, review comments | | pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers | diff --git a/operation/issue/blocking.go b/operation/issue/blocking.go index 8722178..c55a98c 100644 --- a/operation/issue/blocking.go +++ b/operation/issue/blocking.go @@ -59,13 +59,14 @@ func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (* if err != nil { return to.ErrorResult(err) } + blockedOwner, blockedRepo := relatedIssueRepo(args, "blocked_owner", "blocked_repo", owner, repo) method := http.MethodPost if !block { method = http.MethodDelete } var issue gitea_sdk.Issue path := fmt.Sprintf("repos/%s/%s/issues/%d/blocks", url.PathEscape(owner), url.PathEscape(repo), index) - if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocked, Owner: owner, Repo: repo}, &issue); err != nil { + if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocked, Owner: blockedOwner, Repo: blockedRepo}, &issue); err != nil { return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/blocks err: %v", owner, repo, index, err)) } return to.TextResult(slimIssue(&issue)) @@ -108,18 +109,63 @@ func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (* if err != nil { return to.ErrorResult(err) } + blockingOwner, blockingRepo := relatedIssueRepo(args, "blocking_owner", "blocking_repo", owner, repo) method := http.MethodPost if !add { method = http.MethodDelete } var issue gitea_sdk.Issue path := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", url.PathEscape(owner), url.PathEscape(repo), index) - if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocking, Owner: owner, Repo: repo}, &issue); err != nil { + if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocking, Owner: blockingOwner, Repo: blockingRepo}, &issue); err != nil { return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err)) } return to.TextResult(slimIssue(&issue)) } +func checkIssueBlockedFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, repo, index, err := issueRef(args) + if err != nil { + return to.ErrorResult(err) + } + page, pageSize := params.GetPagination(args, 30) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{ + ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err)) + } + openBlockers := make([]map[string]any, 0) + for _, issue := range issues { + if issue == nil { + continue + } + if issue.State == gitea_sdk.StateOpen || issue.State == "" { + openBlockers = append(openBlockers, slimIssue(issue)) + } + } + return to.TextResult(map[string]any{ + "blocked": len(openBlockers) > 0, + "total_dependencies": len(issues), + "open_blockers": len(openBlockers), + "blockers": slimIssues(issues), + "open": openBlockers, + }) +} + +func relatedIssueRepo(args map[string]any, ownerKey, repoKey, owner, repo string) (string, string) { + if o := params.GetOptionalString(args, ownerKey, ""); o != "" { + owner = o + } + if r := params.GetOptionalString(args, repoKey, ""); r != "" { + repo = r + } + return owner, repo +} + func issueRef(args map[string]any) (owner, repo string, index int64, err error) { if owner, err = params.GetString(args, "owner"); err != nil { return "", "", 0, err diff --git a/operation/issue/blocking_test.go b/operation/issue/blocking_test.go index adae1c0..65619d4 100644 --- a/operation/issue/blocking_test.go +++ b/operation/issue/blocking_test.go @@ -31,6 +31,9 @@ func Test_issueBlocking(t *testing.T) { wantMethod string wantQuery string wantIndex float64 + wantOwner string + wantRepo string + wantBody string }{ { name: "list blocks paginates", @@ -47,6 +50,22 @@ func Test_issueBlocking(t *testing.T) { wantPath: blocksPath, wantMethod: http.MethodPost, wantIndex: 7, + wantOwner: owner, + wantRepo: repo, + }, + { + name: "block issue posts cross-repo target", + fn: blockIssueFn, + args: map[string]any{ + "blocked_issue": float64(7), + "blocked_owner": "other", + "blocked_repo": "elsewhere", + }, + wantPath: blocksPath, + wantMethod: http.MethodPost, + wantIndex: 7, + wantOwner: "other", + wantRepo: "elsewhere", }, { name: "unblock issue deletes with index", @@ -55,6 +74,8 @@ func Test_issueBlocking(t *testing.T) { wantPath: blocksPath, wantMethod: http.MethodDelete, wantIndex: 7, + wantOwner: owner, + wantRepo: repo, }, { name: "list dependencies paginates", @@ -71,6 +92,22 @@ func Test_issueBlocking(t *testing.T) { wantPath: depsPath, wantMethod: http.MethodPost, wantIndex: 9, + wantOwner: owner, + wantRepo: repo, + }, + { + name: "add dependency posts cross-repo target", + fn: addIssueDependencyFn, + args: map[string]any{ + "blocking_issue": float64(9), + "blocking_owner": "other", + "blocking_repo": "elsewhere", + }, + wantPath: depsPath, + wantMethod: http.MethodPost, + wantIndex: 9, + wantOwner: "other", + wantRepo: "elsewhere", }, { name: "remove dependency deletes with index", @@ -79,6 +116,17 @@ func Test_issueBlocking(t *testing.T) { wantPath: depsPath, wantMethod: http.MethodDelete, wantIndex: 9, + wantOwner: owner, + wantRepo: repo, + }, + { + name: "check blocked summarizes open dependencies", + fn: checkIssueBlockedFn, + args: map[string]any{"page": float64(1), "per_page": float64(30)}, + wantPath: depsPath, + wantMethod: http.MethodGet, + wantQuery: "limit=30&page=1", + wantBody: `"blocked":true`, }, } @@ -139,15 +187,19 @@ func Test_issueBlocking(t *testing.T) { if gotBody["index"] != tt.wantIndex { t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex) } - if gotBody["owner"] != owner { - t.Fatalf("body owner = %v, want %s", gotBody["owner"], owner) + if gotBody["owner"] != tt.wantOwner { + t.Fatalf("body owner = %v, want %s", gotBody["owner"], tt.wantOwner) } - if gotBody["repo"] != repo { - t.Fatalf("body repo = %v, want %s", gotBody["repo"], repo) + if gotBody["repo"] != tt.wantRepo { + t.Fatalf("body repo = %v, want %s", gotBody["repo"], tt.wantRepo) } } body := res.Content[0].(*mcp.TextContent).Text - if !strings.Contains(body, `"number":7`) && !strings.Contains(body, `"number": 7`) { + if tt.wantBody != "" { + if !strings.Contains(body, tt.wantBody) { + t.Fatalf("expected %s in output, got: %s", tt.wantBody, body) + } + } else if !strings.Contains(body, `"number":7`) && !strings.Contains(body, `"number": 7`) { t.Fatalf("expected slim issue in output, got: %s", body) } }) diff --git a/operation/issue/issue.go b/operation/issue/issue.go index dc2a151..7042677 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -55,9 +55,9 @@ var ( IssueReadTool = tool.NewDefinition( IssueReadToolName, - "Read issue: details, comments, labels, or blocking relationships.", + "Read issue: details, comments, labels, or dependency/blocking links. list_dependencies is issues that block this one (blocked-by); list_blocks is issues this one blocks. check_blocked summarizes whether any of those dependencies are still open. These methods 404 if the repo has issue dependencies disabled.", annotation.ReadOnly("Read issue details"), - tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels", "list_blocks", "list_dependencies")), + tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels", "list_blocks", "list_dependencies", "check_blocked")), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.Number("issue_number", tool.Required()), @@ -67,7 +67,7 @@ var ( IssueWriteTool = tool.NewDefinition( IssueWriteToolName, - "Write issues: create, update, manage comments, labels, and blocking relationships.", + "Write issues: create, update, manage comments, labels, and dependency/blocking links. block_issue makes issue_number block blocked_issue; add_dependency makes issue_number depend on blocking_issue. Cross-repo links use blocked_owner/blocked_repo or blocking_owner/blocking_repo (instance must allow cross-repository dependencies). Repo must have issue dependencies enabled or Gitea returns 404.", annotation.Write("Create or update issues, comments, and labels"), tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels", "block_issue", "unblock_issue", "add_dependency", "remove_dependency")), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), @@ -85,7 +85,11 @@ var ( tool.String("deadline", tool.Description("ISO 8601")), tool.Boolean("remove_deadline"), tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")), + tool.String("blocked_owner", tool.Description("owner of blocked_issue; defaults to owner")), + tool.String("blocked_repo", tool.Description("repo of blocked_issue; defaults to repo")), tool.Number("blocking_issue", tool.Description("issue number that blocks issue_number (required for 'add_dependency'/'remove_dependency')")), + tool.String("blocking_owner", tool.Description("owner of blocking_issue; defaults to owner")), + tool.String("blocking_repo", tool.Description("repo of blocking_issue; defaults to repo")), ) ) @@ -120,6 +124,8 @@ func issueReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, return listIssueBlocksFn(ctx, args) case "list_dependencies": return listIssueDependenciesFn(ctx, args) + case "check_blocked": + return checkIssueBlockedFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } -- 2.39.5 From 07949c1a6342af4516fe7843d1204cc7d6068c49 Mon Sep 17 00:00:00 2001 From: gronod Date: Wed, 16 Sep 2026 08:20:35 +0000 Subject: [PATCH 2/2] fix(issue): satisfy embedlit lint in check_blocked Use the same ListOptions local as the other list helpers instead of embedding gitea_sdk.ListOptions in the composite literal. --- operation/issue/blocking.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operation/issue/blocking.go b/operation/issue/blocking.go index c55a98c..c73d1ef 100644 --- a/operation/issue/blocking.go +++ b/operation/issue/blocking.go @@ -132,8 +132,9 @@ func checkIssueBlockedFn(ctx context.Context, args map[string]any) (*mcp.CallToo if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } + listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize} issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{ - ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, + ListOptions: listOpt, }) if err != nil { return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err)) -- 2.39.5