diff --git a/README.md b/README.md index f0df8f143e..ea93b8adbd 100644 --- a/README.md +++ b/README.md @@ -820,6 +820,7 @@ Options are: - `project_number`: The project's number. (number, required) - **get_project_item** - Get project item + - `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) - `item_id`: The item's ID. (number, required) - `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) - `owner_type`: Owner type (string, required) @@ -832,6 +833,7 @@ Options are: - `project_number`: The project's number. (number, required) - **list_project_items** - List project items + - `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) - `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) - `owner_type`: Owner type (string, required) - `per_page`: Number of results per page (max 100, default: 30) (number, optional) diff --git a/docs/remote-server.md b/docs/remote-server.md index 3a4ec444a3..66c8be388e 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -19,7 +19,8 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to | Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) | |----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) | +| default | Default GitHub MCP toolset (see [default toolset](../README.md#default-toolset)) | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) | +| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/x/all | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github-all&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fall%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/all/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github-all&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fall%2Freadonly%22%7D) | | Actions | GitHub Actions workflows and CI/CD operations | https://api.githubcopilot.com/mcp/x/actions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/actions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%2Freadonly%22%7D) | | Code Security | Code security related tools, such as GitHub Code Scanning | https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D) | | Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) | @@ -76,13 +77,16 @@ Example: ### URL Path Parameters -The Remote GitHub MCP server also supports the URL path parameters: +The Remote GitHub MCP server supports the following URL path patterns: -- `/x/{toolset}` -- `/x/{toolset}/readonly` -- `/readonly` +- `/` - Default toolset (see ["default" toolset](../README.md#default-toolset)) +- `/readonly` - Default toolset in read-only mode +- `/x/all` - All available toolsets +- `/x/all/readonly` - All available toolsets in read-only mode +- `/x/{toolset}` - Single specific toolset +- `/x/{toolset}/readonly` - Single specific toolset in read-only mode -Note: `{toolset}` can only been a single toolset, not a comma-separated list. To combine multiple toolsets, use the `X-MCP-Toolsets` header instead. +Note: `{toolset}` can only be a single toolset, not a comma-separated list. To combine multiple toolsets, use the `X-MCP-Toolsets` header instead. Example: diff --git a/pkg/github/__toolsnaps__/get_project_item.snap b/pkg/github/__toolsnaps__/get_project_item.snap index 6f8f609354..36eb7bb633 100644 --- a/pkg/github/__toolsnaps__/get_project_item.snap +++ b/pkg/github/__toolsnaps__/get_project_item.snap @@ -6,6 +6,13 @@ "description": "Get a specific Project item for a user or org", "inputSchema": { "properties": { + "fields": { + "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.", + "items": { + "type": "string" + }, + "type": "array" + }, "item_id": { "description": "The item's ID.", "type": "number" diff --git a/pkg/github/__toolsnaps__/list_project_items.snap b/pkg/github/__toolsnaps__/list_project_items.snap index 09b3267f0d..ebc7d17df4 100644 --- a/pkg/github/__toolsnaps__/list_project_items.snap +++ b/pkg/github/__toolsnaps__/list_project_items.snap @@ -6,6 +6,13 @@ "description": "List Project items for a user or org", "inputSchema": { "properties": { + "fields": { + "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.", + "items": { + "type": "string" + }, + "type": "array" + }, "owner": { "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.", "type": "string" diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index 766f630bb6..0a02dbcf6d 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -132,20 +132,20 @@ type MinimalProject struct { } type MinimalProjectItem struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - ProjectNodeID *string `json:"project_node_id,omitempty"` - ContentNodeID *string `json:"content_node_id,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - ContentType *string `json:"content_type,omitempty"` - Creator *MinimalUser `json:"creator,omitempty"` - CreatedAt *github.Timestamp `json:"created_at,omitempty"` - UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` - ArchivedAt *github.Timestamp `json:"archived_at,omitempty"` - ItemURL *string `json:"item_url,omitempty"` - Fields []*projectV2Field `json:"fields,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + ProjectNodeID *string `json:"project_node_id,omitempty"` + ContentNodeID *string `json:"content_node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + ContentType *string `json:"content_type,omitempty"` + Creator *MinimalUser `json:"creator,omitempty"` + CreatedAt *github.Timestamp `json:"created_at,omitempty"` + UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` + ArchivedAt *github.Timestamp `json:"archived_at,omitempty"` + ItemURL *string `json:"item_url,omitempty"` + Fields []*projectV2ItemFieldValue `json:"fields,omitempty"` } // Helper functions diff --git a/pkg/github/projects.go b/pkg/github/projects.go index f7bc94677a..262288f836 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -76,11 +76,11 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( projects := []github.ProjectV2{} minimalProjects := []MinimalProject{} - opts := listProjectsOptions{PerPage: perPage} - - if queryStr != "" { - opts.Query = queryStr + opts := listProjectsOptions{ + paginationOptions: paginationOptions{PerPage: perPage}, + filterQueryOptions: filterQueryOptions{Query: queryStr}, } + url, err = addOptions(url, opts) if err != nil { return nil, fmt.Errorf("failed to add options to request: %w", err) @@ -257,7 +257,8 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu } projectFields := []projectV2Field{} - opts := listProjectsOptions{PerPage: perPage} + opts := paginationOptions{PerPage: perPage} + url, err = addOptions(url, opts) if err != nil { return nil, fmt.Errorf("failed to add options to request: %w", err) @@ -402,6 +403,10 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)"), ), + mcp.WithArray("fields", + 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."), + mcp.WithStringItems(), + ), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](req, "owner") if err != nil { @@ -423,6 +428,11 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } + fields, err := OptionalStringArrayParam(req, "fields") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + client, err := getClient(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil @@ -436,10 +446,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun } projectItems := []projectV2Item{} - opts := listProjectsOptions{PerPage: perPage} - if queryStr != "" { - opts.Query = queryStr + opts := listProjectItemsOptions{ + paginationOptions: paginationOptions{PerPage: perPage}, + filterQueryOptions: filterQueryOptions{Query: queryStr}, + fieldSelectionOptions: fieldSelectionOptions{Fields: fields}, } + url, err = addOptions(url, opts) if err != nil { return nil, fmt.Errorf("failed to add options to request: %w", err) @@ -504,6 +516,10 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) mcp.Required(), mcp.Description("The item's ID."), ), + mcp.WithArray("fields", + 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."), + mcp.WithStringItems(), + ), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](req, "owner") if err != nil { @@ -521,6 +537,10 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) if err != nil { return mcp.NewToolResultError(err.Error()), nil } + fields, err := OptionalStringArrayParam(req, "fields") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } client, err := getClient(ctx) if err != nil { @@ -533,6 +553,18 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) } else { url = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID) } + + opts := fieldSelectionOptions{} + + if len(fields) > 0 { + opts.Fields = fields + } + + url, err = addOptions(url, opts) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + projectItem := projectV2Item{} httpRequest, err := client.NewRequest("GET", url, nil) @@ -877,21 +909,53 @@ type projectV2Field struct { UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. } +type projectV2ItemFieldValue struct { + ID *int64 `json:"id,omitempty"` // The unique identifier for this field. + Name string `json:"name,omitempty"` // The display name of the field. + DataType string `json:"data_type,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). + Value interface{} `json:"value,omitempty"` // The value of the field for a specific project item. +} + type projectV2Item struct { - ID *int64 `json:"id,omitempty"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectNodeID *string `json:"project_node_id,omitempty"` - ContentNodeID *string `json:"content_node_id,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - ContentType *string `json:"content_type,omitempty"` - Creator *github.User `json:"creator,omitempty"` - CreatedAt *github.Timestamp `json:"created_at,omitempty"` - UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` - ArchivedAt *github.Timestamp `json:"archived_at,omitempty"` - ItemURL *string `json:"item_url,omitempty"` - Fields []*projectV2Field `json:"fields,omitempty"` + ID *int64 `json:"id,omitempty"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectNodeID *string `json:"project_node_id,omitempty"` + ContentNodeID *string `json:"content_node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + ContentType *string `json:"content_type,omitempty"` + Creator *github.User `json:"creator,omitempty"` + CreatedAt *github.Timestamp `json:"created_at,omitempty"` + UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` + ArchivedAt *github.Timestamp `json:"archived_at,omitempty"` + ItemURL *string `json:"item_url,omitempty"` + Fields []*projectV2ItemFieldValue `json:"fields,omitempty"` +} + +type paginationOptions struct { + PerPage int `url:"per_page,omitempty"` +} + +type filterQueryOptions struct { + Query string `url:"q,omitempty"` +} + +type fieldSelectionOptions struct { + // Specific list of field IDs to include in the response. If not provided, only the title field is included. + // Example: fields=102589,985201,169875 or fields[]=102589&fields[]=985201&fields[]=169875 + Fields []string `url:"fields,omitempty"` +} + +type listProjectsOptions struct { + paginationOptions + filterQueryOptions +} + +type listProjectItemsOptions struct { + paginationOptions + filterQueryOptions + fieldSelectionOptions } func toNewProjectType(projType string) string { @@ -905,14 +969,6 @@ func toNewProjectType(projType string) string { } } -type listProjectsOptions struct { - // For paginated result sets, the number of results to include per page. - PerPage int `url:"per_page,omitempty"` - - // Query Limit results to projects of the specified type. - Query string `url:"q,omitempty"` -} - func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) { if input == nil { return nil, fmt.Errorf("updated_field must be an object") @@ -958,3 +1014,56 @@ func addOptions(s string, opts any) (string, error) { u.RawQuery = qs.Encode() return u.String(), nil } + +func ManageProjectItemsPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) { + return mcp.NewPrompt("ManageProjectItems", + 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.")), + mcp.WithArgument("owner", mcp.ArgumentDescription("The owner of the project (user or organization name)"), mcp.RequiredArgument()), + mcp.WithArgument("owner_type", mcp.ArgumentDescription("Type of owner: 'user' or 'org'"), mcp.RequiredArgument()), + ), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + owner := request.Params.Arguments["owner"] + ownerType := request.Params.Arguments["owner_type"] + + messages := []mcp.PromptMessage{ + { + Role: "user", + 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."), + }, + { + Role: "user", + 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)), + }, + { + Role: "assistant", + Content: mcp.NewTextContent(fmt.Sprintf("I'll help you explore the projects for %s. Let me start by listing the available projects.", owner)), + }, + { + Role: "user", + Content: mcp.NewTextContent("Great! Once you show me the projects, I'd like to understand the fields available in a specific project."), + }, + { + Role: "assistant", + 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."), + }, + { + Role: "user", + Content: mcp.NewTextContent("How do I update field values on project items?"), + }, + { + Role: "assistant", + 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\": , \"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."), + }, + { + Role: "user", + Content: mcp.NewTextContent("Can you give me an example workflow for finding items and updating their status?"), + }, + { + Role: "assistant", + 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)), + }, + } + return &mcp.GetPromptResult{ + Messages: messages, + }, nil + } +} diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index 52adb73e65..a55749cc1b 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -609,10 +609,14 @@ func Test_ListProjectItems(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "project_number") assert.Contains(t, tool.InputSchema.Properties, "query") assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "fields") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number"}) orgItems := []map[string]any{ - {"id": 301, "content_type": "Issue", "project_node_id": "PR_1"}, + {"id": 301, "content_type": "Issue", "project_node_id": "PR_1", "fields": []map[string]any{ + {"id": 123, "name": "Status", "data_type": "single_select", "value": "value1"}, + {"id": 456, "name": "Priority", "data_type": "single_select", "value": "value2"}, + }}, } userItems := []map[string]any{ {"id": 401, "content_type": "PullRequest", "project_node_id": "PR_2"}, @@ -642,6 +646,32 @@ func Test_ListProjectItems(t *testing.T) { }, expectedLength: 1, }, + { + name: "success organization items with fields", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items", Method: http.MethodGet}, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + fieldParams := q["fields"] + if len(fieldParams) == 3 && fieldParams[0] == "123" && fieldParams[1] == "456" && fieldParams[2] == "789" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(mock.MustMarshal(orgItems)) + return + } + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"message":"unexpected query params"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "octo-org", + "owner_type": "org", + "project_number": float64(123), + "fields": []interface{}{"123", "456", "789"}, + }, + expectedLength: 1, + }, { name: "success user items", mockedClient: mock.NewMockedHTTPClient( @@ -775,6 +805,7 @@ func Test_GetProjectItem(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "owner") assert.Contains(t, tool.InputSchema.Properties, "project_number") assert.Contains(t, tool.InputSchema.Properties, "item_id") + assert.Contains(t, tool.InputSchema.Properties, "fields") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) orgItem := map[string]any{ @@ -814,6 +845,33 @@ func Test_GetProjectItem(t *testing.T) { }, expectedID: 301, }, + { + name: "success organization item with fields", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodGet}, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + fieldParams := q["fields"] + if len(fieldParams) == 2 && fieldParams[0] == "123" && fieldParams[1] == "456" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(mock.MustMarshal(orgItem)) + return + } + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"message":"unexpected query params"}`)) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "octo-org", + "owner_type": "org", + "project_number": float64(123), + "item_id": float64(301), + "fields": []interface{}{"123", "456"}, + }, + expectedID: 301, + }, { name: "success user item", mockedClient: mock.NewMockedHTTPClient( diff --git a/pkg/github/tools.go b/pkg/github/tools.go index b82f347f8b..659286e02f 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -327,7 +327,9 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(AddProjectItem(getClient, t)), toolsets.NewServerTool(DeleteProjectItem(getClient, t)), toolsets.NewServerTool(UpdateProjectItem(getClient, t)), - ) + ).AddPrompts( + toolsets.NewServerPrompt(ManageProjectItemsPrompt(t)), + ) stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). AddReadTools( toolsets.NewServerTool(ListStarredRepositories(getClient, t)),