@@ -3,6 +3,7 @@ package github
33import (
44 "context"
55 "encoding/json"
6+ "errors"
67 "fmt"
78 "io"
89 "net/http"
@@ -2702,6 +2703,236 @@ Options are:
27022703 return st
27032704}
27042705
2706+ type CreateIssueInput struct {
2707+ RepositoryID githubv4.ID `json:"repositoryId"`
2708+ Title githubv4.String `json:"title"`
2709+
2710+ Body * githubv4.String `json:"body,omitempty"`
2711+ AssigneeIDs * []githubv4.ID `json:"assigneeIds,omitempty"`
2712+ MilestoneID * githubv4.ID `json:"milestoneId,omitempty"`
2713+ LabelIDs * []githubv4.ID `json:"labelIds,omitempty"`
2714+ IssueTypeID * githubv4.ID `json:"issueTypeId,omitempty"`
2715+ ParentIssueID * githubv4.ID `json:"parentIssueId,omitempty"`
2716+ }
2717+
2718+ type createIssueMutation struct {
2719+ CreateIssue struct {
2720+ Issue struct {
2721+ FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
2722+ URL githubv4.URI
2723+ }
2724+ } `graphql:"createIssue(input: $input)"`
2725+ }
2726+
2727+ type createIssueParentMetadataQuery struct {
2728+ ChildRepository struct {
2729+ ID githubv4.ID
2730+ } `graphql:"childRepository: repository(owner: $owner, name: $repo)"`
2731+ ParentRepository struct {
2732+ Issue struct {
2733+ ID githubv4.ID
2734+ } `graphql:"issue(number: $parentIssueNumber)"`
2735+ } `graphql:"parentRepository: repository(owner: $parentOwner, name: $parentRepo)"`
2736+ }
2737+
2738+ func createIssueWithParent (
2739+ ctx context.Context ,
2740+ client * github.Client ,
2741+ gqlClient * githubv4.Client ,
2742+ owner string ,
2743+ repo string ,
2744+ title string ,
2745+ body string ,
2746+ assignees []string ,
2747+ labels []string ,
2748+ milestoneNumber int ,
2749+ issueType string ,
2750+ parentIssueNumber int ,
2751+ parentOwner string ,
2752+ parentRepo string ,
2753+ ) (* mcp.CallToolResult , error ) {
2754+ if title == "" {
2755+ return utils .NewToolResultError ("missing required parameter: title" ), nil
2756+ }
2757+ if parentIssueNumber < 1 {
2758+ return utils .NewToolResultError ("parent_issue_number must be greater than 0" ), nil
2759+ }
2760+
2761+ parentOwner , parentRepo = parentRepository (owner , repo , parentOwner , parentRepo )
2762+ repositoryID , parentIssueID , err := resolveCreateIssueParent (ctx , gqlClient , owner , repo , parentOwner , parentRepo , parentIssueNumber )
2763+ if err != nil {
2764+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , "failed to resolve parent issue" , err ), nil
2765+ }
2766+
2767+ input := CreateIssueInput {
2768+ RepositoryID : repositoryID ,
2769+ Title : githubv4 .String (title ),
2770+ ParentIssueID : & parentIssueID ,
2771+ }
2772+ if body != "" {
2773+ input .Body = githubv4 .NewString (githubv4 .String (body ))
2774+ }
2775+
2776+ if len (labels ) > 0 {
2777+ labelIDs := make ([]githubv4.ID , 0 , len (labels ))
2778+ for _ , label := range labels {
2779+ labelID , err := getLabelID (ctx , gqlClient , owner , repo , label )
2780+ if err != nil {
2781+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , fmt .Sprintf ("failed to resolve label %q" , label ), err ), nil
2782+ }
2783+ labelIDs = append (labelIDs , labelID )
2784+ }
2785+ input .LabelIDs = & labelIDs
2786+ }
2787+
2788+ if len (assignees ) > 0 {
2789+ assigneeIDs := make ([]githubv4.ID , 0 , len (assignees ))
2790+ for _ , assignee := range assignees {
2791+ assigneeID , err := resolveUserID (ctx , gqlClient , assignee )
2792+ if err != nil {
2793+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , fmt .Sprintf ("failed to resolve assignee %q" , assignee ), err ), nil
2794+ }
2795+ assigneeIDs = append (assigneeIDs , assigneeID )
2796+ }
2797+ input .AssigneeIDs = & assigneeIDs
2798+ }
2799+
2800+ if milestoneNumber != 0 {
2801+ milestoneID , err := resolveMilestoneID (ctx , gqlClient , owner , repo , milestoneNumber )
2802+ if err != nil {
2803+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , "failed to resolve milestone" , err ), nil
2804+ }
2805+ input .MilestoneID = & milestoneID
2806+ }
2807+
2808+ if issueType != "" {
2809+ issueTypeID , resp , err := resolveIssueTypeID (ctx , client , owner , repo , issueType )
2810+ if err != nil {
2811+ return ghErrors .NewGitHubAPIErrorResponse (ctx , fmt .Sprintf ("failed to resolve issue type %q" , issueType ), resp , err ), nil
2812+ }
2813+ input .IssueTypeID = & issueTypeID
2814+ }
2815+
2816+ var mutation createIssueMutation
2817+ if err := gqlClient .Mutate (ctx , & mutation , input , nil ); err != nil {
2818+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , "failed to create issue" , err ), nil
2819+ }
2820+
2821+ response := MinimalResponse {
2822+ ID : string (mutation .CreateIssue .Issue .FullDatabaseID ),
2823+ URL : mutation .CreateIssue .Issue .URL .String (),
2824+ }
2825+ encoded , err := json .Marshal (response )
2826+ if err != nil {
2827+ return utils .NewToolResultErrorFromErr ("failed to marshal response" , err ), nil
2828+ }
2829+ return utils .NewToolResultText (string (encoded )), nil
2830+ }
2831+
2832+ func parentRepository (owner , repo , parentOwner , parentRepo string ) (string , string ) {
2833+ if parentOwner == "" && parentRepo == "" {
2834+ return owner , repo
2835+ }
2836+ return parentOwner , parentRepo
2837+ }
2838+
2839+ func validateParentRepository (parentProvided bool , parentOwner , parentRepo string ) error {
2840+ if ! parentProvided {
2841+ if parentOwner != "" || parentRepo != "" {
2842+ return errors .New ("parent_owner and parent_repo can only be used when parent_issue_number is provided" )
2843+ }
2844+ return nil
2845+ }
2846+ if (parentOwner == "" ) != (parentRepo == "" ) {
2847+ return errors .New ("parent_owner and parent_repo must be provided together" )
2848+ }
2849+ return nil
2850+ }
2851+
2852+ func resolveCreateIssueParent (ctx context.Context , gqlClient * githubv4.Client , owner , repo , parentOwner , parentRepo string , parentIssueNumber int ) (githubv4.ID , githubv4.ID , error ) {
2853+ var query createIssueParentMetadataQuery
2854+ variables := map [string ]any {
2855+ "owner" : githubv4 .String (owner ),
2856+ "repo" : githubv4 .String (repo ),
2857+ "parentOwner" : githubv4 .String (parentOwner ),
2858+ "parentRepo" : githubv4 .String (parentRepo ),
2859+ "parentIssueNumber" : githubv4 .Int (parentIssueNumber ), // #nosec G115 - issue numbers are small positive integers
2860+ }
2861+ if err := gqlClient .Query (ctx , & query , variables ); err != nil {
2862+ return "" , "" , err
2863+ }
2864+ if query .ChildRepository .ID == "" {
2865+ return "" , "" , fmt .Errorf ("repository %s/%s was not found" , owner , repo )
2866+ }
2867+ if query .ParentRepository .Issue .ID == "" {
2868+ return "" , "" , fmt .Errorf ("parent issue #%d was not found in %s/%s" , parentIssueNumber , parentOwner , parentRepo )
2869+ }
2870+ return query .ChildRepository .ID , query .ParentRepository .Issue .ID , nil
2871+ }
2872+
2873+ func resolveUserID (ctx context.Context , gqlClient * githubv4.Client , login string ) (githubv4.ID , error ) {
2874+ var query struct {
2875+ User struct {
2876+ ID githubv4.ID
2877+ Login githubv4.String
2878+ } `graphql:"user(login: $login)"`
2879+ }
2880+ if err := gqlClient .Query (ctx , & query , map [string ]any {"login" : githubv4 .String (login )}); err != nil {
2881+ return "" , err
2882+ }
2883+ if query .User .ID == "" {
2884+ return "" , fmt .Errorf ("user %q was not found" , login )
2885+ }
2886+ return query .User .ID , nil
2887+ }
2888+
2889+ func resolveMilestoneID (ctx context.Context , gqlClient * githubv4.Client , owner , repo string , milestoneNumber int ) (githubv4.ID , error ) {
2890+ var query struct {
2891+ Repository struct {
2892+ Milestone struct {
2893+ ID githubv4.ID
2894+ } `graphql:"milestone(number: $milestoneNumber)"`
2895+ } `graphql:"repository(owner: $owner, name: $repo)"`
2896+ }
2897+ variables := map [string ]any {
2898+ "owner" : githubv4 .String (owner ),
2899+ "repo" : githubv4 .String (repo ),
2900+ "milestoneNumber" : githubv4 .Int (milestoneNumber ), // #nosec G115 - milestone numbers are small positive integers
2901+ }
2902+ if err := gqlClient .Query (ctx , & query , variables ); err != nil {
2903+ return "" , err
2904+ }
2905+ if query .Repository .Milestone .ID == "" {
2906+ return "" , fmt .Errorf ("milestone #%d was not found in %s/%s" , milestoneNumber , owner , repo )
2907+ }
2908+ return query .Repository .Milestone .ID , nil
2909+ }
2910+
2911+ func resolveIssueTypeID (ctx context.Context , client * github.Client , owner , repo , issueTypeName string ) (githubv4.ID , * github.Response , error ) {
2912+ req , err := client .NewRequest (ctx , "GET" , fmt .Sprintf ("repos/%s/%s/issue-types" , owner , repo ), nil )
2913+ if err != nil {
2914+ return "" , nil , err
2915+ }
2916+
2917+ var issueTypes []* github.IssueType
2918+ resp , err := client .Do (req , & issueTypes )
2919+ if resp != nil && resp .Body != nil {
2920+ defer func () { _ = resp .Body .Close () }()
2921+ }
2922+ if err != nil {
2923+ return "" , resp , err
2924+ }
2925+ for _ , issueType := range issueTypes {
2926+ if issueType != nil && strings .EqualFold (strings .TrimSpace (issueType .GetName ()), strings .TrimSpace (issueTypeName )) {
2927+ if issueType .GetNodeID () == "" {
2928+ return "" , resp , fmt .Errorf ("issue type %q is missing a node ID" , issueTypeName )
2929+ }
2930+ return githubv4 .ID (issueType .GetNodeID ()), resp , nil
2931+ }
2932+ }
2933+ return "" , resp , fmt .Errorf ("issue type %q was not found in %s/%s" , issueTypeName , owner , repo )
2934+ }
2935+
27052936func CreateIssue (ctx context.Context , client * github.Client , owner string , repo string , title string , body string , assignees []string , labels []string , milestoneNum int , issueType string , issueFieldValues []* github.IssueRequestFieldValue ) (* mcp.CallToolResult , error ) {
27062937 if title == "" {
27072938 return utils .NewToolResultError ("missing required parameter: title" ), nil
0 commit comments