Skip to content

Commit 695805d

Browse files
committed
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 github#950 (write functionality)
1 parent 4873ab1 commit 695805d

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

‎pkg/github/issues.go‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,3 +2176,176 @@ func GetIssueDependencies(ctx context.Context, client *github.Client, cache *loc
21762176

21772177
return utils.NewToolResultText(string(r)), nil
21782178
}
2179+
2180+
// DependencyWrite creates a tool to manage dependencies between issues.
2181+
func DependencyWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
2182+
return NewTool(
2183+
ToolsetMetadataIssues,
2184+
mcp.Tool{
2185+
Name: "dependency_write",
2186+
Description: t("TOOL_DEPENDENCY_WRITE_DESCRIPTION", "Add or remove dependency relationships between issues in a GitHub repository."),
2187+
Annotations: &mcp.ToolAnnotations{
2188+
Title: t("TOOL_DEPENDENCY_WRITE_USER_TITLE", "Manage issue dependencies"),
2189+
ReadOnlyHint: false,
2190+
},
2191+
InputSchema: &jsonschema.Schema{
2192+
Type: "object",
2193+
Properties: map[string]*jsonschema.Schema{
2194+
"method": {
2195+
Type: "string",
2196+
Description: `The action to perform on issue dependencies.
2197+
Options are:
2198+
- 'add' - add a dependency relationship (mark an issue as blocked by another issue).
2199+
- 'remove' - remove a dependency relationship.
2200+
`,
2201+
Enum: []any{"add", "remove"},
2202+
},
2203+
"owner": {
2204+
Type: "string",
2205+
Description: "Repository owner",
2206+
},
2207+
"repo": {
2208+
Type: "string",
2209+
Description: "Repository name",
2210+
},
2211+
"issue_number": {
2212+
Type: "number",
2213+
Description: "The number of the issue",
2214+
},
2215+
"dependency_issue_number": {
2216+
Type: "number",
2217+
Description: "The number of the issue that blocks this issue (for 'add') or the dependency ID (for 'remove')",
2218+
},
2219+
},
2220+
Required: []string{"method", "owner", "repo", "issue_number", "dependency_issue_number"},
2221+
},
2222+
},
2223+
[]scopes.Scope{scopes.Repo},
2224+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
2225+
method, err := RequiredParam[string](args, "method")
2226+
if err != nil {
2227+
return utils.NewToolResultError(err.Error()), nil, nil
2228+
}
2229+
2230+
owner, err := RequiredParam[string](args, "owner")
2231+
if err != nil {
2232+
return utils.NewToolResultError(err.Error()), nil, nil
2233+
}
2234+
2235+
repo, err := RequiredParam[string](args, "repo")
2236+
if err != nil {
2237+
return utils.NewToolResultError(err.Error()), nil, nil
2238+
}
2239+
2240+
issueNumber, err := RequiredInt(args, "issue_number")
2241+
if err != nil {
2242+
return utils.NewToolResultError(err.Error()), nil, nil
2243+
}
2244+
2245+
dependencyIssueNumber, err := RequiredInt(args, "dependency_issue_number")
2246+
if err != nil {
2247+
return utils.NewToolResultError(err.Error()), nil, nil
2248+
}
2249+
2250+
client, err := deps.GetClient(ctx)
2251+
if err != nil {
2252+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
2253+
}
2254+
2255+
switch method {
2256+
case "add":
2257+
result, err := AddIssueDependency(ctx, client, owner, repo, issueNumber, dependencyIssueNumber)
2258+
return result, nil, err
2259+
case "remove":
2260+
result, err := RemoveIssueDependency(ctx, client, owner, repo, issueNumber, dependencyIssueNumber)
2261+
return result, nil, err
2262+
default:
2263+
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
2264+
}
2265+
})
2266+
}
2267+
2268+
// AddIssueDependency adds a dependency relationship between two issues.
2269+
// The issue specified by issueNumber will be blocked by the issue specified by dependencyIssueNumber.
2270+
func AddIssueDependency(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, dependencyIssueNumber int) (*mcp.CallToolResult, error) {
2271+
url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", owner, repo, issueNumber)
2272+
2273+
body := map[string]interface{}{
2274+
"dependency_issue_id": dependencyIssueNumber,
2275+
}
2276+
2277+
req, err := client.NewRequest("POST", url, body)
2278+
if err != nil {
2279+
return nil, fmt.Errorf("failed to create request: %w", err)
2280+
}
2281+
2282+
var dependency IssueDependency
2283+
resp, err := client.Do(ctx, req, &dependency)
2284+
if err != nil {
2285+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
2286+
"failed to add issue dependency",
2287+
resp,
2288+
err,
2289+
), nil
2290+
}
2291+
2292+
defer func() { _ = resp.Body.Close() }()
2293+
2294+
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
2295+
body, err := io.ReadAll(resp.Body)
2296+
if err != nil {
2297+
return nil, fmt.Errorf("failed to read response body: %w", err)
2298+
}
2299+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to add issue dependency", resp, body), nil
2300+
}
2301+
2302+
r, err := json.Marshal(map[string]interface{}{
2303+
"success": true,
2304+
"message": fmt.Sprintf("Issue #%d is now blocked by issue #%d", issueNumber, dependencyIssueNumber),
2305+
"dependency": dependency,
2306+
})
2307+
if err != nil {
2308+
return nil, fmt.Errorf("failed to marshal response: %w", err)
2309+
}
2310+
2311+
return utils.NewToolResultText(string(r)), nil
2312+
}
2313+
2314+
// RemoveIssueDependency removes a dependency relationship between two issues.
2315+
func RemoveIssueDependency(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, dependencyID int) (*mcp.CallToolResult, error) {
2316+
url := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies/%d", owner, repo, issueNumber, dependencyID)
2317+
2318+
req, err := client.NewRequest("DELETE", url, nil)
2319+
if err != nil {
2320+
return nil, fmt.Errorf("failed to create request: %w", err)
2321+
}
2322+
2323+
resp, err := client.Do(ctx, req, nil)
2324+
if err != nil {
2325+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
2326+
"failed to remove issue dependency",
2327+
resp,
2328+
err,
2329+
), nil
2330+
}
2331+
2332+
defer func() { _ = resp.Body.Close() }()
2333+
2334+
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
2335+
body, err := io.ReadAll(resp.Body)
2336+
if err != nil {
2337+
return nil, fmt.Errorf("failed to read response body: %w", err)
2338+
}
2339+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to remove issue dependency", resp, body), nil
2340+
}
2341+
2342+
r, err := json.Marshal(map[string]interface{}{
2343+
"success": true,
2344+
"message": fmt.Sprintf("Dependency removed from issue #%d", issueNumber),
2345+
})
2346+
if err != nil {
2347+
return nil, fmt.Errorf("failed to marshal response: %w", err)
2348+
}
2349+
2350+
return utils.NewToolResultText(string(r)), nil
2351+
}

‎pkg/github/tools.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
195195
AddIssueComment(t),
196196
AssignCopilotToIssue(t),
197197
SubIssueWrite(t),
198+
DependencyWrite(t),
198199

199200
// User tools
200201
SearchUsers(t),

0 commit comments

Comments
 (0)