@@ -23,6 +23,73 @@ const (
2323 MaxProjectsPerPage = 50
2424)
2525
26+ // FlexibleString handles JSON unmarshaling of fields that can be either
27+ // a plain string or an object with "raw" and "html" fields.
28+ // This is needed because the GitHub API returns option names as strings,
29+ // while go-github v79 expects them to be ProjectV2TextContent objects.
30+ type FlexibleString struct {
31+ Raw string `json:"raw,omitempty"`
32+ HTML string `json:"html,omitempty"`
33+ }
34+
35+ // UnmarshalJSON implements custom unmarshaling for FlexibleString
36+ func (f * FlexibleString ) UnmarshalJSON (data []byte ) error {
37+ // Try to unmarshal as a plain string first
38+ var s string
39+ if err := json .Unmarshal (data , & s ); err == nil {
40+ f .Raw = s
41+ f .HTML = s
42+ return nil
43+ }
44+
45+ // If that fails, try to unmarshal as an object
46+ type flexibleStringAlias FlexibleString
47+ var obj flexibleStringAlias
48+ if err := json .Unmarshal (data , & obj ); err != nil {
49+ return err
50+ }
51+ * f = FlexibleString (obj )
52+ return nil
53+ }
54+
55+ // ProjectFieldOption represents an option for single_select or iteration fields.
56+ // This is a custom type that handles the flexible name format from the GitHub API.
57+ type ProjectFieldOption struct {
58+ ID string `json:"id,omitempty"`
59+ Name * FlexibleString `json:"name,omitempty"`
60+ Color string `json:"color,omitempty"`
61+ Description * FlexibleString `json:"description,omitempty"`
62+ }
63+
64+ // ProjectFieldIteration represents an iteration within a project field.
65+ type ProjectFieldIteration struct {
66+ ID string `json:"id,omitempty"`
67+ Title * FlexibleString `json:"title,omitempty"`
68+ StartDate string `json:"start_date,omitempty"`
69+ Duration int `json:"duration,omitempty"`
70+ }
71+
72+ // ProjectFieldConfiguration represents the configuration for iteration fields.
73+ type ProjectFieldConfiguration struct {
74+ Duration int `json:"duration,omitempty"`
75+ StartDay int `json:"start_day,omitempty"`
76+ Iterations []* ProjectFieldIteration `json:"iterations,omitempty"`
77+ }
78+
79+ // ProjectField represents a field in a GitHub Project V2.
80+ // This is a custom type that properly handles the options array format from the GitHub API.
81+ type ProjectField struct {
82+ ID int64 `json:"id,omitempty"`
83+ NodeID string `json:"node_id,omitempty"`
84+ Name string `json:"name,omitempty"`
85+ DataType string `json:"data_type,omitempty"`
86+ ProjectURL string `json:"project_url,omitempty"`
87+ Options []* ProjectFieldOption `json:"options,omitempty"`
88+ Configuration * ProjectFieldConfiguration `json:"configuration,omitempty"`
89+ CreatedAt string `json:"created_at,omitempty"`
90+ UpdatedAt string `json:"updated_at,omitempty"`
91+ }
92+
2693func ListProjects (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
2794 return mcp .NewTool ("list_projects" ,
2895 mcp .WithDescription (t ("TOOL_LIST_PROJECTS_DESCRIPTION" , `List Projects for a user or organization` )),
@@ -253,19 +320,22 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
253320 return mcp .NewToolResultError (err .Error ()), nil
254321 }
255322
256- var resp * github.Response
257- var projectFields []* github.ProjectV2Field
323+ // Build the URL for the API request
324+ var urlPath string
325+ if ownerType == "org" {
326+ urlPath = fmt .Sprintf ("orgs/%s/projectsV2/%d/fields" , owner , projectNumber )
327+ } else {
328+ urlPath = fmt .Sprintf ("users/%s/projectsV2/%d/fields" , owner , projectNumber )
329+ }
258330
331+ // Create options for the request
259332 opts := & github.ListProjectsOptions {
260333 ListProjectsPaginationOptions : pagination ,
261334 }
262335
263- if ownerType == "org" {
264- projectFields , resp , err = client .Projects .ListOrganizationProjectFields (ctx , owner , projectNumber , opts )
265- } else {
266- projectFields , resp , err = client .Projects .ListUserProjectFields (ctx , owner , projectNumber , opts )
267- }
268-
336+ // Make the raw API request using go-github's client
337+ // We use our custom ProjectField type which handles flexible name format
338+ projectFields , resp , err := listProjectFieldsRaw (ctx , client , urlPath , opts )
269339 if err != nil {
270340 return ghErrors .NewGitHubAPIErrorResponse (ctx ,
271341 "failed to list project fields" ,
@@ -289,6 +359,70 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
289359 }
290360}
291361
362+ // listProjectFieldsRaw makes a raw API request to list project fields and parses
363+ // the response using our custom ProjectField type that handles flexible name formats.
364+ func listProjectFieldsRaw (ctx context.Context , client * github.Client , urlPath string , opts * github.ListProjectsOptions ) ([]* ProjectField , * github.Response , error ) {
365+ u , err := addProjectOptions (urlPath , opts )
366+ if err != nil {
367+ return nil , nil , err
368+ }
369+
370+ req , err := client .NewRequest ("GET" , u , nil )
371+ if err != nil {
372+ return nil , nil , err
373+ }
374+
375+ var fields []* ProjectField
376+ resp , err := client .Do (ctx , req , & fields )
377+ if err != nil {
378+ return nil , resp , err
379+ }
380+ return fields , resp , nil
381+ }
382+
383+ // addProjectOptions adds query parameters to a URL for project API requests.
384+ func addProjectOptions (s string , opts * github.ListProjectsOptions ) (string , error ) {
385+ if opts == nil {
386+ return s , nil
387+ }
388+
389+ // Build query parameters manually
390+ params := make ([]string , 0 )
391+ if opts .PerPage != nil && * opts .PerPage > 0 {
392+ params = append (params , fmt .Sprintf ("per_page=%d" , * opts .PerPage ))
393+ }
394+ if opts .After != nil && * opts .After != "" {
395+ params = append (params , fmt .Sprintf ("after=%s" , * opts .After ))
396+ }
397+ if opts .Before != nil && * opts .Before != "" {
398+ params = append (params , fmt .Sprintf ("before=%s" , * opts .Before ))
399+ }
400+ if opts .Query != nil && * opts .Query != "" {
401+ params = append (params , fmt .Sprintf ("q=%s" , * opts .Query ))
402+ }
403+
404+ if len (params ) > 0 {
405+ s = s + "?" + strings .Join (params , "&" )
406+ }
407+ return s , nil
408+ }
409+
410+ // getProjectFieldRaw makes a raw API request to get a single project field and parses
411+ // the response using our custom ProjectField type that handles flexible name formats.
412+ func getProjectFieldRaw (ctx context.Context , client * github.Client , urlPath string ) (* ProjectField , * github.Response , error ) {
413+ req , err := client .NewRequest ("GET" , urlPath , nil )
414+ if err != nil {
415+ return nil , nil , err
416+ }
417+
418+ var field ProjectField
419+ resp , err := client .Do (ctx , req , & field )
420+ if err != nil {
421+ return nil , resp , err
422+ }
423+ return & field , resp , nil
424+ }
425+
292426func GetProjectField (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
293427 return mcp .NewTool ("get_project_field" ,
294428 mcp .WithDescription (t ("TOOL_GET_PROJECT_FIELD_DESCRIPTION" , "Get Project field for a user or org" )),
@@ -332,15 +466,17 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc
332466 return mcp .NewToolResultError (err .Error ()), nil
333467 }
334468
335- var resp * github.Response
336- var projectField * github.ProjectV2Field
337-
469+ // Build the URL for the API request
470+ var urlPath string
338471 if ownerType == "org" {
339- projectField , resp , err = client . Projects . GetOrganizationProjectField ( ctx , owner , projectNumber , fieldID )
472+ urlPath = fmt . Sprintf ( "orgs/%s/projectsV2/%d/fields/%d" , owner , projectNumber , fieldID )
340473 } else {
341- projectField , resp , err = client . Projects . GetUserProjectField ( ctx , owner , projectNumber , fieldID )
474+ urlPath = fmt . Sprintf ( "users/%s/projectsV2/%d/fields/%d" , owner , projectNumber , fieldID )
342475 }
343476
477+ // Make the raw API request using go-github's client
478+ // We use our custom ProjectField type which handles flexible name format
479+ projectField , resp , err := getProjectFieldRaw (ctx , client , urlPath )
344480 if err != nil {
345481 return ghErrors .NewGitHubAPIErrorResponse (ctx ,
346482 "failed to get project field" ,
0 commit comments