From 4873ab1e45057cc6fffd657bd875a7f507d0fe33 Mon Sep 17 00:00:00 2001 From: Pepper Pancoast Date: Mon, 2 Feb 2026 18:25:38 -0600 Subject: [PATCH 1/3] feat: Add get_dependencies method to issue_read tool - Add 'get_dependencies' enum value to IssueRead tool method parameter - Implement GetIssueDependencies function to fetch dependency relationships - Support both 'depends_on' (blocked by) and 'blocking' relationships - Use GitHub REST API /repos/{owner}/{repo}/issues/{number}/dependencies endpoint Addresses #950 (read functionality) --- pkg/github/issues.go | 66 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 62e1a0bac..edac3eafc 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -244,8 +244,9 @@ Options are: 2. get_comments - Get issue comments. 3. get_sub_issues - Get sub-issues of the issue. 4. get_labels - Get labels assigned to the issue. +5. get_dependencies - Get issue dependencies (blocked by/blocking relationships). `, - Enum: []any{"get", "get_comments", "get_sub_issues", "get_labels"}, + Enum: []any{"get", "get_comments", "get_sub_issues", "get_labels", "get_dependencies"}, }, "owner": { Type: "string", @@ -323,6 +324,9 @@ Options are: case "get_labels": result, err := GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber) return result, nil, err + case "get_dependencies": + result, err := GetIssueDependencies(ctx, client, deps.GetRepoAccessCache(), owner, repo, issueNumber) + return result, nil, err default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } @@ -2112,3 +2116,63 @@ func GetGraphQLFeatures(ctx context.Context) []string { } return nil } + +// IssueDependency represents a dependency relationship between issues +type IssueDependency struct { + ID int64 `json:"id"` + NodeID string `json:"node_id"` + Number int `json:"number"` + Title string `json:"title"` + State string `json:"state"` + Repository struct { + Name string `json:"name"` + FullName string `json:"full_name"` + Owner struct { + Login string `json:"login"` + } `json:"owner"` + } `json:"repository"` + HTMLURL string `json:"html_url"` +} + +// IssueDependencies represents the complete dependency information for an issue +type IssueDependencies struct { + DependsOn []IssueDependency `json:"depends_on"` // Issues this issue depends on (blocked by) + Blocking []IssueDependency `json:"blocking"` // Issues that depend on this issue +} + +// GetIssueDependencies retrieves dependency information for an issue. +// Returns both "depends_on" (issues blocking this issue) and "blocking" (issues blocked by this issue). +func GetIssueDependencies(ctx context.Context, client *github.Client, cache *lockdown.RepoAccessCache, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) { + url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", owner, repo, issueNumber) + req, err := client.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + var dependencies IssueDependencies + resp, err := client.Do(ctx, req, &dependencies) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get issue dependencies", + resp, + err, + ), nil + } + + 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 ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get issue dependencies", resp, body), nil + } + + r, err := json.Marshal(dependencies) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} From 695805d938168636c75069d0ef281b82e26b1632 Mon Sep 17 00:00:00 2001 From: Pepper Pancoast Date: Mon, 2 Feb 2026 18:27:28 -0600 Subject: [PATCH 2/3] feat: Add dependency_write tool for managing issue dependencies - Implement DependencyWrite tool with 'add' and 'remove' methods - Add AddIssueDependency function to create dependency relationships - Add RemoveIssueDependency function to delete dependency relationships - Register DependencyWrite tool in AllTools list - Support POST /repos/{owner}/{repo}/issues/{number}/dependencies endpoint - Support DELETE /repos/{owner}/{repo}/issues/{number}/dependencies/{id} endpoint Addresses #950 (write functionality) --- pkg/github/issues.go | 173 +++++++++++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 1 + 2 files changed, 174 insertions(+) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index edac3eafc..4d4999e7e 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -2176,3 +2176,176 @@ func GetIssueDependencies(ctx context.Context, client *github.Client, cache *loc return utils.NewToolResultText(string(r)), nil } + +// DependencyWrite creates a tool to manage dependencies between issues. +func DependencyWrite(t translations.TranslationHelperFunc) inventory.ServerTool { + return NewTool( + ToolsetMetadataIssues, + mcp.Tool{ + Name: "dependency_write", + Description: t("TOOL_DEPENDENCY_WRITE_DESCRIPTION", "Add or remove dependency relationships between issues in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_DEPENDENCY_WRITE_USER_TITLE", "Manage issue dependencies"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "method": { + Type: "string", + Description: `The action to perform on issue dependencies. +Options are: +- 'add' - add a dependency relationship (mark an issue as blocked by another issue). +- 'remove' - remove a dependency relationship. + `, + Enum: []any{"add", "remove"}, + }, + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "issue_number": { + Type: "number", + Description: "The number of the issue", + }, + "dependency_issue_number": { + Type: "number", + Description: "The number of the issue that blocks this issue (for 'add') or the dependency ID (for 'remove')", + }, + }, + Required: []string{"method", "owner", "repo", "issue_number", "dependency_issue_number"}, + }, + }, + []scopes.Scope{scopes.Repo}, + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + method, err := RequiredParam[string](args, "method") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + issueNumber, err := RequiredInt(args, "issue_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + dependencyIssueNumber, err := RequiredInt(args, "dependency_issue_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } + + switch method { + case "add": + result, err := AddIssueDependency(ctx, client, owner, repo, issueNumber, dependencyIssueNumber) + return result, nil, err + case "remove": + result, err := RemoveIssueDependency(ctx, client, owner, repo, issueNumber, dependencyIssueNumber) + return result, nil, err + default: + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil + } + }) +} + +// AddIssueDependency adds a dependency relationship between two issues. +// The issue specified by issueNumber will be blocked by the issue specified by dependencyIssueNumber. +func AddIssueDependency(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, dependencyIssueNumber int) (*mcp.CallToolResult, error) { + url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", owner, repo, issueNumber) + + body := map[string]interface{}{ + "dependency_issue_id": dependencyIssueNumber, + } + + req, err := client.NewRequest("POST", url, body) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + var dependency IssueDependency + resp, err := client.Do(ctx, req, &dependency) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to add issue dependency", + resp, + err, + ), nil + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated && 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 ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to add issue dependency", resp, body), nil + } + + r, err := json.Marshal(map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("Issue #%d is now blocked by issue #%d", issueNumber, dependencyIssueNumber), + "dependency": dependency, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} + +// RemoveIssueDependency removes a dependency relationship between two issues. +func RemoveIssueDependency(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, dependencyID int) (*mcp.CallToolResult, error) { + url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies/%d", owner, repo, issueNumber, dependencyID) + + req, err := client.NewRequest("DELETE", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := client.Do(ctx, req, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to remove issue dependency", + resp, + err, + ), nil + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNoContent && 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 ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to remove issue dependency", resp, body), nil + } + + r, err := json.Marshal(map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("Dependency removed from issue #%d", issueNumber), + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index a169ff591..ea67f4688 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -195,6 +195,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool { AddIssueComment(t), AssignCopilotToIssue(t), SubIssueWrite(t), + DependencyWrite(t), // User tools SearchUsers(t), From 30c2cd00e493aeef1764d76d5ba55379b940c178 Mon Sep 17 00:00:00 2001 From: Pepper Pancoast Date: Mon, 2 Feb 2026 18:35:37 -0600 Subject: [PATCH 3/3] Fix linter issues and update generated documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused cache parameter from GetIssueDependencies - Update tool snapshots for issue_read tool with new get_dependencies method - Regenerate README.md with updated tool documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 13 +++++++++++++ pkg/github/__toolsnaps__/issue_read.snap | 5 +++-- pkg/github/issues.go | 14 +++++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index afe003002..6303bca1e 100644 --- a/README.md +++ b/README.md @@ -802,6 +802,18 @@ The following sets of tools are available: - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) +- **dependency_write** - Manage issue dependencies + - **Required OAuth Scopes**: `repo` + - `dependency_issue_number`: The number of the issue that blocks this issue (for 'add') or the dependency ID (for 'remove') (number, required) + - `issue_number`: The number of the issue (number, required) + - `method`: The action to perform on issue dependencies. + Options are: + - 'add' - add a dependency relationship (mark an issue as blocked by another issue). + - 'remove' - remove a dependency relationship. + (string, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **get_label** - Get a specific label from a repository. - **Required OAuth Scopes**: `repo` - `name`: Label name. (string, required) @@ -817,6 +829,7 @@ The following sets of tools are available: 2. get_comments - Get issue comments. 3. get_sub_issues - Get sub-issues of the issue. 4. get_labels - Get labels assigned to the issue. + 5. get_dependencies - Get issue dependencies (blocked by/blocking relationships). (string, required) - `owner`: The owner of the repository (string, required) - `page`: Page number for pagination (min 1) (number, optional) diff --git a/pkg/github/__toolsnaps__/issue_read.snap b/pkg/github/__toolsnaps__/issue_read.snap index 21aa361f5..8d8beb036 100644 --- a/pkg/github/__toolsnaps__/issue_read.snap +++ b/pkg/github/__toolsnaps__/issue_read.snap @@ -11,12 +11,13 @@ "type": "number" }, "method": { - "description": "The read operation to perform on a single issue.\nOptions are:\n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n", + "description": "The read operation to perform on a single issue.\nOptions are:\n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n5. get_dependencies - Get issue dependencies (blocked by/blocking relationships).\n", "enum": [ "get", "get_comments", "get_sub_issues", - "get_labels" + "get_labels", + "get_dependencies" ], "type": "string" }, diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 4d4999e7e..9a34e7695 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -325,7 +325,7 @@ Options are: result, err := GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber) return result, nil, err case "get_dependencies": - result, err := GetIssueDependencies(ctx, client, deps.GetRepoAccessCache(), owner, repo, issueNumber) + result, err := GetIssueDependencies(ctx, client, owner, repo, issueNumber) return result, nil, err default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil @@ -2142,7 +2142,7 @@ type IssueDependencies struct { // GetIssueDependencies retrieves dependency information for an issue. // Returns both "depends_on" (issues blocking this issue) and "blocking" (issues blocked by this issue). -func GetIssueDependencies(ctx context.Context, client *github.Client, cache *lockdown.RepoAccessCache, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) { +func GetIssueDependencies(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) { url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", owner, repo, issueNumber) req, err := client.NewRequest("GET", url, nil) if err != nil { @@ -2269,11 +2269,11 @@ Options are: // The issue specified by issueNumber will be blocked by the issue specified by dependencyIssueNumber. func AddIssueDependency(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, dependencyIssueNumber int) (*mcp.CallToolResult, error) { url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", owner, repo, issueNumber) - + body := map[string]interface{}{ "dependency_issue_id": dependencyIssueNumber, } - + req, err := client.NewRequest("POST", url, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -2300,8 +2300,8 @@ func AddIssueDependency(ctx context.Context, client *github.Client, owner string } r, err := json.Marshal(map[string]interface{}{ - "success": true, - "message": fmt.Sprintf("Issue #%d is now blocked by issue #%d", issueNumber, dependencyIssueNumber), + "success": true, + "message": fmt.Sprintf("Issue #%d is now blocked by issue #%d", issueNumber, dependencyIssueNumber), "dependency": dependency, }) if err != nil { @@ -2314,7 +2314,7 @@ func AddIssueDependency(ctx context.Context, client *github.Client, owner string // RemoveIssueDependency removes a dependency relationship between two issues. func RemoveIssueDependency(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, dependencyID int) (*mcp.CallToolResult, error) { url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies/%d", owner, repo, issueNumber, dependencyID) - + req, err := client.NewRequest("DELETE", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err)