Compare commits
2
Commits
10f5a345d8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07949c1a63 | ||
|
|
3e093c10b9 |
@@ -160,8 +160,8 @@ Once configured, try `list all my repositories` in the chat box.
|
|||||||
| package_write | packages | Write | Delete a package version (irreversible) |
|
| package_write | packages | Write | Delete a package version (irreversible) |
|
||||||
| list_issues | issue | Read | List repository issues |
|
| list_issues | issue | Read | List repository issues |
|
||||||
| attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content |
|
| 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_read | issue | Read | Read issue: details, comments, labels, or dependency/blocking links |
|
||||||
| issue_write | issue | Write | Write issues: create, update, manage comments, labels, and blocking relationships |
|
| 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 |
|
| 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_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 |
|
| pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers |
|
||||||
|
|||||||
@@ -59,13 +59,14 @@ func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (*
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(err)
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
blockedOwner, blockedRepo := relatedIssueRepo(args, "blocked_owner", "blocked_repo", owner, repo)
|
||||||
method := http.MethodPost
|
method := http.MethodPost
|
||||||
if !block {
|
if !block {
|
||||||
method = http.MethodDelete
|
method = http.MethodDelete
|
||||||
}
|
}
|
||||||
var issue gitea_sdk.Issue
|
var issue gitea_sdk.Issue
|
||||||
path := fmt.Sprintf("repos/%s/%s/issues/%d/blocks", url.PathEscape(owner), url.PathEscape(repo), index)
|
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.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/blocks err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
return to.TextResult(slimIssue(&issue))
|
return to.TextResult(slimIssue(&issue))
|
||||||
@@ -108,18 +109,64 @@ func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (*
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(err)
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
blockingOwner, blockingRepo := relatedIssueRepo(args, "blocking_owner", "blocking_repo", owner, repo)
|
||||||
method := http.MethodPost
|
method := http.MethodPost
|
||||||
if !add {
|
if !add {
|
||||||
method = http.MethodDelete
|
method = http.MethodDelete
|
||||||
}
|
}
|
||||||
var issue gitea_sdk.Issue
|
var issue gitea_sdk.Issue
|
||||||
path := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", url.PathEscape(owner), url.PathEscape(repo), index)
|
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.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
return to.TextResult(slimIssue(&issue))
|
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))
|
||||||
|
}
|
||||||
|
listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize}
|
||||||
|
issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{
|
||||||
|
ListOptions: listOpt,
|
||||||
|
})
|
||||||
|
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) {
|
func issueRef(args map[string]any) (owner, repo string, index int64, err error) {
|
||||||
if owner, err = params.GetString(args, "owner"); err != nil {
|
if owner, err = params.GetString(args, "owner"); err != nil {
|
||||||
return "", "", 0, err
|
return "", "", 0, err
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ func Test_issueBlocking(t *testing.T) {
|
|||||||
wantMethod string
|
wantMethod string
|
||||||
wantQuery string
|
wantQuery string
|
||||||
wantIndex float64
|
wantIndex float64
|
||||||
|
wantOwner string
|
||||||
|
wantRepo string
|
||||||
|
wantBody string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "list blocks paginates",
|
name: "list blocks paginates",
|
||||||
@@ -47,6 +50,22 @@ func Test_issueBlocking(t *testing.T) {
|
|||||||
wantPath: blocksPath,
|
wantPath: blocksPath,
|
||||||
wantMethod: http.MethodPost,
|
wantMethod: http.MethodPost,
|
||||||
wantIndex: 7,
|
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",
|
name: "unblock issue deletes with index",
|
||||||
@@ -55,6 +74,8 @@ func Test_issueBlocking(t *testing.T) {
|
|||||||
wantPath: blocksPath,
|
wantPath: blocksPath,
|
||||||
wantMethod: http.MethodDelete,
|
wantMethod: http.MethodDelete,
|
||||||
wantIndex: 7,
|
wantIndex: 7,
|
||||||
|
wantOwner: owner,
|
||||||
|
wantRepo: repo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "list dependencies paginates",
|
name: "list dependencies paginates",
|
||||||
@@ -71,6 +92,22 @@ func Test_issueBlocking(t *testing.T) {
|
|||||||
wantPath: depsPath,
|
wantPath: depsPath,
|
||||||
wantMethod: http.MethodPost,
|
wantMethod: http.MethodPost,
|
||||||
wantIndex: 9,
|
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",
|
name: "remove dependency deletes with index",
|
||||||
@@ -79,6 +116,17 @@ func Test_issueBlocking(t *testing.T) {
|
|||||||
wantPath: depsPath,
|
wantPath: depsPath,
|
||||||
wantMethod: http.MethodDelete,
|
wantMethod: http.MethodDelete,
|
||||||
wantIndex: 9,
|
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 {
|
if gotBody["index"] != tt.wantIndex {
|
||||||
t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex)
|
t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex)
|
||||||
}
|
}
|
||||||
if gotBody["owner"] != owner {
|
if gotBody["owner"] != tt.wantOwner {
|
||||||
t.Fatalf("body owner = %v, want %s", gotBody["owner"], owner)
|
t.Fatalf("body owner = %v, want %s", gotBody["owner"], tt.wantOwner)
|
||||||
}
|
}
|
||||||
if gotBody["repo"] != repo {
|
if gotBody["repo"] != tt.wantRepo {
|
||||||
t.Fatalf("body repo = %v, want %s", gotBody["repo"], repo)
|
t.Fatalf("body repo = %v, want %s", gotBody["repo"], tt.wantRepo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := res.Content[0].(*mcp.TextContent).Text
|
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)
|
t.Fatalf("expected slim issue in output, got: %s", body)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ var (
|
|||||||
|
|
||||||
IssueReadTool = tool.NewDefinition(
|
IssueReadTool = tool.NewDefinition(
|
||||||
IssueReadToolName,
|
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"),
|
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("owner", tool.Required(), tool.Description(params.OwnerDesc)),
|
||||||
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
|
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
|
||||||
tool.Number("issue_number", tool.Required()),
|
tool.Number("issue_number", tool.Required()),
|
||||||
@@ -67,7 +67,7 @@ var (
|
|||||||
|
|
||||||
IssueWriteTool = tool.NewDefinition(
|
IssueWriteTool = tool.NewDefinition(
|
||||||
IssueWriteToolName,
|
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"),
|
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("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)),
|
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
|
||||||
@@ -85,7 +85,11 @@ var (
|
|||||||
tool.String("deadline", tool.Description("ISO 8601")),
|
tool.String("deadline", tool.Description("ISO 8601")),
|
||||||
tool.Boolean("remove_deadline"),
|
tool.Boolean("remove_deadline"),
|
||||||
tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")),
|
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.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)
|
return listIssueBlocksFn(ctx, args)
|
||||||
case "list_dependencies":
|
case "list_dependencies":
|
||||||
return listIssueDependenciesFn(ctx, args)
|
return listIssueDependenciesFn(ctx, args)
|
||||||
|
case "check_blocked":
|
||||||
|
return checkIssueBlockedFn(ctx, args)
|
||||||
default:
|
default:
|
||||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user