Skip to content

Commit 56d0dd4

Browse files
authored
Merge branch 'main' into main
2 parents 29f7978 + e95d8ee commit 56d0dd4

File tree

1 file changed

+55
-167
lines changed

1 file changed

+55
-167
lines changed

pkg/github/projects.go

Lines changed: 55 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9-
"net/url"
10-
"reflect"
119
"strings"
1210

1311
ghErrors "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
256253
return mcp.NewToolResultError(err.Error()), nil
257254
}
258255

256+
var resp *github.Response
257+
var projectFields []*github.ProjectV2Field
258+
259259
opts := &github.ListProjectsOptions{
260260
ListProjectsPaginationOptions: pagination,
261261
}
262262

263-
var url string
264263
if 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-
283269
if err != nil {
284270
return ghErrors.NewGitHubAPIErrorResponse(ctx,
285271
"failed to list project fields",
@@ -452,7 +438,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
452438
}
453439

454440
var resp *github.Response
455-
var projectItems []projectV2Item
441+
var projectItems []*github.ProjectV2Item
456442
var queryPtr *string
457443

458444
if queryStr != "" {
@@ -467,25 +453,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
467453
},
468454
}
469455

470-
var url string
471456
if 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-
489462
if err != nil {
490463
return ghErrors.NewGitHubAPIErrorResponse(ctx,
491464
ProjectListFailedError,
@@ -566,32 +539,22 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
566539
return 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

578546
if 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)
595558
if err != nil {
596559
return ghErrors.NewGitHubAPIErrorResponse(ctx,
597560
"failed to get project item",
@@ -748,7 +711,7 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
748711
if err != nil {
749712
return mcp.NewToolResultError(err.Error()), nil
750713
}
751-
itemID, err := RequiredInt(req, "item_id")
714+
itemID, err := RequiredBigInt(req, "item_id")
752715
if err != nil {
753716
return mcp.NewToolResultError(err.Error()), nil
754717
}
@@ -773,21 +736,15 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
773736
return mcp.NewToolResultError(err.Error()), nil
774737
}
775738

776-
var projectsURL string
739+
var resp *github.Response
740+
var updatedItem *github.ProjectV2Item
741+
777742
if 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)
791748
if err != nil {
792749
return ghErrors.NewGitHubAPIErrorResponse(ctx,
793750
ProjectUpdateFailedError,
@@ -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-
940846
type pageInfo struct {
941847
HasNextPage bool `json:"hasNextPage"`
942848
HasPreviousPage bool `json:"hasPreviousPage"`
943849
NextCursor string `json:"nextCursor,omitempty"`
944850
PrevCursor 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-
959853
func toNewProjectType(projType string) string {
960854
switch strings.ToLower(projType) {
961855
case "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) {
971885
if input == nil {
972886
return nil, fmt.Errorf("updated_field must be an object")
973887
}
@@ -977,16 +891,22 @@ func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) {
977891
return 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

985899
valueField, ok := input["value"]
986900
if !ok {
987901
return 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

991911
return payload, nil
992912
}
@@ -1034,35 +954,3 @@ func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsP
1034954

1035955
return 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-
}

0 commit comments

Comments
 (0)