Skip to content

Commit 6d77cde

Browse files
committed
Add update project item tool
1 parent 6d01897 commit 6d77cde

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

pkg/github/minimal_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ type MinimalProject struct {
134134
type MinimalProjectItem struct {
135135
ID *int64 `json:"id,omitempty"`
136136
NodeID *string `json:"node_id,omitempty"`
137+
Title *string `json:"title,omitempty"`
138+
Description *string `json:"description,omitempty"`
137139
ProjectNodeID *string `json:"project_node_id,omitempty"`
138140
ContentNodeID *string `json:"content_node_id,omitempty"`
139141
ProjectURL *string `json:"project_url,omitempty"`
@@ -192,6 +194,8 @@ func convertToMinimalProjectItem(item *projectV2Item) *MinimalProjectItem {
192194
return &MinimalProjectItem{
193195
ID: item.ID,
194196
NodeID: item.NodeID,
197+
Title: item.Title,
198+
Description: item.Description,
195199
ProjectNodeID: item.ProjectNodeID,
196200
ContentNodeID: item.ContentNodeID,
197201
ProjectURL: item.ProjectURL,

pkg/github/projects.go

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
561648
func 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

624711
type 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+
629725
type 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

640736
type 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.
676801
func addOptions(s string, opts any) (string, error) {

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
202202
AddWriteTools(
203203
toolsets.NewServerTool(AddProjectItem(getClient, t)),
204204
toolsets.NewServerTool(DeleteProjectItem(getClient, t)),
205+
toolsets.NewServerTool(UpdateProjectItem(getClient, t)),
205206
)
206207

207208
// Add toolsets to the group

0 commit comments

Comments
 (0)