Skip to content

Commit b68bec0

Browse files
Update mcp server with latest google/go-github API (#1358)
* licences update from required CI build * fixes licences * implements new helpers to support google/go-github v77 API * upgrades toolset to leverage google/go-github v77 with the exception of Update and Delete items * refactor string conversion helpers * reverts migration google/go-github GetProjectItem due to bug with the underlying library implementation * additional refactoring for server helpers based on recent updates to google/go-github * test updates based on underlying lib requirements * resolves licences conflicts with script/licenses * cleanup * reduce change diff * updates helper docs to reflect to methods * upgrades delete projects item to google/go-github * returns error from parsing string to int64 * improved OptionalBigIntArrayParam doc * improves implementation for RequiredBigInt * improves documentation for temporary fieldSelectionOptions struct
1 parent cf0e05e commit b68bec0

File tree

3 files changed

+146
-119
lines changed

3 files changed

+146
-119
lines changed

pkg/github/projects.go

Lines changed: 67 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (
6969

7070
var resp *github.Response
7171
var projects []*github.ProjectV2
72-
minimalProjects := []MinimalProject{}
73-
7472
var queryPtr *string
73+
7574
if queryStr != "" {
7675
queryPtr = &queryStr
7776
}
7877

78+
minimalProjects := []MinimalProject{}
7979
opts := &github.ListProjectsOptions{
8080
ListProjectsPaginationOptions: github.ListProjectsPaginationOptions{PerPage: &perPage},
8181
Query: queryPtr,
@@ -237,27 +237,19 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
237237
return mcp.NewToolResultError(err.Error()), nil
238238
}
239239

240-
var url string
241-
if ownerType == "org" {
242-
url = fmt.Sprintf("orgs/%s/projectsV2/%d/fields", owner, projectNumber)
243-
} else {
244-
url = fmt.Sprintf("users/%s/projectsV2/%d/fields", owner, projectNumber)
245-
}
246-
projectFields := []projectV2Field{}
247-
248-
opts := paginationOptions{PerPage: perPage}
240+
var resp *github.Response
241+
var projectFields []*github.ProjectV2Field
249242

250-
url, err = addOptions(url, opts)
251-
if err != nil {
252-
return nil, fmt.Errorf("failed to add options to request: %w", err)
243+
opts := &github.ListProjectsOptions{
244+
ListProjectsPaginationOptions: github.ListProjectsPaginationOptions{PerPage: &perPage},
253245
}
254246

255-
httpRequest, err := client.NewRequest("GET", url, nil)
256-
if err != nil {
257-
return nil, fmt.Errorf("failed to create request: %w", err)
247+
if ownerType == "org" {
248+
projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts)
249+
} else {
250+
projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts)
258251
}
259252

260-
resp, err := client.Do(ctx, httpRequest, &projectFields)
261253
if err != nil {
262254
return ghErrors.NewGitHubAPIErrorResponse(ctx,
263255
"failed to list project fields",
@@ -317,7 +309,7 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc
317309
if err != nil {
318310
return mcp.NewToolResultError(err.Error()), nil
319311
}
320-
fieldID, err := RequiredInt(req, "field_id")
312+
fieldID, err := RequiredBigInt(req, "field_id")
321313
if err != nil {
322314
return mcp.NewToolResultError(err.Error()), nil
323315
}
@@ -326,21 +318,15 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc
326318
return mcp.NewToolResultError(err.Error()), nil
327319
}
328320

329-
var url string
321+
var resp *github.Response
322+
var projectField *github.ProjectV2Field
323+
330324
if ownerType == "org" {
331-
url = fmt.Sprintf("orgs/%s/projectsV2/%d/fields/%d", owner, projectNumber, fieldID)
325+
projectField, resp, err = client.Projects.GetOrganizationProjectField(ctx, owner, projectNumber, fieldID)
332326
} else {
333-
url = fmt.Sprintf("users/%s/projectsV2/%d/fields/%d", owner, projectNumber, fieldID)
327+
projectField, resp, err = client.Projects.GetUserProjectField(ctx, owner, projectNumber, fieldID)
334328
}
335329

336-
projectField := projectV2Field{}
337-
338-
httpRequest, err := client.NewRequest("GET", url, nil)
339-
if err != nil {
340-
return nil, fmt.Errorf("failed to create request: %w", err)
341-
}
342-
343-
resp, err := client.Do(ctx, httpRequest, &projectField)
344330
if err != nil {
345331
return ghErrors.NewGitHubAPIErrorResponse(ctx,
346332
"failed to get project field",
@@ -416,41 +402,37 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
416402
if err != nil {
417403
return mcp.NewToolResultError(err.Error()), nil
418404
}
419-
fields, err := OptionalStringArrayParam(req, "fields")
405+
fields, err := OptionalBigIntArrayParam(req, "fields")
420406
if err != nil {
421407
return mcp.NewToolResultError(err.Error()), nil
422408
}
423-
424409
client, err := getClient(ctx)
425410
if err != nil {
426411
return mcp.NewToolResultError(err.Error()), nil
427412
}
428413

429-
var url string
430-
if ownerType == "org" {
431-
url = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber)
432-
} else {
433-
url = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
434-
}
435-
projectItems := []projectV2Item{}
414+
var resp *github.Response
415+
var projectItems []*github.ProjectV2Item
416+
var queryPtr *string
436417

437-
opts := listProjectItemsOptions{
438-
paginationOptions: paginationOptions{PerPage: perPage},
439-
filterQueryOptions: filterQueryOptions{Query: queryStr},
440-
fieldSelectionOptions: fieldSelectionOptions{Fields: fields},
418+
if queryStr != "" {
419+
queryPtr = &queryStr
441420
}
442421

443-
url, err = addOptions(url, opts)
444-
if err != nil {
445-
return nil, fmt.Errorf("failed to add options to request: %w", err)
422+
opts := &github.ListProjectItemsOptions{
423+
Fields: fields,
424+
ListProjectsOptions: github.ListProjectsOptions{
425+
ListProjectsPaginationOptions: github.ListProjectsPaginationOptions{PerPage: &perPage},
426+
Query: queryPtr,
427+
},
446428
}
447429

448-
httpRequest, err := client.NewRequest("GET", url, nil)
449-
if err != nil {
450-
return nil, fmt.Errorf("failed to create request: %w", err)
430+
if ownerType == "org" {
431+
projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts)
432+
} else {
433+
projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts)
451434
}
452435

453-
resp, err := client.Do(ctx, httpRequest, &projectItems)
454436
if err != nil {
455437
return ghErrors.NewGitHubAPIErrorResponse(ctx,
456438
ProjectListFailedError,
@@ -518,11 +500,11 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
518500
if err != nil {
519501
return mcp.NewToolResultError(err.Error()), nil
520502
}
521-
itemID, err := RequiredInt(req, "item_id")
503+
itemID, err := RequiredBigInt(req, "item_id")
522504
if err != nil {
523505
return mcp.NewToolResultError(err.Error()), nil
524506
}
525-
fields, err := OptionalStringArrayParam(req, "fields")
507+
fields, err := OptionalBigIntArrayParam(req, "fields")
526508
if err != nil {
527509
return mcp.NewToolResultError(err.Error()), nil
528510
}
@@ -624,7 +606,7 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
624606
if err != nil {
625607
return mcp.NewToolResultError(err.Error()), nil
626608
}
627-
itemID, err := RequiredInt(req, "item_id")
609+
itemID, err := RequiredBigInt(req, "item_id")
628610
if err != nil {
629611
return mcp.NewToolResultError(err.Error()), nil
630612
}
@@ -642,24 +624,20 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
642624
return mcp.NewToolResultError(err.Error()), nil
643625
}
644626

645-
var projectsURL string
646-
if ownerType == "org" {
647-
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber)
648-
} else {
649-
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
650-
}
651-
652-
newItem := &newProjectItem{
653-
ID: int64(itemID),
627+
newItem := &github.AddProjectItemOptions{
628+
ID: itemID,
654629
Type: toNewProjectType(itemType),
655630
}
656-
httpRequest, err := client.NewRequest("POST", projectsURL, newItem)
657-
if err != nil {
658-
return nil, fmt.Errorf("failed to create request: %w", err)
631+
632+
var resp *github.Response
633+
var addedItem *github.ProjectV2Item
634+
635+
if ownerType == "org" {
636+
addedItem, resp, err = client.Projects.AddOrganizationProjectItem(ctx, owner, projectNumber, newItem)
637+
} else {
638+
addedItem, resp, err = client.Projects.AddUserProjectItem(ctx, owner, projectNumber, newItem)
659639
}
660-
addedItem := projectV2Item{}
661640

662-
resp, err := client.Do(ctx, httpRequest, &addedItem)
663641
if err != nil {
664642
return ghErrors.NewGitHubAPIErrorResponse(ctx,
665643
ProjectAddFailedError,
@@ -827,7 +805,7 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
827805
if err != nil {
828806
return mcp.NewToolResultError(err.Error()), nil
829807
}
830-
itemID, err := RequiredInt(req, "item_id")
808+
itemID, err := RequiredBigInt(req, "item_id")
831809
if err != nil {
832810
return mcp.NewToolResultError(err.Error()), nil
833811
}
@@ -836,19 +814,13 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
836814
return mcp.NewToolResultError(err.Error()), nil
837815
}
838816

839-
var projectsURL string
817+
var resp *github.Response
840818
if ownerType == "org" {
841-
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
819+
resp, err = client.Projects.DeleteOrganizationProjectItem(ctx, owner, projectNumber, itemID)
842820
} else {
843-
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
821+
resp, err = client.Projects.DeleteUserProjectItem(ctx, owner, projectNumber, itemID)
844822
}
845823

846-
httpRequest, err := client.NewRequest("DELETE", projectsURL, nil)
847-
if err != nil {
848-
return nil, fmt.Errorf("failed to create request: %w", err)
849-
}
850-
851-
resp, err := client.Do(ctx, httpRequest, nil)
852824
if err != nil {
853825
return ghErrors.NewGitHubAPIErrorResponse(ctx,
854826
ProjectDeleteFailedError,
@@ -869,9 +841,10 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
869841
}
870842
}
871843

872-
type newProjectItem struct {
873-
ID int64 `json:"id,omitempty"`
874-
Type string `json:"type,omitempty"`
844+
type fieldSelectionOptions struct {
845+
// Specific list of field IDs to include in the response. If not provided, only the title field is included.
846+
// The comma tag encodes the slice as comma-separated values: fields=102589,985201,169875
847+
Fields []int64 `url:"fields,omitempty,comma"`
875848
}
876849

877850
type updateProjectItemPayload struct {
@@ -883,17 +856,6 @@ type updateProjectItem struct {
883856
Value any `json:"value"`
884857
}
885858

886-
type projectV2Field struct {
887-
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
888-
NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field.
889-
Name string `json:"name,omitempty"` // The display name of the field.
890-
DataType string `json:"data_type,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select").
891-
URL string `json:"url,omitempty"` // The API URL for this field.
892-
Options []*any `json:"options,omitempty"` // Available options for single_select and multi_select fields.
893-
CreatedAt *github.Timestamp `json:"created_at,omitempty"` // The time when this field was created.
894-
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated.
895-
}
896-
897859
type projectV2ItemFieldValue struct {
898860
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
899861
Name string `json:"name,omitempty"` // The display name of the field.
@@ -931,26 +893,6 @@ type projectV2ItemContent struct {
931893
URL *string `json:"url,omitempty"`
932894
}
933895

934-
type paginationOptions struct {
935-
PerPage int `url:"per_page,omitempty"`
936-
}
937-
938-
type filterQueryOptions struct {
939-
Query string `url:"q,omitempty"`
940-
}
941-
942-
type fieldSelectionOptions struct {
943-
// Specific list of field IDs to include in the response. If not provided, only the title field is included.
944-
// Example: fields=102589,985201,169875 or fields[]=102589&fields[]=985201&fields[]=169875
945-
Fields []string `url:"fields,omitempty"`
946-
}
947-
948-
type listProjectItemsOptions struct {
949-
paginationOptions
950-
filterQueryOptions
951-
fieldSelectionOptions
952-
}
953-
954896
func toNewProjectType(projType string) string {
955897
switch strings.ToLower(projType) {
956898
case "issue":
@@ -994,18 +936,28 @@ func addOptions(s string, opts any) (string, error) {
994936
return s, nil
995937
}
996938

997-
u, err := url.Parse(s)
939+
origURL, err := url.Parse(s)
998940
if err != nil {
999941
return s, err
1000942
}
1001943

1002-
qs, err := query.Values(opts)
944+
origValues := origURL.Query()
945+
946+
// Use the github.com/google/go-querystring library to parse the struct
947+
newValues, err := query.Values(opts)
1003948
if err != nil {
1004949
return s, err
1005950
}
1006951

1007-
u.RawQuery = qs.Encode()
1008-
return u.String(), nil
952+
// Merge the values
953+
for key, values := range newValues {
954+
for _, value := range values {
955+
origValues.Add(key, value)
956+
}
957+
}
958+
959+
origURL.RawQuery = origValues.Encode()
960+
return origURL.String(), nil
1009961
}
1010962

1011963
func ManageProjectItemsPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) {

pkg/github/projects_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -653,8 +653,8 @@ func Test_ListProjectItems(t *testing.T) {
653653
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items", Method: http.MethodGet},
654654
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
655655
q := r.URL.Query()
656-
fieldParams := q["fields"]
657-
if len(fieldParams) == 3 && fieldParams[0] == "123" && fieldParams[1] == "456" && fieldParams[2] == "789" {
656+
fieldParams := q.Get("fields")
657+
if fieldParams == "123,456,789" {
658658
w.WriteHeader(http.StatusOK)
659659
_, _ = w.Write(mock.MustMarshal(orgItems))
660660
return
@@ -852,8 +852,8 @@ func Test_GetProjectItem(t *testing.T) {
852852
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodGet},
853853
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
854854
q := r.URL.Query()
855-
fieldParams := q["fields"]
856-
if len(fieldParams) == 2 && fieldParams[0] == "123" && fieldParams[1] == "456" {
855+
fieldParams := q.Get("fields")
856+
if fieldParams == "123,456" {
857857
w.WriteHeader(http.StatusOK)
858858
_, _ = w.Write(mock.MustMarshal(orgItem))
859859
return

0 commit comments

Comments
 (0)