From bd7ca66505f00f83d3e7704e85d8085464bcefbc Mon Sep 17 00:00:00 2001
From: Jonathan Otalora <stephenotalora@github.com>
Date: Fri, 14 Nov 2025 10:24:05 -0700
Subject: [PATCH 1/3] updates mcp-server with latest google-go-github APIs

---
 pkg/github/projects.go | 106 ++++++++++++-----------------------------
 1 file changed, 31 insertions(+), 75 deletions(-)

diff --git a/pkg/github/projects.go b/pkg/github/projects.go
index 9ecd18fe0a..1e40be70fd 100644
--- a/pkg/github/projects.go
+++ b/pkg/github/projects.go
@@ -256,30 +256,19 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
 				return mcp.NewToolResultError(err.Error()), nil
 			}
 
+			var resp *github.Response
+			var projectFields []*github.ProjectV2Field
+
 			opts := &github.ListProjectsOptions{
 				ListProjectsPaginationOptions: pagination,
 			}
 
-			var url string
 			if ownerType == "org" {
-				url = fmt.Sprintf("orgs/%s/projectsV2/%d/fields", owner, projectNumber)
+				projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts)
 			} else {
-				url = fmt.Sprintf("users/%s/projectsV2/%d/fields", owner, projectNumber)
-			}
-
-			url, err = addOptions(url, opts)
-			if err != nil {
-				return mcp.NewToolResultError(err.Error()), nil
+				projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts)
 			}
 
-			httpRequest, err := client.NewRequest("GET", url, nil)
-			if err != nil {
-				return nil, fmt.Errorf("failed to create request: %w", err)
-			}
-
-			var projectFields []projectV2Field
-			resp, err := client.Do(ctx, httpRequest, &projectFields)
-
 			if err != nil {
 				return ghErrors.NewGitHubAPIErrorResponse(ctx,
 					"failed to list project fields",
@@ -452,7 +441,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
 			}
 
 			var resp *github.Response
-			var projectItems []projectV2Item
+			var projectItems []*github.ProjectV2Item
 			var queryPtr *string
 
 			if queryStr != "" {
@@ -467,25 +456,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
 				},
 			}
 
-			var url string
 			if ownerType == "org" {
-				url = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber)
+				projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts)
 			} else {
-				url = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
+				projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts)
 			}
 
-			url, err = addOptions(url, opts)
-			if err != nil {
-				return mcp.NewToolResultError(err.Error()), nil
-			}
-
-			httpRequest, err := client.NewRequest("GET", url, nil)
-			if err != nil {
-				return nil, fmt.Errorf("failed to create request: %w", err)
-			}
-
-			resp, err = client.Do(ctx, httpRequest, &projectItems)
-
 			if err != nil {
 				return ghErrors.NewGitHubAPIErrorResponse(ctx,
 					ProjectListFailedError,
@@ -566,32 +542,22 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
 				return mcp.NewToolResultError(err.Error()), nil
 			}
 
-			var url string
-			if ownerType == "org" {
-				url = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
-			} else {
-				url = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
-			}
-
-			opts := fieldSelectionOptions{}
+			resp := &github.Response{}
+			projectItem := &github.ProjectV2Item{}
+			var opts *github.GetProjectItemOptions
 
 			if len(fields) > 0 {
-				opts.Fields = fields
-			}
-
-			url, err = addOptions(url, opts)
-			if err != nil {
-				return mcp.NewToolResultError(err.Error()), nil
+				opts = &github.GetProjectItemOptions{
+					Fields: fields,
+				}
 			}
 
-			projectItem := projectV2Item{}
-
-			httpRequest, err := client.NewRequest("GET", url, nil)
-			if err != nil {
-				return nil, fmt.Errorf("failed to create request: %w", err)
+			if ownerType == "org" {
+				projectItem, resp, err = client.Projects.GetOrganizationProjectItem(ctx, owner, projectNumber, itemID, opts)
+			} else {
+				projectItem, resp, err = client.Projects.GetUserProjectItem(ctx, owner, projectNumber, itemID, opts)
 			}
 
-			resp, err := client.Do(ctx, httpRequest, &projectItem)
 			if err != nil {
 				return ghErrors.NewGitHubAPIErrorResponse(ctx,
 					"failed to get project item",
@@ -748,7 +714,7 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
 			if err != nil {
 				return mcp.NewToolResultError(err.Error()), nil
 			}
-			itemID, err := RequiredInt(req, "item_id")
+			itemID, err := RequiredBigInt(req, "item_id")
 			if err != nil {
 				return mcp.NewToolResultError(err.Error()), nil
 			}
@@ -773,21 +739,15 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
 				return mcp.NewToolResultError(err.Error()), nil
 			}
 
-			var projectsURL string
+			var resp *github.Response
+			var updatedItem *github.ProjectV2Item
+
 			if ownerType == "org" {
-				projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
+				updatedItem, resp, err = client.Projects.UpdateOrganizationProjectItem(ctx, owner, projectNumber, itemID, updatePayload)
 			} else {
-				projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
-			}
-			httpRequest, err := client.NewRequest("PATCH", projectsURL, updateProjectItemPayload{
-				Fields: []updateProjectItem{*updatePayload},
-			})
-			if err != nil {
-				return nil, fmt.Errorf("failed to create request: %w", err)
+				updatedItem, resp, err = client.Projects.UpdateUserProjectItem(ctx, owner, projectNumber, itemID, updatePayload)
 			}
-			updatedItem := projectV2Item{}
 
-			resp, err := client.Do(ctx, httpRequest, &updatedItem)
 			if err != nil {
 				return ghErrors.NewGitHubAPIErrorResponse(ctx,
 					ProjectUpdateFailedError,
@@ -892,15 +852,6 @@ type fieldSelectionOptions struct {
 	Fields []int64 `url:"fields,omitempty,comma"`
 }
 
-type updateProjectItemPayload struct {
-	Fields []updateProjectItem `json:"fields"`
-}
-
-type updateProjectItem struct {
-	ID    int `json:"id"`
-	Value any `json:"value"`
-}
-
 type projectV2ItemFieldValue struct {
 	ID       *int64 `json:"id,omitempty"`
 	Name     string `json:"name,omitempty"`
@@ -967,7 +918,7 @@ func toNewProjectType(projType string) string {
 	}
 }
 
-func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) {
+func buildUpdateProjectItem(input map[string]any) (*github.UpdateProjectItemOptions, error) {
 	if input == nil {
 		return nil, fmt.Errorf("updated_field must be an object")
 	}
@@ -986,7 +937,12 @@ func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) {
 	if !ok {
 		return nil, fmt.Errorf("updated_field.value is required")
 	}
-	payload := &updateProjectItem{ID: int(idFieldAsFloat64), Value: valueField}
+	payload := &github.UpdateProjectItemOptions{
+		Fields: []*github.UpdateProjectV2Field{{
+			ID:    int64(idFieldAsFloat64),
+			Value: valueField,
+		}},
+	}
 
 	return payload, nil
 }

From 1d6499d8755d2f10aa992d90cd2775f8d61bf5c4 Mon Sep 17 00:00:00 2001
From: Jonathan Otalora <stephenotalora@github.com>
Date: Fri, 14 Nov 2025 10:37:04 -0700
Subject: [PATCH 2/3] removes unused code

---
 pkg/github/projects.go | 93 +-----------------------------------------
 1 file changed, 2 insertions(+), 91 deletions(-)

diff --git a/pkg/github/projects.go b/pkg/github/projects.go
index 1e40be70fd..e43c514c08 100644
--- a/pkg/github/projects.go
+++ b/pkg/github/projects.go
@@ -6,14 +6,11 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"net/url"
-	"reflect"
 	"strings"
 
 	ghErrors "github.com/github/github-mcp-server/pkg/errors"
 	"github.com/github/github-mcp-server/pkg/translations"
 	"github.com/google/go-github/v79/github"
-	"github.com/google/go-querystring/query"
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 )
@@ -542,8 +539,8 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
 				return mcp.NewToolResultError(err.Error()), nil
 			}
 
-			resp := &github.Response{}
-			projectItem := &github.ProjectV2Item{}
+			var resp *github.Response
+			var projectItem *github.ProjectV2Item
 			var opts *github.GetProjectItemOptions
 
 			if len(fields) > 0 {
@@ -846,48 +843,6 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
 		}
 }
 
-type fieldSelectionOptions struct {
-	// Specific list of field IDs to include in the response. If not provided, only the title field is included.
-	// The comma tag encodes the slice as comma-separated values: fields=102589,985201,169875
-	Fields []int64 `url:"fields,omitempty,comma"`
-}
-
-type projectV2ItemFieldValue struct {
-	ID       *int64 `json:"id,omitempty"`
-	Name     string `json:"name,omitempty"`
-	DataType string `json:"data_type,omitempty"`
-	Value    any    `json:"value,omitempty"`
-}
-
-type projectV2Item struct {
-	ArchivedAt  *github.Timestamp          `json:"archived_at,omitempty"`
-	Content     *projectV2ItemContent      `json:"content,omitempty"`
-	ContentType *string                    `json:"content_type,omitempty"`
-	CreatedAt   *github.Timestamp          `json:"created_at,omitempty"`
-	Creator     *github.User               `json:"creator,omitempty"`
-	Description *string                    `json:"description,omitempty"`
-	Fields      []*projectV2ItemFieldValue `json:"fields,omitempty"`
-	ID          *int64                     `json:"id,omitempty"`
-	ItemURL     *string                    `json:"item_url,omitempty"`
-	NodeID      *string                    `json:"node_id,omitempty"`
-	ProjectURL  *string                    `json:"project_url,omitempty"`
-	Title       *string                    `json:"title,omitempty"`
-	UpdatedAt   *github.Timestamp          `json:"updated_at,omitempty"`
-}
-
-type projectV2ItemContent struct {
-	Body        *string           `json:"body,omitempty"`
-	ClosedAt    *github.Timestamp `json:"closed_at,omitempty"`
-	CreatedAt   *github.Timestamp `json:"created_at,omitempty"`
-	ID          *int64            `json:"id,omitempty"`
-	Number      *int              `json:"number,omitempty"`
-	State       *string           `json:"state,omitempty"`
-	StateReason *string           `json:"stateReason,omitempty"`
-	Title       *string           `json:"title,omitempty"`
-	UpdatedAt   *github.Timestamp `json:"updated_at,omitempty"`
-	URL         *string           `json:"url,omitempty"`
-}
-
 type pageInfo struct {
 	HasNextPage     bool   `json:"hasNextPage"`
 	HasPreviousPage bool   `json:"hasPreviousPage"`
@@ -895,18 +850,6 @@ type pageInfo struct {
 	PrevCursor      string `json:"prevCursor,omitempty"`
 }
 
-type projectV2Field struct {
-	ID            *int64            `json:"id,omitempty"`
-	NodeID        *string           `json:"node_id,omitempty"`
-	Name          *string           `json:"name,omitempty"`
-	DataType      *string           `json:"data_type,omitempty"`
-	ProjectURL    *string           `json:"project_url,omitempty"`
-	Options       []any             `json:"options,omitempty"`
-	Configuration any               `json:"configuration,omitempty"`
-	CreatedAt     *github.Timestamp `json:"created_at,omitempty"`
-	UpdatedAt     *github.Timestamp `json:"updated_at,omitempty"`
-}
-
 func toNewProjectType(projType string) string {
 	switch strings.ToLower(projType) {
 	case "issue":
@@ -990,35 +933,3 @@ func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsP
 
 	return opts, nil
 }
-
-// addOptions adds the parameters in opts as URL query parameters to s. opts
-// must be a struct whose fields may contain "url" tags.
-func addOptions(s string, opts any) (string, error) {
-	v := reflect.ValueOf(opts)
-	if v.Kind() == reflect.Ptr && v.IsNil() {
-		return s, nil
-	}
-
-	origURL, err := url.Parse(s)
-	if err != nil {
-		return s, err
-	}
-
-	origValues := origURL.Query()
-
-	// Use the github.com/google/go-querystring library to parse the struct
-	newValues, err := query.Values(opts)
-	if err != nil {
-		return s, err
-	}
-
-	// Merge the values
-	for key, values := range newValues {
-		for _, value := range values {
-			origValues.Add(key, value)
-		}
-	}
-
-	origURL.RawQuery = origValues.Encode()
-	return origURL.String(), nil
-}

From e95d8ee6fe241e797eda82da13b3d7809bd9187a Mon Sep 17 00:00:00 2001
From: Jonathan Otalora <stephenotalora@github.com>
Date: Fri, 14 Nov 2025 14:00:28 -0700
Subject: [PATCH 3/3] addresses float64 to in64 feedback from copilot

---
 pkg/github/projects.go | 29 +++++++++++++++++++++++++----
 1 file changed, 25 insertions(+), 4 deletions(-)

diff --git a/pkg/github/projects.go b/pkg/github/projects.go
index e43c514c08..4a2a68bf28 100644
--- a/pkg/github/projects.go
+++ b/pkg/github/projects.go
@@ -861,6 +861,26 @@ func toNewProjectType(projType string) string {
 	}
 }
 
+// validateAndConvertToInt64 ensures the value is a number and converts it to int64.
+func validateAndConvertToInt64(value any) (int64, error) {
+	switch v := value.(type) {
+	case float64:
+		// Validate that the float64 can be safely converted to int64
+		intVal := int64(v)
+		if float64(intVal) != v {
+			return 0, fmt.Errorf("value must be a valid integer (got %v)", v)
+		}
+		return intVal, nil
+	case int64:
+		return v, nil
+	case int:
+		return int64(v), nil
+	default:
+		return 0, fmt.Errorf("value must be a number (got %T)", v)
+	}
+}
+
+// buildUpdateProjectItem constructs UpdateProjectItemOptions from the input map.
 func buildUpdateProjectItem(input map[string]any) (*github.UpdateProjectItemOptions, error) {
 	if input == nil {
 		return nil, fmt.Errorf("updated_field must be an object")
@@ -871,18 +891,19 @@ func buildUpdateProjectItem(input map[string]any) (*github.UpdateProjectItemOpti
 		return nil, fmt.Errorf("updated_field.id is required")
 	}
 
-	idFieldAsFloat64, ok := idField.(float64) // JSON numbers are float64
-	if !ok {
-		return nil, fmt.Errorf("updated_field.id must be a number")
+	fieldID, err := validateAndConvertToInt64(idField)
+	if err != nil {
+		return nil, fmt.Errorf("updated_field.id: %w", err)
 	}
 
 	valueField, ok := input["value"]
 	if !ok {
 		return nil, fmt.Errorf("updated_field.value is required")
 	}
+
 	payload := &github.UpdateProjectItemOptions{
 		Fields: []*github.UpdateProjectV2Field{{
-			ID:    int64(idFieldAsFloat64),
+			ID:    fieldID,
 			Value: valueField,
 		}},
 	}
