@@ -558,6 +558,93 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
558558 }
559559}
560560
561+ func UpdateProjectItem (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
562+ return mcp .NewTool ("update_project_item" ,
563+ mcp .WithDescription (t ("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION" , "Update a specific Project item for a user or org" )),
564+ mcp .WithToolAnnotation (mcp.ToolAnnotation {Title : t ("TOOL_UPDATE_PROJECT_ITEM_USER_TITLE" , "Update project item" ), ReadOnlyHint : ToBoolPtr (false )}),
565+ mcp .WithString ("owner_type" , mcp .Required (), mcp .Description ("Owner type" ), mcp .Enum ("user" , "org" )),
566+ mcp .WithString ("owner" , mcp .Required (), mcp .Description ("If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive." )),
567+ mcp .WithNumber ("project_number" , mcp .Required (), mcp .Description ("The project's number." )),
568+ mcp .WithNumber ("item_id" , mcp .Required (), mcp .Description ("The numeric ID of the issue or pull request to add to the project." )),
569+ mcp .WithObject ("new_field" , mcp .Required (), mcp .Description ("Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set this to null. Example: {\" id\" : 123456, \" value\" : \" New Value\" }" )),
570+ ), func (ctx context.Context , req mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
571+ owner , err := RequiredParam [string ](req , "owner" )
572+ if err != nil {
573+ return mcp .NewToolResultError (err .Error ()), nil
574+ }
575+ ownerType , err := RequiredParam [string ](req , "owner_type" )
576+ if err != nil {
577+ return mcp .NewToolResultError (err .Error ()), nil
578+ }
579+ projectNumber , err := RequiredInt (req , "project_number" )
580+ if err != nil {
581+ return mcp .NewToolResultError (err .Error ()), nil
582+ }
583+ itemID , err := RequiredInt (req , "item_id" )
584+ if err != nil {
585+ return mcp .NewToolResultError (err .Error ()), nil
586+ }
587+
588+ rawNewField , exists := req .GetArguments ()["new_field" ]
589+ if ! exists {
590+ return mcp .NewToolResultError ("missing required parameter: new_field" ), nil
591+ }
592+
593+ newField , ok := rawNewField .(map [string ]any )
594+ if ! ok || newField == nil {
595+ return mcp .NewToolResultError ("new_field must be an object" ), nil
596+ }
597+
598+ updatePayload , err := buildUpdateProjectItem (newField )
599+ if err != nil {
600+ return mcp .NewToolResultError (err .Error ()), nil
601+ }
602+
603+ client , err := getClient (ctx )
604+ if err != nil {
605+ return mcp .NewToolResultError (err .Error ()), nil
606+ }
607+
608+ var projectsURL string
609+ if ownerType == "org" {
610+ projectsURL = fmt .Sprintf ("orgs/%s/projectsV2/%d/items/%d" , owner , projectNumber , itemID )
611+ } else {
612+ projectsURL = fmt .Sprintf ("users/%s/projectsV2/%d/items/%d" , owner , projectNumber , itemID )
613+ }
614+ httpRequest , err := client .NewRequest ("PATCH" , projectsURL , updateProjectItemPayload {
615+ Fields : []updateProjectItem {* updatePayload },
616+ })
617+ if err != nil {
618+ return nil , fmt .Errorf ("failed to create request: %w" , err )
619+ }
620+ addedItem := projectV2Item {}
621+
622+ resp , err := client .Do (ctx , httpRequest , & addedItem )
623+ if err != nil {
624+ return ghErrors .NewGitHubAPIErrorResponse (ctx ,
625+ "failed to add a project item" ,
626+ resp ,
627+ err ,
628+ ), nil
629+ }
630+ defer func () { _ = resp .Body .Close () }()
631+
632+ if resp .StatusCode != http .StatusCreated && resp .StatusCode != http .StatusOK {
633+ body , err := io .ReadAll (resp .Body )
634+ if err != nil {
635+ return nil , fmt .Errorf ("failed to read response body: %w" , err )
636+ }
637+ return mcp .NewToolResultError (fmt .Sprintf ("failed to add a project item: %s" , string (body ))), nil
638+ }
639+ r , err := json .Marshal (convertToMinimalProjectItem (& addedItem ))
640+ if err != nil {
641+ return nil , fmt .Errorf ("failed to marshal response: %w" , err )
642+ }
643+
644+ return mcp .NewToolResultText (string (r )), nil
645+ }
646+ }
647+
561648func DeleteProjectItem (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
562649 return mcp .NewTool ("delete_project_item" ,
563650 mcp .WithDescription (t ("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION" , "Delete a specific Project item for a user or org" )),
@@ -622,10 +709,19 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
622709}
623710
624711type newProjectItem struct {
625- ID int64 `json:"id,omitempty"` // Issue or Pull Request ID to add to the project.
712+ ID int64 `json:"id,omitempty"`
626713 Type string `json:"type,omitempty"`
627714}
628715
716+ type updateProjectItemPayload struct {
717+ Fields []updateProjectItem `json:"fields"`
718+ }
719+
720+ type updateProjectItem struct {
721+ ID int `json:"id"`
722+ Value any `json:"value"`
723+ }
724+
629725type projectV2Field struct {
630726 ID * int64 `json:"id,omitempty"` // The unique identifier for this field.
631727 NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field.
@@ -639,6 +735,8 @@ type projectV2Field struct {
639735
640736type projectV2Item struct {
641737 ID * int64 `json:"id,omitempty"`
738+ Title * string `json:"title,omitempty"`
739+ Description * string `json:"description,omitempty"`
642740 NodeID * string `json:"node_id,omitempty"`
643741 ProjectNodeID * string `json:"project_node_id,omitempty"`
644742 ContentNodeID * string `json:"content_node_id,omitempty"`
@@ -671,6 +769,33 @@ type listProjectsOptions struct {
671769 Query string `url:"q,omitempty"`
672770}
673771
772+ func buildUpdateProjectItem (input map [string ]any ) (* updateProjectItem , error ) {
773+ if input == nil {
774+ return nil , fmt .Errorf ("new_field must be an object" )
775+ }
776+
777+ fieldIDValue , ok := input ["id" ]
778+ if ! ok {
779+ fieldIDValue , ok = input ["value" ]
780+ if ! ok {
781+ return nil , fmt .Errorf ("new_field.id is required" )
782+ }
783+ }
784+
785+ fieldIDAsInt , ok := fieldIDValue .(float64 ) // JSON numbers are float64
786+ if ! ok {
787+ return nil , fmt .Errorf ("new_field.id must be a number" )
788+ }
789+
790+ value , ok := input ["value" ]
791+ if ! ok {
792+ return nil , fmt .Errorf ("new_field.value is required" )
793+ }
794+ payload := & updateProjectItem {ID : int (fieldIDAsInt ), Value : value }
795+
796+ return payload , nil
797+ }
798+
674799// addOptions adds the parameters in opts as URL query parameters to s. opts
675800// must be a struct whose fields may contain "url" tags.
676801func addOptions (s string , opts any ) (string , error ) {
0 commit comments