Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit af2e93b

Browse files
tmelliottjrTom Elliott
and
Tom Elliott
authored
projects: add item field support (#1282)
* add fields * generate docs * pr feedback --------- Co-authored-by: Tom Elliott <tmelliottjr@lcjr0246td.lan>
1 parent 3ba8d4a commit af2e93b

File tree

7 files changed

+231
-46
lines changed

7 files changed

+231
-46
lines changed

β€ŽREADME.mdβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,7 @@ Options are:
820820
- `project_number`: The project's number. (number, required)
821821

822822
- **get_project_item** - Get project item
823+
- `fields`: Specific list of field IDs to include in the response (e.g. ["102589", "985201", "169875"]). If not provided, only the title field is included. (string[], optional)
823824
- `item_id`: The item's ID. (number, required)
824825
- `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)
825826
- `owner_type`: Owner type (string, required)
@@ -832,6 +833,7 @@ Options are:
832833
- `project_number`: The project's number. (number, required)
833834

834835
- **list_project_items** - List project items
836+
- `fields`: Specific list of field IDs to include in the response (e.g. ["102589", "985201", "169875"]). If not provided, only the title field is included. (string[], optional)
835837
- `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)
836838
- `owner_type`: Owner type (string, required)
837839
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)

β€Žpkg/github/__toolsnaps__/get_project_item.snapβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
"description": "Get a specific Project item for a user or org",
77
"inputSchema": {
88
"properties": {
9+
"fields": {
10+
"description": "Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included.",
11+
"items": {
12+
"type": "string"
13+
},
14+
"type": "array"
15+
},
916
"item_id": {
1017
"description": "The item's ID.",
1118
"type": "number"

β€Žpkg/github/__toolsnaps__/list_project_items.snapβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
"description": "List Project items for a user or org",
77
"inputSchema": {
88
"properties": {
9+
"fields": {
10+
"description": "Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included.",
11+
"items": {
12+
"type": "string"
13+
},
14+
"type": "array"
15+
},
916
"owner": {
1017
"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.",
1118
"type": "string"

β€Žpkg/github/minimal_types.goβ€Ž

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,20 +132,20 @@ type MinimalProject struct {
132132
}
133133

134134
type MinimalProjectItem struct {
135-
ID *int64 `json:"id,omitempty"`
136-
NodeID *string `json:"node_id,omitempty"`
137-
Title *string `json:"title,omitempty"`
138-
Description *string `json:"description,omitempty"`
139-
ProjectNodeID *string `json:"project_node_id,omitempty"`
140-
ContentNodeID *string `json:"content_node_id,omitempty"`
141-
ProjectURL *string `json:"project_url,omitempty"`
142-
ContentType *string `json:"content_type,omitempty"`
143-
Creator *MinimalUser `json:"creator,omitempty"`
144-
CreatedAt *github.Timestamp `json:"created_at,omitempty"`
145-
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"`
146-
ArchivedAt *github.Timestamp `json:"archived_at,omitempty"`
147-
ItemURL *string `json:"item_url,omitempty"`
148-
Fields []*projectV2Field `json:"fields,omitempty"`
135+
ID *int64 `json:"id,omitempty"`
136+
NodeID *string `json:"node_id,omitempty"`
137+
Title *string `json:"title,omitempty"`
138+
Description *string `json:"description,omitempty"`
139+
ProjectNodeID *string `json:"project_node_id,omitempty"`
140+
ContentNodeID *string `json:"content_node_id,omitempty"`
141+
ProjectURL *string `json:"project_url,omitempty"`
142+
ContentType *string `json:"content_type,omitempty"`
143+
Creator *MinimalUser `json:"creator,omitempty"`
144+
CreatedAt *github.Timestamp `json:"created_at,omitempty"`
145+
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"`
146+
ArchivedAt *github.Timestamp `json:"archived_at,omitempty"`
147+
ItemURL *string `json:"item_url,omitempty"`
148+
Fields []*projectV2ItemFieldValue `json:"fields,omitempty"`
149149
}
150150

151151
// Helper functions

β€Žpkg/github/projects.goβ€Ž

Lines changed: 139 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (
7676
projects := []github.ProjectV2{}
7777
minimalProjects := []MinimalProject{}
7878

79-
opts := listProjectsOptions{PerPage: perPage}
80-
81-
if queryStr != "" {
82-
opts.Query = queryStr
79+
opts := listProjectsOptions{
80+
paginationOptions: paginationOptions{PerPage: perPage},
81+
filterQueryOptions: filterQueryOptions{Query: queryStr},
8382
}
83+
8484
url, err = addOptions(url, opts)
8585
if err != nil {
8686
return nil, fmt.Errorf("failed to add options to request: %w", err)
@@ -257,7 +257,8 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
257257
}
258258
projectFields := []projectV2Field{}
259259

260-
opts := listProjectsOptions{PerPage: perPage}
260+
opts := paginationOptions{PerPage: perPage}
261+
261262
url, err = addOptions(url, opts)
262263
if err != nil {
263264
return nil, fmt.Errorf("failed to add options to request: %w", err)
@@ -402,6 +403,10 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
402403
mcp.WithNumber("per_page",
403404
mcp.Description("Number of results per page (max 100, default: 30)"),
404405
),
406+
mcp.WithArray("fields",
407+
mcp.Description("Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included."),
408+
mcp.WithStringItems(),
409+
),
405410
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
406411
owner, err := RequiredParam[string](req, "owner")
407412
if err != nil {
@@ -423,6 +428,11 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
423428
if err != nil {
424429
return mcp.NewToolResultError(err.Error()), nil
425430
}
431+
fields, err := OptionalStringArrayParam(req, "fields")
432+
if err != nil {
433+
return mcp.NewToolResultError(err.Error()), nil
434+
}
435+
426436
client, err := getClient(ctx)
427437
if err != nil {
428438
return mcp.NewToolResultError(err.Error()), nil
@@ -436,10 +446,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
436446
}
437447
projectItems := []projectV2Item{}
438448

439-
opts := listProjectsOptions{PerPage: perPage}
440-
if queryStr != "" {
441-
opts.Query = queryStr
449+
opts := listProjectItemsOptions{
450+
paginationOptions: paginationOptions{PerPage: perPage},
451+
filterQueryOptions: filterQueryOptions{Query: queryStr},
452+
fieldSelectionOptions: fieldSelectionOptions{Fields: fields},
442453
}
454+
443455
url, err = addOptions(url, opts)
444456
if err != nil {
445457
return nil, fmt.Errorf("failed to add options to request: %w", err)
@@ -504,6 +516,10 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
504516
mcp.Required(),
505517
mcp.Description("The item's ID."),
506518
),
519+
mcp.WithArray("fields",
520+
mcp.Description("Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included."),
521+
mcp.WithStringItems(),
522+
),
507523
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
508524
owner, err := RequiredParam[string](req, "owner")
509525
if err != nil {
@@ -521,6 +537,10 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
521537
if err != nil {
522538
return mcp.NewToolResultError(err.Error()), nil
523539
}
540+
fields, err := OptionalStringArrayParam(req, "fields")
541+
if err != nil {
542+
return mcp.NewToolResultError(err.Error()), nil
543+
}
524544

525545
client, err := getClient(ctx)
526546
if err != nil {
@@ -533,6 +553,18 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
533553
} else {
534554
url = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
535555
}
556+
557+
opts := fieldSelectionOptions{}
558+
559+
if len(fields) > 0 {
560+
opts.Fields = fields
561+
}
562+
563+
url, err = addOptions(url, opts)
564+
if err != nil {
565+
return mcp.NewToolResultError(err.Error()), nil
566+
}
567+
536568
projectItem := projectV2Item{}
537569

538570
httpRequest, err := client.NewRequest("GET", url, nil)
@@ -877,21 +909,53 @@ type projectV2Field struct {
877909
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated.
878910
}
879911

912+
type projectV2ItemFieldValue struct {
913+
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
914+
Name string `json:"name,omitempty"` // The display name of the field.
915+
DataType string `json:"data_type,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select").
916+
Value interface{} `json:"value,omitempty"` // The value of the field for a specific project item.
917+
}
918+
880919
type projectV2Item struct {
881-
ID *int64 `json:"id,omitempty"`
882-
Title *string `json:"title,omitempty"`
883-
Description *string `json:"description,omitempty"`
884-
NodeID *string `json:"node_id,omitempty"`
885-
ProjectNodeID *string `json:"project_node_id,omitempty"`
886-
ContentNodeID *string `json:"content_node_id,omitempty"`
887-
ProjectURL *string `json:"project_url,omitempty"`
888-
ContentType *string `json:"content_type,omitempty"`
889-
Creator *github.User `json:"creator,omitempty"`
890-
CreatedAt *github.Timestamp `json:"created_at,omitempty"`
891-
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"`
892-
ArchivedAt *github.Timestamp `json:"archived_at,omitempty"`
893-
ItemURL *string `json:"item_url,omitempty"`
894-
Fields []*projectV2Field `json:"fields,omitempty"`
920+
ID *int64 `json:"id,omitempty"`
921+
Title *string `json:"title,omitempty"`
922+
Description *string `json:"description,omitempty"`
923+
NodeID *string `json:"node_id,omitempty"`
924+
ProjectNodeID *string `json:"project_node_id,omitempty"`
925+
ContentNodeID *string `json:"content_node_id,omitempty"`
926+
ProjectURL *string `json:"project_url,omitempty"`
927+
ContentType *string `json:"content_type,omitempty"`
928+
Creator *github.User `json:"creator,omitempty"`
929+
CreatedAt *github.Timestamp `json:"created_at,omitempty"`
930+
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"`
931+
ArchivedAt *github.Timestamp `json:"archived_at,omitempty"`
932+
ItemURL *string `json:"item_url,omitempty"`
933+
Fields []*projectV2ItemFieldValue `json:"fields,omitempty"`
934+
}
935+
936+
type paginationOptions struct {
937+
PerPage int `url:"per_page,omitempty"`
938+
}
939+
940+
type filterQueryOptions struct {
941+
Query string `url:"q,omitempty"`
942+
}
943+
944+
type fieldSelectionOptions struct {
945+
// Specific list of field IDs to include in the response. If not provided, only the title field is included.
946+
// Example: fields=102589,985201,169875 or fields[]=102589&fields[]=985201&fields[]=169875
947+
Fields []string `url:"fields,omitempty"`
948+
}
949+
950+
type listProjectsOptions struct {
951+
paginationOptions
952+
filterQueryOptions
953+
}
954+
955+
type listProjectItemsOptions struct {
956+
paginationOptions
957+
filterQueryOptions
958+
fieldSelectionOptions
895959
}
896960

897961
func toNewProjectType(projType string) string {
@@ -905,14 +969,6 @@ func toNewProjectType(projType string) string {
905969
}
906970
}
907971

908-
type listProjectsOptions struct {
909-
// For paginated result sets, the number of results to include per page.
910-
PerPage int `url:"per_page,omitempty"`
911-
912-
// Query Limit results to projects of the specified type.
913-
Query string `url:"q,omitempty"`
914-
}
915-
916972
func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) {
917973
if input == nil {
918974
return nil, fmt.Errorf("updated_field must be an object")
@@ -958,3 +1014,56 @@ func addOptions(s string, opts any) (string, error) {
9581014
u.RawQuery = qs.Encode()
9591015
return u.String(), nil
9601016
}
1017+
1018+
func ManageProjectItemsPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) {
1019+
return mcp.NewPrompt("ManageProjectItems",
1020+
mcp.WithPromptDescription(t("PROMPT_MANAGE_PROJECT_ITEMS_DESCRIPTION", "Guide for working with GitHub Projects, including listing projects, viewing fields, querying items, and updating field values.")),
1021+
mcp.WithArgument("owner", mcp.ArgumentDescription("The owner of the project (user or organization name)"), mcp.RequiredArgument()),
1022+
mcp.WithArgument("owner_type", mcp.ArgumentDescription("Type of owner: 'user' or 'org'"), mcp.RequiredArgument()),
1023+
), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
1024+
owner := request.Params.Arguments["owner"]
1025+
ownerType := request.Params.Arguments["owner_type"]
1026+
1027+
messages := []mcp.PromptMessage{
1028+
{
1029+
Role: "user",
1030+
Content: mcp.NewTextContent("You are an assistant helping users work with GitHub Projects V2. Your role is to help them discover projects, understand project fields, query items, and update field values on project items."),
1031+
},
1032+
{
1033+
Role: "user",
1034+
Content: mcp.NewTextContent(fmt.Sprintf("I want to work with projects owned by %s (owner_type: %s). Please help me understand what projects are available.", owner, ownerType)),
1035+
},
1036+
{
1037+
Role: "assistant",
1038+
Content: mcp.NewTextContent(fmt.Sprintf("I'll help you explore the projects for %s. Let me start by listing the available projects.", owner)),
1039+
},
1040+
{
1041+
Role: "user",
1042+
Content: mcp.NewTextContent("Great! Once you show me the projects, I'd like to understand the fields available in a specific project."),
1043+
},
1044+
{
1045+
Role: "assistant",
1046+
Content: mcp.NewTextContent("Perfect! After showing you the projects, I can help you:\n\n1. πŸ“‹ List all fields in a project (using `list_project_fields`)\n2. πŸ” Get details about specific fields including their IDs, data types, and options\n3. πŸ“Š Query project items with specific field values (using `list_project_items`)\n\nIMPORTANT: When querying project items, you must provide a list of field IDs in the 'fields' parameter to access field values. For example: fields=[\"198354254\", \"198354255\"] to get Status and Assignees. Without this parameter, only the title field is returned."),
1047+
},
1048+
{
1049+
Role: "user",
1050+
Content: mcp.NewTextContent("How do I update field values on project items?"),
1051+
},
1052+
{
1053+
Role: "assistant",
1054+
Content: mcp.NewTextContent("To update field values on project items, you'll use the `update_project_item` tool. Here's what you need to know:\n\n1. **Get the item_id**: Use `list_project_items` to find the internal project item ID (not the issue/PR number)\n2. **Get the field_id**: Use `list_project_fields` to find the ID of the field you want to update\n3. **Update the field**: Call `update_project_item` with:\n - project_number: The project's number\n - item_id: The internal project item ID\n - updated_field: An object with {\"id\": <field_id>, \"value\": <new_value>}\n\nFor single_select fields, the value should be the option name (e.g., \"In Progress\").\nFor text fields, provide a string value.\nFor number fields, provide a numeric value.\nTo clear a field, set \"value\" to null."),
1055+
},
1056+
{
1057+
Role: "user",
1058+
Content: mcp.NewTextContent("Can you give me an example workflow for finding items and updating their status?"),
1059+
},
1060+
{
1061+
Role: "assistant",
1062+
Content: mcp.NewTextContent(fmt.Sprintf("Absolutely! Here's a complete workflow:\n\n**Step 1: Find your project**\nUse `list_projects` with owner=\"%s\" and owner_type=\"%s\"\n\n**Step 2: Get the Status field ID**\nUse `list_project_fields` with the project_number from step 1\nLook for the field with name=\"Status\" and note its ID (e.g., 198354254)\n\n**Step 3: Query items with the Status field**\nUse `list_project_items` with fields=[\"198354254\"] to see current status values\nOptionally add a query parameter to filter items (e.g., query=\"assignee:@me\")\n\n**Step 4: Update an item's status**\nUse `update_project_item` with:\n- item_id: The ID from the item you want to update\n- updated_field: {\"id\": 198354254, \"value\": \"In Progress\"}\n\nLet me start by listing your projects now.", owner, ownerType)),
1063+
},
1064+
}
1065+
return &mcp.GetPromptResult{
1066+
Messages: messages,
1067+
}, nil
1068+
}
1069+
}

0 commit comments

Comments
(0)

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /