Skip to content

Commit c3cd512

Browse files
committed
Add add_project_item tool
1 parent 0885601 commit c3cd512

File tree

5 files changed

+382
-0
lines changed

5 files changed

+382
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,13 @@ The following sets of tools are available (all are on by default):
658658

659659
<summary>Projects</summary>
660660

661+
- **add_project_item** - Add project item
662+
- `item_id`: The numeric ID of the issue or pull request to add to the project. (number, required)
663+
- `item_type`: The item's type, either issue or pull_request. (string, required)
664+
- `owner`: 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. (string, required)
665+
- `owner_type`: Owner type (string, required)
666+
- `project_number`: The project's number. (number, required)
667+
661668
- **get_project** - Get project
662669
- `owner`: 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. (string, required)
663670
- `owner_type`: Owner type (string, required)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"annotations": {
3+
"title": "Add project item",
4+
"readOnlyHint": false
5+
},
6+
"description": "Add a specific Project item for a user or org",
7+
"inputSchema": {
8+
"properties": {
9+
"item_id": {
10+
"description": "The numeric ID of the issue or pull request to add to the project.",
11+
"type": "number"
12+
},
13+
"item_type": {
14+
"description": "The item's type, either issue or pull_request.",
15+
"enum": [
16+
"issue",
17+
"pull_request"
18+
],
19+
"type": "string"
20+
},
21+
"owner": {
22+
"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.",
23+
"type": "string"
24+
},
25+
"owner_type": {
26+
"description": "Owner type",
27+
"enum": [
28+
"user",
29+
"org"
30+
],
31+
"type": "string"
32+
},
33+
"project_number": {
34+
"description": "The project's number.",
35+
"type": "number"
36+
}
37+
},
38+
"required": [
39+
"owner_type",
40+
"owner",
41+
"project_number",
42+
"item_type",
43+
"item_id"
44+
],
45+
"type": "object"
46+
},
47+
"name": "add_project_item"
48+
}

pkg/github/projects.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"net/url"
1010
"reflect"
11+
"strings"
1112

1213
ghErrors "github.com/github/github-mcp-server/pkg/errors"
1314
"github.com/github/github-mcp-server/pkg/translations"
@@ -474,6 +475,90 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
474475
}
475476
}
476477

478+
func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
479+
return mcp.NewTool("add_project_item",
480+
mcp.WithDescription(t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org")),
481+
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_ADD_PROJECT_ITEM_USER_TITLE", "Add project item"), ReadOnlyHint: ToBoolPtr(false)}),
482+
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
483+
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.")),
484+
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number.")),
485+
mcp.WithString("item_type", mcp.Required(), mcp.Description("The item's type, either issue or pull_request."), mcp.Enum("issue", "pull_request")),
486+
mcp.WithNumber("item_id", mcp.Required(), mcp.Description("The numeric ID of the issue or pull request to add to the project.")),
487+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
488+
owner, err := RequiredParam[string](req, "owner")
489+
if err != nil {
490+
return mcp.NewToolResultError(err.Error()), nil
491+
}
492+
ownerType, err := RequiredParam[string](req, "owner_type")
493+
if err != nil {
494+
return mcp.NewToolResultError(err.Error()), nil
495+
}
496+
projectNumber, err := RequiredInt(req, "project_number")
497+
if err != nil {
498+
return mcp.NewToolResultError(err.Error()), nil
499+
}
500+
itemID, err := RequiredInt(req, "item_id")
501+
if err != nil {
502+
return mcp.NewToolResultError(err.Error()), nil
503+
}
504+
505+
itemType, err := RequiredParam[string](req, "item_type")
506+
if err != nil {
507+
return mcp.NewToolResultError(err.Error()), nil
508+
}
509+
if itemType != "issue" && itemType != "pull_request" {
510+
return mcp.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil
511+
}
512+
513+
client, err := getClient(ctx)
514+
if err != nil {
515+
return mcp.NewToolResultError(err.Error()), nil
516+
}
517+
518+
var projectsURL string
519+
if ownerType == "org" {
520+
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber)
521+
} else {
522+
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
523+
}
524+
form := url.Values{}
525+
form.Add("type", toNewProjectType(itemType))
526+
form.Add("id", fmt.Sprintf("%d", itemID))
527+
528+
body := strings.NewReader(form.Encode())
529+
530+
httpRequest, err := client.NewFormRequest(projectsURL, body)
531+
if err != nil {
532+
return nil, fmt.Errorf("failed to create request: %w", err)
533+
}
534+
addedItem := projectV2Item{}
535+
536+
resp, err := client.Do(ctx, httpRequest, &addedItem)
537+
if err != nil {
538+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
539+
"failed to add a project item",
540+
resp,
541+
err,
542+
), nil
543+
}
544+
defer func() { _ = resp.Body.Close() }()
545+
546+
if resp.StatusCode != http.StatusOK {
547+
body, err := io.ReadAll(resp.Body)
548+
if err != nil {
549+
return nil, fmt.Errorf("failed to read response body: %w", err)
550+
}
551+
return mcp.NewToolResultError(fmt.Sprintf("failed to add a project item: %s", string(body))), nil
552+
}
553+
r, err := json.Marshal(convertToMinimalProjectItem(&addedItem))
554+
if err != nil {
555+
return nil, fmt.Errorf("failed to marshal response: %w", err)
556+
}
557+
558+
return mcp.NewToolResultText(string(r)), nil
559+
}
560+
}
561+
477562
type projectV2Field struct {
478563
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
479564
NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field.
@@ -500,6 +585,17 @@ type projectV2Item struct {
500585
Fields []*projectV2Field `json:"fields,omitempty"`
501586
}
502587

588+
func toNewProjectType(projType string) string {
589+
switch strings.ToLower(projType) {
590+
case "issue":
591+
return "Issue"
592+
case "pull_request":
593+
return "PullRequest"
594+
default:
595+
return ""
596+
}
597+
}
598+
503599
type listProjectsOptions struct {
504600
// For paginated result sets, the number of results to include per page.
505601
PerPage int `url:"per_page,omitempty"`

0 commit comments

Comments
 (0)