Repository: github/github-mcp-server

Issue:
Sub-issue endpoint
### Describe the feature or problem you’d like to solve

Enable replication of current projects workflows, enhancing issue management flexibility

### Proposed solution

Add sub-issue endpoint

### Additional context

https://github.blog/changelog/2025-01-13-evolving-github-issues-public-preview

https://docs.github.com/en/rest/issues/sub-issues

Discussion:
@wtf403: https://github.com/github/github-mcp-server/issues/117@SamMorrowDrums: https://github.com/github/github-mcp-server/pull/235@e7217: @wtf403 @SamMorrowDrums 
Hello, it seems that this issue is not progressing.
Would it be okay if I proceed with supporting the sub-issue feature?@a-man-called-q: @SamMorrowDrums it seems like #470 is available now.


Solution patch:
diff --git a/README.md b/README.md
index c06142b..39aa015 100644
--- a/README.md
+++ b/README.md
@@ -478,6 +478,13 @@ The following sets of tools are available (all are on by default):
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
 
+- **add_sub_issue** - Add sub-issue
+  - `issue_number`: The number of the parent issue (number, required)
+  - `owner`: Repository owner (string, required)
+  - `replace_parent`: When true, replaces the sub-issue's current parent issue (boolean, optional)
+  - `repo`: Repository name (string, required)
+  - `sub_issue_id`: The ID of the sub-issue to add. ID is not the same as issue number (number, required)
+
 - **assign_copilot_to_issue** - Assign Copilot to issue
   - `issueNumber`: Issue number (number, required)
   - `owner`: Repository owner (string, required)
@@ -515,6 +522,27 @@ The following sets of tools are available (all are on by default):
   - `sort`: Sort order (string, optional)
   - `state`: Filter by state (string, optional)
 
+- **list_sub_issues** - List sub-issues
+  - `issue_number`: Issue number (number, required)
+  - `owner`: Repository owner (string, required)
+  - `page`: Page number for pagination (default: 1) (number, optional)
+  - `per_page`: Number of results per page (max 100, default: 30) (number, optional)
+  - `repo`: Repository name (string, required)
+
+- **remove_sub_issue** - Remove sub-issue
+  - `issue_number`: The number of the parent issue (number, required)
+  - `owner`: Repository owner (string, required)
+  - `repo`: Repository name (string, required)
+  - `sub_issue_id`: The ID of the sub-issue to remove. ID is not the same as issue number (number, required)
+
+- **reprioritize_sub_issue** - Reprioritize sub-issue
+  - `after_id`: The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified) (number, optional)
+  - `before_id`: The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified) (number, optional)
+  - `issue_number`: The number of the parent issue (number, required)
+  - `owner`: Repository owner (string, required)
+  - `repo`: Repository name (string, required)
+  - `sub_issue_id`: The ID of the sub-issue to reprioritize. ID is not the same as issue number (number, required)
+
 - **search_issues** - Search issues
   - `order`: Sort order (string, optional)
   - `owner`: Optional repository owner. If provided with repo, only notifications for this repository are listed. (string, optional)
diff --git a/pkg/github/__toolsnaps__/add_sub_issue.snap b/pkg/github/__toolsnaps__/add_sub_issue.snap
new file mode 100644
index 0000000..2d462bc
--- /dev/null
+++ b/pkg/github/__toolsnaps__/add_sub_issue.snap
@@ -0,0 +1,39 @@
+{
+  "annotations": {
+    "title": "Add sub-issue",
+    "readOnlyHint": false
+  },
+  "description": "Add a sub-issue to a parent issue in a GitHub repository.",
+  "inputSchema": {
+    "properties": {
+      "issue_number": {
+        "description": "The number of the parent issue",
+        "type": "number"
+      },
+      "owner": {
+        "description": "Repository owner",
+        "type": "string"
+      },
+      "replace_parent": {
+        "description": "When true, replaces the sub-issue's current parent issue",
+        "type": "boolean"
+      },
+      "repo": {
+        "description": "Repository name",
+        "type": "string"
+      },
+      "sub_issue_id": {
+        "description": "The ID of the sub-issue to add. ID is not the same as issue number",
+        "type": "number"
+      }
+    },
+    "required": [
+      "owner",
+      "repo",
+      "issue_number",
+      "sub_issue_id"
+    ],
+    "type": "object"
+  },
+  "name": "add_sub_issue"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/list_sub_issues.snap b/pkg/github/__toolsnaps__/list_sub_issues.snap
new file mode 100644
index 0000000..70640e2
--- /dev/null
+++ b/pkg/github/__toolsnaps__/list_sub_issues.snap
@@ -0,0 +1,38 @@
+{
+  "annotations": {
+    "title": "List sub-issues",
+    "readOnlyHint": true
+  },
+  "description": "List sub-issues for a specific issue in a GitHub repository.",
+  "inputSchema": {
+    "properties": {
+      "issue_number": {
+        "description": "Issue number",
+        "type": "number"
+      },
+      "owner": {
+        "description": "Repository owner",
+        "type": "string"
+      },
+      "page": {
+        "description": "Page number for pagination (default: 1)",
+        "type": "number"
+      },
+      "per_page": {
+        "description": "Number of results per page (max 100, default: 30)",
+        "type": "number"
+      },
+      "repo": {
+        "description": "Repository name",
+        "type": "string"
+      }
+    },
+    "required": [
+      "owner",
+      "repo",
+      "issue_number"
+    ],
+    "type": "object"
+  },
+  "name": "list_sub_issues"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/remove_sub_issue.snap b/pkg/github/__toolsnaps__/remove_sub_issue.snap
new file mode 100644
index 0000000..a290200
--- /dev/null
+++ b/pkg/github/__toolsnaps__/remove_sub_issue.snap
@@ -0,0 +1,35 @@
+{
+  "annotations": {
+    "title": "Remove sub-issue",
+    "readOnlyHint": false
+  },
+  "description": "Remove a sub-issue from a parent issue in a GitHub repository.",
+  "inputSchema": {
+    "properties": {
+      "issue_number": {
+        "description": "The number of the parent issue",
+        "type": "number"
+      },
+      "owner": {
+        "description": "Repository owner",
+        "type": "string"
+      },
+      "repo": {
+        "description": "Repository name",
+        "type": "string"
+      },
+      "sub_issue_id": {
+        "description": "The ID of the sub-issue to remove. ID is not the same as issue number",
+        "type": "number"
+      }
+    },
+    "required": [
+      "owner",
+      "repo",
+      "issue_number",
+      "sub_issue_id"
+    ],
+    "type": "object"
+  },
+  "name": "remove_sub_issue"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/reprioritize_sub_issue.snap b/pkg/github/__toolsnaps__/reprioritize_sub_issue.snap
new file mode 100644
index 0000000..43c258b
--- /dev/null
+++ b/pkg/github/__toolsnaps__/reprioritize_sub_issue.snap
@@ -0,0 +1,43 @@
+{
+  "annotations": {
+    "title": "Reprioritize sub-issue",
+    "readOnlyHint": false
+  },
+  "description": "Reprioritize a sub-issue to a different position in the parent issue's sub-issue list.",
+  "inputSchema": {
+    "properties": {
+      "after_id": {
+        "description": "The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)",
+        "type": "number"
+      },
+      "before_id": {
+        "description": "The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)",
+        "type": "number"
+      },
+      "issue_number": {
+        "description": "The number of the parent issue",
+        "type": "number"
+      },
+      "owner": {
+        "description": "Repository owner",
+        "type": "string"
+      },
+      "repo": {
+        "description": "Repository name",
+        "type": "string"
+      },
+      "sub_issue_id": {
+        "description": "The ID of the sub-issue to reprioritize. ID is not the same as issue number",
+        "type": "number"
+      }
+    },
+    "required": [
+      "owner",
+      "repo",
+      "issue_number",
+      "sub_issue_id"
+    ],
+    "type": "object"
+  },
+  "name": "reprioritize_sub_issue"
+}
\ No newline at end of file
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index 80a150a..f718c37 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	ghErrors "github.com/github/github-mcp-server/pkg/errors"
 	"github.com/github/github-mcp-server/pkg/translations"
 	"github.com/go-viper/mapstructure/v2"
 	"github.com/google/go-github/v73/github"
@@ -153,6 +154,408 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
 		}
 }
 
+// AddSubIssue creates a tool to add a sub-issue to a parent issue.
+func AddSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("add_sub_issue",
+			mcp.WithDescription(t("TOOL_ADD_SUB_ISSUE_DESCRIPTION", "Add a sub-issue to a parent issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_ADD_SUB_ISSUE_USER_TITLE", "Add sub-issue"),
+				ReadOnlyHint: ToBoolPtr(false),
+			}),
+			mcp.WithString("owner",
+				mcp.Required(),
+				mcp.Description("Repository owner"),
+			),
+			mcp.WithString("repo",
+				mcp.Required(),
+				mcp.Description("Repository name"),
+			),
+			mcp.WithNumber("issue_number",
+				mcp.Required(),
+				mcp.Description("The number of the parent issue"),
+			),
+			mcp.WithNumber("sub_issue_id",
+				mcp.Required(),
+				mcp.Description("The ID of the sub-issue to add. ID is not the same as issue number"),
+			),
+			mcp.WithBoolean("replace_parent",
+				mcp.Description("When true, replaces the sub-issue's current parent issue"),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			owner, err := RequiredParam[string](request, "owner")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			repo, err := RequiredParam[string](request, "repo")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			issueNumber, err := RequiredInt(request, "issue_number")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			subIssueID, err := RequiredInt(request, "sub_issue_id")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			replaceParent, err := OptionalParam[bool](request, "replace_parent")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			subIssueRequest := github.SubIssueRequest{
+				SubIssueID:    int64(subIssueID),
+				ReplaceParent: ToBoolPtr(replaceParent),
+			}
+
+			subIssue, resp, err := client.SubIssue.Add(ctx, owner, repo, int64(issueNumber), subIssueRequest)
+			if err != nil {
+				return ghErrors.NewGitHubAPIErrorResponse(ctx,
+					"failed to add sub-issue",
+					resp,
+					err,
+				), nil
+			}
+
+			defer func() { _ = resp.Body.Close() }()
+
+			if resp.StatusCode != http.StatusCreated {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("failed to read response body: %w", err)
+				}
+				return mcp.NewToolResultError(fmt.Sprintf("failed to add sub-issue: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(subIssue)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
+
+// ListSubIssues creates a tool to list sub-issues for a GitHub issue.
+func ListSubIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("list_sub_issues",
+			mcp.WithDescription(t("TOOL_LIST_SUB_ISSUES_DESCRIPTION", "List sub-issues for a specific issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_SUB_ISSUES_USER_TITLE", "List sub-issues"),
+				ReadOnlyHint: ToBoolPtr(true),
+			}),
+			mcp.WithString("owner",
+				mcp.Required(),
+				mcp.Description("Repository owner"),
+			),
+			mcp.WithString("repo",
+				mcp.Required(),
+				mcp.Description("Repository name"),
+			),
+			mcp.WithNumber("issue_number",
+				mcp.Required(),
+				mcp.Description("Issue number"),
+			),
+			mcp.WithNumber("page",
+				mcp.Description("Page number for pagination (default: 1)"),
+			),
+			mcp.WithNumber("per_page",
+				mcp.Description("Number of results per page (max 100, default: 30)"),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			owner, err := RequiredParam[string](request, "owner")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			repo, err := RequiredParam[string](request, "repo")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			issueNumber, err := RequiredInt(request, "issue_number")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			page, err := OptionalIntParamWithDefault(request, "page", 1)
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			perPage, err := OptionalIntParamWithDefault(request, "per_page", 30)
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			opts := &github.IssueListOptions{
+				ListOptions: github.ListOptions{
+					Page:    page,
+					PerPage: perPage,
+				},
+			}
+
+			subIssues, resp, err := client.SubIssue.ListByIssue(ctx, owner, repo, int64(issueNumber), opts)
+			if err != nil {
+				return ghErrors.NewGitHubAPIErrorResponse(ctx,
+					"failed to list sub-issues",
+					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 mcp.NewToolResultError(fmt.Sprintf("failed to list sub-issues: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(subIssues)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+
+}
+
+// RemoveSubIssue creates a tool to remove a sub-issue from a parent issue.
+// Unlike other sub-issue tools, this currently uses a direct HTTP DELETE request
+// because of a bug in the go-github library.
+// Once the fix is released, this can be updated to use the library method.
+// See: https://github.com/google/go-github/pull/3613
+func RemoveSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("remove_sub_issue",
+			mcp.WithDescription(t("TOOL_REMOVE_SUB_ISSUE_DESCRIPTION", "Remove a sub-issue from a parent issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_REMOVE_SUB_ISSUE_USER_TITLE", "Remove sub-issue"),
+				ReadOnlyHint: ToBoolPtr(false),
+			}),
+			mcp.WithString("owner",
+				mcp.Required(),
+				mcp.Description("Repository owner"),
+			),
+			mcp.WithString("repo",
+				mcp.Required(),
+				mcp.Description("Repository name"),
+			),
+			mcp.WithNumber("issue_number",
+				mcp.Required(),
+				mcp.Description("The number of the parent issue"),
+			),
+			mcp.WithNumber("sub_issue_id",
+				mcp.Required(),
+				mcp.Description("The ID of the sub-issue to remove. ID is not the same as issue number"),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			owner, err := RequiredParam[string](request, "owner")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			repo, err := RequiredParam[string](request, "repo")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			issueNumber, err := RequiredInt(request, "issue_number")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			subIssueID, err := RequiredInt(request, "sub_issue_id")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			// Create the request body
+			requestBody := map[string]interface{}{
+				"sub_issue_id": subIssueID,
+			}
+			reqBodyBytes, err := json.Marshal(requestBody)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal request body: %w", err)
+			}
+
+			// Create the HTTP request
+			url := fmt.Sprintf("%srepos/%s/%s/issues/%d/sub_issue",
+				client.BaseURL.String(), owner, repo, issueNumber)
+			req, err := http.NewRequestWithContext(ctx, "DELETE", url, strings.NewReader(string(reqBodyBytes)))
+			if err != nil {
+				return nil, fmt.Errorf("failed to create request: %w", err)
+			}
+			req.Header.Set("Accept", "application/vnd.github+json")
+			req.Header.Set("Content-Type", "application/json")
+			req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
+
+			httpClient := client.Client() // Use authenticated GitHub client
+			resp, err := httpClient.Do(req)
+			if err != nil {
+				var ghResp *github.Response
+				if resp != nil {
+					ghResp = &github.Response{Response: resp}
+				}
+				return ghErrors.NewGitHubAPIErrorResponse(ctx,
+					"failed to remove sub-issue",
+					ghResp,
+					err,
+				), nil
+			}
+			defer func() { _ = resp.Body.Close() }()
+
+			body, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return nil, fmt.Errorf("failed to read response body: %w", err)
+			}
+
+			if resp.StatusCode != http.StatusOK {
+				return mcp.NewToolResultError(fmt.Sprintf("failed to remove sub-issue: %s", string(body))), nil
+			}
+
+			// Parse and re-marshal to ensure consistent formatting
+			var result interface{}
+			if err := json.Unmarshal(body, &result); err != nil {
+				return nil, fmt.Errorf("failed to unmarshal response: %w", err)
+			}
+
+			r, err := json.Marshal(result)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
+
+// ReprioritizeSubIssue creates a tool to reprioritize a sub-issue to a different position in the parent list.
+func ReprioritizeSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("reprioritize_sub_issue",
+			mcp.WithDescription(t("TOOL_REPRIORITIZE_SUB_ISSUE_DESCRIPTION", "Reprioritize a sub-issue to a different position in the parent issue's sub-issue list.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_REPRIORITIZE_SUB_ISSUE_USER_TITLE", "Reprioritize sub-issue"),
+				ReadOnlyHint: ToBoolPtr(false),
+			}),
+			mcp.WithString("owner",
+				mcp.Required(),
+				mcp.Description("Repository owner"),
+			),
+			mcp.WithString("repo",
+				mcp.Required(),
+				mcp.Description("Repository name"),
+			),
+			mcp.WithNumber("issue_number",
+				mcp.Required(),
+				mcp.Description("The number of the parent issue"),
+			),
+			mcp.WithNumber("sub_issue_id",
+				mcp.Required(),
+				mcp.Description("The ID of the sub-issue to reprioritize. ID is not the same as issue number"),
+			),
+			mcp.WithNumber("after_id",
+				mcp.Description("The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)"),
+			),
+			mcp.WithNumber("before_id",
+				mcp.Description("The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)"),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			owner, err := RequiredParam[string](request, "owner")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			repo, err := RequiredParam[string](request, "repo")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			issueNumber, err := RequiredInt(request, "issue_number")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			subIssueID, err := RequiredInt(request, "sub_issue_id")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			// Handle optional positioning parameters
+			afterID, err := OptionalIntParam(request, "after_id")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			beforeID, err := OptionalIntParam(request, "before_id")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			// Validate that either after_id or before_id is specified, but not both
+			if afterID == 0 && beforeID == 0 {
+				return mcp.NewToolResultError("either after_id or before_id must be specified"), nil
+			}
+			if afterID != 0 && beforeID != 0 {
+				return mcp.NewToolResultError("only one of after_id or before_id should be specified, not both"), nil
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			subIssueRequest := github.SubIssueRequest{
+				SubIssueID: int64(subIssueID),
+			}
+
+			if afterID != 0 {
+				afterIDInt64 := int64(afterID)
+				subIssueRequest.AfterID = &afterIDInt64
+			}
+			if beforeID != 0 {
+				beforeIDInt64 := int64(beforeID)
+				subIssueRequest.BeforeID = &beforeIDInt64
+			}
+
+			subIssue, resp, err := client.SubIssue.Reprioritize(ctx, owner, repo, int64(issueNumber), subIssueRequest)
+			if err != nil {
+				return ghErrors.NewGitHubAPIErrorResponse(ctx,
+					"failed to reprioritize sub-issue",
+					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 mcp.NewToolResultError(fmt.Sprintf("failed to reprioritize sub-issue: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(subIssue)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
+
 // SearchIssues creates a tool to search for issues.
 func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("search_issues",
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index bd34917..e01b7cc 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -53,12 +53,16 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
 			toolsets.NewServerTool(SearchIssues(getClient, t)),
 			toolsets.NewServerTool(ListIssues(getClient, t)),
 			toolsets.NewServerTool(GetIssueComments(getClient, t)),
+			toolsets.NewServerTool(ListSubIssues(getClient, t)),
 		).
 		AddWriteTools(
 			toolsets.NewServerTool(CreateIssue(getClient, t)),
 			toolsets.NewServerTool(AddIssueComment(getClient, t)),
 			toolsets.NewServerTool(UpdateIssue(getClient, t)),
 			toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)),
+			toolsets.NewServerTool(AddSubIssue(getClient, t)),
+			toolsets.NewServerTool(RemoveSubIssue(getClient, t)),
+			toolsets.NewServerTool(ReprioritizeSubIssue(getClient, t)),
 		).AddPrompts(toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)))
 	users := toolsets.NewToolset("users", "GitHub User related tools").
 		AddReadTools(



Test patch:
diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go
index 1462594..2bdb89b 100644
--- a/pkg/github/issues_test.go
+++ b/pkg/github/issues_test.go
@@ -38,6 +38,9 @@ func Test_GetIssue(t *testing.T) {
 		Body:    github.Ptr("This is a test issue"),
 		State:   github.Ptr("open"),
 		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
+		User: &github.User{
+			Login: github.Ptr("testuser"),
+		},
 	}
 
 	tests := []struct {
@@ -111,6 +114,9 @@ func Test_GetIssue(t *testing.T) {
 			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
 			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
 			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
+			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
+			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
+			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
 		})
 	}
 }
@@ -1629,3 +1635,969 @@ func TestAssignCopilotToIssue(t *testing.T) {
 		})
 	}
 }
+
+func Test_AddSubIssue(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := AddSubIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+	require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+	assert.Equal(t, "add_sub_issue", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.Contains(t, tool.InputSchema.Properties, "issue_number")
+	assert.Contains(t, tool.InputSchema.Properties, "sub_issue_id")
+	assert.Contains(t, tool.InputSchema.Properties, "replace_parent")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number", "sub_issue_id"})
+
+	// Setup mock issue for success case (matches GitHub API response format)
+	mockIssue := &github.Issue{
+		Number:  github.Ptr(42),
+		Title:   github.Ptr("Parent Issue"),
+		Body:    github.Ptr("This is the parent issue with a sub-issue"),
+		State:   github.Ptr("open"),
+		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
+		User: &github.User{
+			Login: github.Ptr("testuser"),
+		},
+		Labels: []*github.Label{
+			{
+				Name:        github.Ptr("enhancement"),
+				Color:       github.Ptr("84b6eb"),
+				Description: github.Ptr("New feature or request"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedIssue  *github.Issue
+		expectedErrMsg string
+	}{
+		{
+			name: "successful sub-issue addition with all parameters",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusCreated, mockIssue),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":          "owner",
+				"repo":           "repo",
+				"issue_number":   float64(42),
+				"sub_issue_id":   float64(123),
+				"replace_parent": true,
+			},
+			expectError:   false,
+			expectedIssue: mockIssue,
+		},
+		{
+			name: "successful sub-issue addition with minimal parameters",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusCreated, mockIssue),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(456),
+			},
+			expectError:   false,
+			expectedIssue: mockIssue,
+		},
+		{
+			name: "successful sub-issue addition with replace_parent false",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusCreated, mockIssue),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":          "owner",
+				"repo":           "repo",
+				"issue_number":   float64(42),
+				"sub_issue_id":   float64(789),
+				"replace_parent": false,
+			},
+			expectError:   false,
+			expectedIssue: mockIssue,
+		},
+		{
+			name: "parent issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Parent issue not found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(999),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to add sub-issue",
+		},
+		{
+			name: "sub-issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Sub-issue not found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(999),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to add sub-issue",
+		},
+		{
+			name: "validation failed - sub-issue cannot be parent of itself",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusUnprocessableEntity, `{"message": "Validation failed", "errors": [{"message": "Sub-issue cannot be a parent of itself"}]}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(42),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to add sub-issue",
+		},
+		{
+			name: "insufficient permissions",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusForbidden, `{"message": "Must have write access to repository"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to add sub-issue",
+		},
+		{
+			name:         "missing required parameter owner",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: owner",
+		},
+		{
+			name:         "missing required parameter sub_issue_id",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: sub_issue_id",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := AddSubIssue(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			if tc.expectedErrMsg != "" {
+				require.NotNil(t, result)
+				textContent := getTextResult(t, result)
+				assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedIssue github.Issue
+			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
+			require.NoError(t, err)
+			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
+			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
+			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
+			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
+			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
+			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
+		})
+	}
+}
+
+func Test_ListSubIssues(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := ListSubIssues(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+	require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+	assert.Equal(t, "list_sub_issues", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.Contains(t, tool.InputSchema.Properties, "issue_number")
+	assert.Contains(t, tool.InputSchema.Properties, "page")
+	assert.Contains(t, tool.InputSchema.Properties, "per_page")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"})
+
+	// Setup mock sub-issues for success case
+	mockSubIssues := []*github.Issue{
+		{
+			Number:  github.Ptr(123),
+			Title:   github.Ptr("Sub-issue 1"),
+			Body:    github.Ptr("This is the first sub-issue"),
+			State:   github.Ptr("open"),
+			HTMLURL: github.Ptr("https://github.com/owner/repo/issues/123"),
+			User: &github.User{
+				Login: github.Ptr("user1"),
+			},
+			Labels: []*github.Label{
+				{
+					Name:        github.Ptr("bug"),
+					Color:       github.Ptr("d73a4a"),
+					Description: github.Ptr("Something isn't working"),
+				},
+			},
+		},
+		{
+			Number:  github.Ptr(124),
+			Title:   github.Ptr("Sub-issue 2"),
+			Body:    github.Ptr("This is the second sub-issue"),
+			State:   github.Ptr("closed"),
+			HTMLURL: github.Ptr("https://github.com/owner/repo/issues/124"),
+			User: &github.User{
+				Login: github.Ptr("user2"),
+			},
+			Assignees: []*github.User{
+				{Login: github.Ptr("assignee1")},
+			},
+		},
+	}
+
+	tests := []struct {
+		name              string
+		mockedClient      *http.Client
+		requestArgs       map[string]interface{}
+		expectError       bool
+		expectedSubIssues []*github.Issue
+		expectedErrMsg    string
+	}{
+		{
+			name: "successful sub-issues listing with minimal parameters",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatch(
+					mock.GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockSubIssues,
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:       false,
+			expectedSubIssues: mockSubIssues,
+		},
+		{
+			name: "successful sub-issues listing with pagination",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					expectQueryParams(t, map[string]string{
+						"page":     "2",
+						"per_page": "10",
+					}).andThen(
+						mockResponse(t, http.StatusOK, mockSubIssues),
+					),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"page":         float64(2),
+				"per_page":     float64(10),
+			},
+			expectError:       false,
+			expectedSubIssues: mockSubIssues,
+		},
+		{
+			name: "successful sub-issues listing with empty result",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatch(
+					mock.GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					[]*github.Issue{},
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:       false,
+			expectedSubIssues: []*github.Issue{},
+		},
+		{
+			name: "parent issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(999),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to list sub-issues",
+		},
+		{
+			name: "repository not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "nonexistent",
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to list sub-issues",
+		},
+		{
+			name: "sub-issues feature gone/deprecated",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusGone, `{"message": "This feature has been deprecated"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to list sub-issues",
+		},
+		{
+			name:         "missing required parameter owner",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: owner",
+		},
+		{
+			name:         "missing required parameter issue_number",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: issue_number",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := ListSubIssues(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			if tc.expectedErrMsg != "" {
+				require.NotNil(t, result)
+				textContent := getTextResult(t, result)
+				assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedSubIssues []*github.Issue
+			err = json.Unmarshal([]byte(textContent.Text), &returnedSubIssues)
+			require.NoError(t, err)
+
+			assert.Len(t, returnedSubIssues, len(tc.expectedSubIssues))
+			for i, subIssue := range returnedSubIssues {
+				if i < len(tc.expectedSubIssues) {
+					assert.Equal(t, *tc.expectedSubIssues[i].Number, *subIssue.Number)
+					assert.Equal(t, *tc.expectedSubIssues[i].Title, *subIssue.Title)
+					assert.Equal(t, *tc.expectedSubIssues[i].State, *subIssue.State)
+					assert.Equal(t, *tc.expectedSubIssues[i].HTMLURL, *subIssue.HTMLURL)
+					assert.Equal(t, *tc.expectedSubIssues[i].User.Login, *subIssue.User.Login)
+
+					if tc.expectedSubIssues[i].Body != nil {
+						assert.Equal(t, *tc.expectedSubIssues[i].Body, *subIssue.Body)
+					}
+				}
+			}
+		})
+	}
+}
+
+func Test_RemoveSubIssue(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := RemoveSubIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+	require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+	assert.Equal(t, "remove_sub_issue", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.Contains(t, tool.InputSchema.Properties, "issue_number")
+	assert.Contains(t, tool.InputSchema.Properties, "sub_issue_id")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number", "sub_issue_id"})
+
+	// Setup mock issue for success case (matches GitHub API response format - the updated parent issue)
+	mockIssue := &github.Issue{
+		Number:  github.Ptr(42),
+		Title:   github.Ptr("Parent Issue"),
+		Body:    github.Ptr("This is the parent issue after sub-issue removal"),
+		State:   github.Ptr("open"),
+		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
+		User: &github.User{
+			Login: github.Ptr("testuser"),
+		},
+		Labels: []*github.Label{
+			{
+				Name:        github.Ptr("enhancement"),
+				Color:       github.Ptr("84b6eb"),
+				Description: github.Ptr("New feature or request"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedIssue  *github.Issue
+		expectedErrMsg string
+	}{
+		{
+			name: "successful sub-issue removal",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusOK, mockIssue),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:   false,
+			expectedIssue: mockIssue,
+		},
+		{
+			name: "parent issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(999),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to remove sub-issue",
+		},
+		{
+			name: "sub-issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Sub-issue not found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(999),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to remove sub-issue",
+		},
+		{
+			name: "bad request - invalid sub_issue_id",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusBadRequest, `{"message": "Invalid sub_issue_id"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(-1),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to remove sub-issue",
+		},
+		{
+			name: "repository not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "nonexistent",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to remove sub-issue",
+		},
+		{
+			name: "insufficient permissions",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusForbidden, `{"message": "Must have write access to repository"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to remove sub-issue",
+		},
+		{
+			name:         "missing required parameter owner",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: owner",
+		},
+		{
+			name:         "missing required parameter sub_issue_id",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: sub_issue_id",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := RemoveSubIssue(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			if tc.expectedErrMsg != "" {
+				require.NotNil(t, result)
+				textContent := getTextResult(t, result)
+				assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedIssue github.Issue
+			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
+			require.NoError(t, err)
+			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
+			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
+			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
+			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
+			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
+			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
+		})
+	}
+}
+
+func Test_ReprioritizeSubIssue(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := ReprioritizeSubIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+	require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+	assert.Equal(t, "reprioritize_sub_issue", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.Contains(t, tool.InputSchema.Properties, "issue_number")
+	assert.Contains(t, tool.InputSchema.Properties, "sub_issue_id")
+	assert.Contains(t, tool.InputSchema.Properties, "after_id")
+	assert.Contains(t, tool.InputSchema.Properties, "before_id")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number", "sub_issue_id"})
+
+	// Setup mock issue for success case (matches GitHub API response format - the updated parent issue)
+	mockIssue := &github.Issue{
+		Number:  github.Ptr(42),
+		Title:   github.Ptr("Parent Issue"),
+		Body:    github.Ptr("This is the parent issue with reprioritized sub-issues"),
+		State:   github.Ptr("open"),
+		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
+		User: &github.User{
+			Login: github.Ptr("testuser"),
+		},
+		Labels: []*github.Label{
+			{
+				Name:        github.Ptr("enhancement"),
+				Color:       github.Ptr("84b6eb"),
+				Description: github.Ptr("New feature or request"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedIssue  *github.Issue
+		expectedErrMsg string
+	}{
+		{
+			name: "successful reprioritization with after_id",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusOK, mockIssue),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"after_id":     float64(456),
+			},
+			expectError:   false,
+			expectedIssue: mockIssue,
+		},
+		{
+			name: "successful reprioritization with before_id",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusOK, mockIssue),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"before_id":    float64(789),
+			},
+			expectError:   false,
+			expectedIssue: mockIssue,
+		},
+		{
+			name:         "validation error - neither after_id nor before_id specified",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+			},
+			expectError:    false,
+			expectedErrMsg: "either after_id or before_id must be specified",
+		},
+		{
+			name:         "validation error - both after_id and before_id specified",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"after_id":     float64(456),
+				"before_id":    float64(789),
+			},
+			expectError:    false,
+			expectedErrMsg: "only one of after_id or before_id should be specified, not both",
+		},
+		{
+			name: "parent issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(999),
+				"sub_issue_id": float64(123),
+				"after_id":     float64(456),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to reprioritize sub-issue",
+		},
+		{
+			name: "sub-issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusNotFound, `{"message": "Sub-issue not found"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(999),
+				"after_id":     float64(456),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to reprioritize sub-issue",
+		},
+		{
+			name: "validation failed - positioning sub-issue not found",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusUnprocessableEntity, `{"message": "Validation failed", "errors": [{"message": "Positioning sub-issue not found"}]}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"after_id":     float64(999),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to reprioritize sub-issue",
+		},
+		{
+			name: "insufficient permissions",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusForbidden, `{"message": "Must have write access to repository"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"after_id":     float64(456),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to reprioritize sub-issue",
+		},
+		{
+			name: "service unavailable",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber,
+					mockResponse(t, http.StatusServiceUnavailable, `{"message": "Service Unavailable"}`),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"before_id":    float64(456),
+			},
+			expectError:    false,
+			expectedErrMsg: "failed to reprioritize sub-issue",
+		},
+		{
+			name:         "missing required parameter owner",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"sub_issue_id": float64(123),
+				"after_id":     float64(456),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: owner",
+		},
+		{
+			name:         "missing required parameter sub_issue_id",
+			mockedClient: mock.NewMockedHTTPClient(
+			// No mocked requests needed since validation fails before HTTP call
+			),
+			requestArgs: map[string]interface{}{
+				"owner":        "owner",
+				"repo":         "repo",
+				"issue_number": float64(42),
+				"after_id":     float64(456),
+			},
+			expectError:    false,
+			expectedErrMsg: "missing required parameter: sub_issue_id",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := ReprioritizeSubIssue(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			if tc.expectedErrMsg != "" {
+				require.NotNil(t, result)
+				textContent := getTextResult(t, result)
+				assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedIssue github.Issue
+			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
+			require.NoError(t, err)
+			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
+			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
+			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
+			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
+			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
+			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
+		})
+	}
+}