@@ -6,14 +6,11 @@ import (
66"fmt"
77"io"
88"net/http"
9- "net/url"
10- "reflect"
119"strings"
1210
1311ghErrors"github.com/github/github-mcp-server/pkg/errors"
1412"github.com/github/github-mcp-server/pkg/translations"
1513"github.com/google/go-github/v79/github"
16- "github.com/google/go-querystring/query"
1714"github.com/mark3labs/mcp-go/mcp"
1815"github.com/mark3labs/mcp-go/server"
1916)
@@ -256,30 +253,19 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
256253return mcp .NewToolResultError (err .Error ()),nil
257254}
258255
256+ var resp * github.Response
257+ var projectFields []* github.ProjectV2Field
258+
259259opts := & github.ListProjectsOptions {
260260ListProjectsPaginationOptions :pagination ,
261261}
262262
263- var url string
264263if ownerType == "org" {
265- url = fmt . Sprintf ( "orgs/%s/projectsV2/%d/fields" ,owner ,projectNumber )
264+ projectFields , resp , err = client . Projects . ListOrganizationProjectFields ( ctx ,owner ,projectNumber , opts )
266265}else {
267- url = fmt .Sprintf ("users/%s/projectsV2/%d/fields" ,owner ,projectNumber )
268- }
269-
270- url ,err = addOptions (url ,opts )
271- if err != nil {
272- return mcp .NewToolResultError (err .Error ()),nil
266+ projectFields ,resp ,err = client .Projects .ListUserProjectFields (ctx ,owner ,projectNumber ,opts )
273267}
274268
275- httpRequest ,err := client .NewRequest ("GET" ,url ,nil )
276- if err != nil {
277- return nil ,fmt .Errorf ("failed to create request: %w" ,err )
278- }
279-
280- var projectFields []projectV2Field
281- resp ,err := client .Do (ctx ,httpRequest ,& projectFields )
282-
283269if err != nil {
284270return ghErrors .NewGitHubAPIErrorResponse (ctx ,
285271"failed to list project fields" ,
@@ -452,7 +438,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
452438}
453439
454440var resp * github.Response
455- var projectItems []projectV2Item
441+ var projectItems []* github. ProjectV2Item
456442var queryPtr * string
457443
458444if queryStr != "" {
@@ -467,25 +453,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
467453},
468454}
469455
470- var url string
471456if ownerType == "org" {
472- url = fmt . Sprintf ( "orgs/%s/projectsV2/%d/items" ,owner ,projectNumber )
457+ projectItems , resp , err = client . Projects . ListOrganizationProjectItems ( ctx ,owner ,projectNumber , opts )
473458}else {
474- url = fmt . Sprintf ( "users/%s/projectsV2/%d/items" ,owner ,projectNumber )
459+ projectItems , resp , err = client . Projects . ListUserProjectItems ( ctx ,owner ,projectNumber , opts )
475460}
476461
477- url ,err = addOptions (url ,opts )
478- if err != nil {
479- return mcp .NewToolResultError (err .Error ()),nil
480- }
481-
482- httpRequest ,err := client .NewRequest ("GET" ,url ,nil )
483- if err != nil {
484- return nil ,fmt .Errorf ("failed to create request: %w" ,err )
485- }
486-
487- resp ,err = client .Do (ctx ,httpRequest ,& projectItems )
488-
489462if err != nil {
490463return ghErrors .NewGitHubAPIErrorResponse (ctx ,
491464ProjectListFailedError ,
@@ -566,32 +539,22 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
566539return mcp .NewToolResultError (err .Error ()),nil
567540}
568541
569- var url string
570- if ownerType == "org" {
571- url = fmt .Sprintf ("orgs/%s/projectsV2/%d/items/%d" ,owner ,projectNumber ,itemID )
572- }else {
573- url = fmt .Sprintf ("users/%s/projectsV2/%d/items/%d" ,owner ,projectNumber ,itemID )
574- }
575-
576- opts := fieldSelectionOptions {}
542+ var resp * github.Response
543+ var projectItem * github.ProjectV2Item
544+ var opts * github.GetProjectItemOptions
577545
578546if len (fields )> 0 {
579- opts .Fields = fields
580- }
581-
582- url ,err = addOptions (url ,opts )
583- if err != nil {
584- return mcp .NewToolResultError (err .Error ()),nil
547+ opts = & github.GetProjectItemOptions {
548+ Fields :fields ,
549+ }
585550}
586551
587- projectItem := projectV2Item {}
588-
589- httpRequest ,err := client .NewRequest ("GET" ,url ,nil )
590- if err != nil {
591- return nil ,fmt .Errorf ("failed to create request: %w" ,err )
552+ if ownerType == "org" {
553+ projectItem ,resp ,err = client .Projects .GetOrganizationProjectItem (ctx ,owner ,projectNumber ,itemID ,opts )
554+ }else {
555+ projectItem ,resp ,err = client .Projects .GetUserProjectItem (ctx ,owner ,projectNumber ,itemID ,opts )
592556}
593557
594- resp ,err := client .Do (ctx ,httpRequest ,& projectItem )
595558if err != nil {
596559return ghErrors .NewGitHubAPIErrorResponse (ctx ,
597560"failed to get project item" ,
@@ -748,7 +711,7 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
748711if err != nil {
749712return mcp .NewToolResultError (err .Error ()),nil
750713}
751- itemID ,err := RequiredInt (req ,"item_id" )
714+ itemID ,err := RequiredBigInt (req ,"item_id" )
752715if err != nil {
753716return mcp .NewToolResultError (err .Error ()),nil
754717}
@@ -773,21 +736,15 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
773736return mcp .NewToolResultError (err .Error ()),nil
774737}
775738
776- var projectsURL string
739+ var resp * github.Response
740+ var updatedItem * github.ProjectV2Item
741+
777742if ownerType == "org" {
778- projectsURL = fmt . Sprintf ( "orgs/%s/projectsV2/%d/items/%d" ,owner ,projectNumber ,itemID )
743+ updatedItem , resp , err = client . Projects . UpdateOrganizationProjectItem ( ctx ,owner ,projectNumber ,itemID , updatePayload )
779744}else {
780- projectsURL = fmt .Sprintf ("users/%s/projectsV2/%d/items/%d" ,owner ,projectNumber ,itemID )
781- }
782- httpRequest ,err := client .NewRequest ("PATCH" ,projectsURL ,updateProjectItemPayload {
783- Fields : []updateProjectItem {* updatePayload },
784- })
785- if err != nil {
786- return nil ,fmt .Errorf ("failed to create request: %w" ,err )
745+ updatedItem ,resp ,err = client .Projects .UpdateUserProjectItem (ctx ,owner ,projectNumber ,itemID ,updatePayload )
787746}
788- updatedItem := projectV2Item {}
789747
790- resp ,err := client .Do (ctx ,httpRequest ,& updatedItem )
791748if err != nil {
792749return ghErrors .NewGitHubAPIErrorResponse (ctx ,
793750ProjectUpdateFailedError ,
@@ -886,76 +843,13 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
886843}
887844}
888845
889- type fieldSelectionOptions struct {
890- // Specific list of field IDs to include in the response. If not provided, only the title field is included.
891- // The comma tag encodes the slice as comma-separated values: fields=102589,985201,169875
892- Fields []int64 `url:"fields,omitempty,comma"`
893- }
894-
895- type updateProjectItemPayload struct {
896- Fields []updateProjectItem `json:"fields"`
897- }
898-
899- type updateProjectItem struct {
900- ID int `json:"id"`
901- Value any `json:"value"`
902- }
903-
904- type projectV2ItemFieldValue struct {
905- ID * int64 `json:"id,omitempty"`
906- Name string `json:"name,omitempty"`
907- DataType string `json:"data_type,omitempty"`
908- Value any `json:"value,omitempty"`
909- }
910-
911- type projectV2Item struct {
912- ArchivedAt * github.Timestamp `json:"archived_at,omitempty"`
913- Content * projectV2ItemContent `json:"content,omitempty"`
914- ContentType * string `json:"content_type,omitempty"`
915- CreatedAt * github.Timestamp `json:"created_at,omitempty"`
916- Creator * github.User `json:"creator,omitempty"`
917- Description * string `json:"description,omitempty"`
918- Fields []* projectV2ItemFieldValue `json:"fields,omitempty"`
919- ID * int64 `json:"id,omitempty"`
920- ItemURL * string `json:"item_url,omitempty"`
921- NodeID * string `json:"node_id,omitempty"`
922- ProjectURL * string `json:"project_url,omitempty"`
923- Title * string `json:"title,omitempty"`
924- UpdatedAt * github.Timestamp `json:"updated_at,omitempty"`
925- }
926-
927- type projectV2ItemContent struct {
928- Body * string `json:"body,omitempty"`
929- ClosedAt * github.Timestamp `json:"closed_at,omitempty"`
930- CreatedAt * github.Timestamp `json:"created_at,omitempty"`
931- ID * int64 `json:"id,omitempty"`
932- Number * int `json:"number,omitempty"`
933- State * string `json:"state,omitempty"`
934- StateReason * string `json:"stateReason,omitempty"`
935- Title * string `json:"title,omitempty"`
936- UpdatedAt * github.Timestamp `json:"updated_at,omitempty"`
937- URL * string `json:"url,omitempty"`
938- }
939-
940846type pageInfo struct {
941847HasNextPage bool `json:"hasNextPage"`
942848HasPreviousPage bool `json:"hasPreviousPage"`
943849NextCursor string `json:"nextCursor,omitempty"`
944850PrevCursor string `json:"prevCursor,omitempty"`
945851}
946852
947- type projectV2Field struct {
948- ID * int64 `json:"id,omitempty"`
949- NodeID * string `json:"node_id,omitempty"`
950- Name * string `json:"name,omitempty"`
951- DataType * string `json:"data_type,omitempty"`
952- ProjectURL * string `json:"project_url,omitempty"`
953- Options []any `json:"options,omitempty"`
954- Configuration any `json:"configuration,omitempty"`
955- CreatedAt * github.Timestamp `json:"created_at,omitempty"`
956- UpdatedAt * github.Timestamp `json:"updated_at,omitempty"`
957- }
958-
959853func toNewProjectType (projType string )string {
960854switch strings .ToLower (projType ) {
961855case "issue" :
@@ -967,7 +861,27 @@ func toNewProjectType(projType string) string {
967861}
968862}
969863
970- func buildUpdateProjectItem (input map [string ]any ) (* updateProjectItem ,error ) {
864+ // validateAndConvertToInt64 ensures the value is a number and converts it to int64.
865+ func validateAndConvertToInt64 (value any ) (int64 ,error ) {
866+ switch v := value .(type ) {
867+ case float64 :
868+ // Validate that the float64 can be safely converted to int64
869+ intVal := int64 (v )
870+ if float64 (intVal )!= v {
871+ return 0 ,fmt .Errorf ("value must be a valid integer (got %v)" ,v )
872+ }
873+ return intVal ,nil
874+ case int64 :
875+ return v ,nil
876+ case int :
877+ return int64 (v ),nil
878+ default :
879+ return 0 ,fmt .Errorf ("value must be a number (got %T)" ,v )
880+ }
881+ }
882+
883+ // buildUpdateProjectItem constructs UpdateProjectItemOptions from the input map.
884+ func buildUpdateProjectItem (input map [string ]any ) (* github.UpdateProjectItemOptions ,error ) {
971885if input == nil {
972886return nil ,fmt .Errorf ("updated_field must be an object" )
973887}
@@ -977,16 +891,22 @@ func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) {
977891return nil ,fmt .Errorf ("updated_field.id is required" )
978892}
979893
980- idFieldAsFloat64 , ok := idField .( float64 ) // JSON numbers are float64
981- if ! ok {
982- return nil ,fmt .Errorf ("updated_field.id must be a number" )
894+ fieldID , err := validateAndConvertToInt64 ( idField )
895+ if err != nil {
896+ return nil ,fmt .Errorf ("updated_field.id: %w" , err )
983897}
984898
985899valueField ,ok := input ["value" ]
986900if ! ok {
987901return nil ,fmt .Errorf ("updated_field.value is required" )
988902}
989- payload := & updateProjectItem {ID :int (idFieldAsFloat64 ),Value :valueField }
903+
904+ payload := & github.UpdateProjectItemOptions {
905+ Fields : []* github.UpdateProjectV2Field {{
906+ ID :fieldID ,
907+ Value :valueField ,
908+ }},
909+ }
990910
991911return payload ,nil
992912}
@@ -1034,35 +954,3 @@ func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsP
1034954
1035955return opts ,nil
1036956}
1037-
1038- // addOptions adds the parameters in opts as URL query parameters to s. opts
1039- // must be a struct whose fields may contain "url" tags.
1040- func addOptions (s string ,opts any ) (string ,error ) {
1041- v := reflect .ValueOf (opts )
1042- if v .Kind ()== reflect .Ptr && v .IsNil () {
1043- return s ,nil
1044- }
1045-
1046- origURL ,err := url .Parse (s )
1047- if err != nil {
1048- return s ,err
1049- }
1050-
1051- origValues := origURL .Query ()
1052-
1053- // Use the github.com/google/go-querystring library to parse the struct
1054- newValues ,err := query .Values (opts )
1055- if err != nil {
1056- return s ,err
1057- }
1058-
1059- // Merge the values
1060- for key ,values := range newValues {
1061- for _ ,value := range values {
1062- origValues .Add (key ,value )
1063- }
1064- }
1065-
1066- origURL .RawQuery = origValues .Encode ()
1067- return origURL .String (),nil
1068- }