Add tools to add, update and delete project items (#1152)
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Docker / build (push) Has been cancelled
GoReleaser Release / release (push) Has been cancelled
License Check / license-check (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Docker / build (push) Has been cancelled
GoReleaser Release / release (push) Has been cancelled
License Check / license-check (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
* Add add_project_item tool * Add tools to update and delete project items * Update pkg/github/projects.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update pkg/github/projects.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add tests * Lint the code * Fix req params --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -658,6 +658,19 @@ The following sets of tools are available (all are on by default):
|
||||
|
||||
<summary>Projects</summary>
|
||||
|
||||
- **add_project_item** - Add project item
|
||||
- `item_id`: The numeric ID of the issue or pull request to add to the project. (number, required)
|
||||
- `item_type`: The item's type, either issue or pull_request. (string, 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)
|
||||
- `project_number`: The project's number. (number, required)
|
||||
|
||||
- **delete_project_item** - Delete project item
|
||||
- `item_id`: The internal project item ID to delete from the project (not the issue or pull request 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)
|
||||
- `project_number`: The project's number. (number, required)
|
||||
|
||||
- **get_project** - Get project
|
||||
- `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)
|
||||
@@ -694,6 +707,13 @@ The following sets of tools are available (all are on by default):
|
||||
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
|
||||
- `query`: Filter projects by a search query (matches title and description) (string, optional)
|
||||
|
||||
- **update_project_item** - Update project item
|
||||
- `fields`: A list of field updates to apply. (array, required)
|
||||
- `item_id`: The numeric ID of the project item to update (not the issue or pull request 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)
|
||||
- `project_number`: The project's number. (number, required)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
@@ -118,8 +118,8 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
|
||||
|
||||
// Generate instructions based on enabled toolsets
|
||||
instructions := github.GenerateInstructions(enabledToolsets)
|
||||
|
||||
ghServer := github.NewServer(cfg.Version,
|
||||
|
||||
ghServer := github.NewServer(cfg.Version,
|
||||
server.WithInstructions(instructions),
|
||||
server.WithHooks(hooks),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"annotations": {
|
||||
"title": "Add project item",
|
||||
"readOnlyHint": false
|
||||
},
|
||||
"description": "Add a specific Project item for a user or org",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"description": "The numeric ID of the issue or pull request to add to the project.",
|
||||
"type": "number"
|
||||
},
|
||||
"item_type": {
|
||||
"description": "The item's type, either issue or pull_request.",
|
||||
"enum": [
|
||||
"issue",
|
||||
"pull_request"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"owner_type": {
|
||||
"description": "Owner type",
|
||||
"enum": [
|
||||
"user",
|
||||
"org"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"project_number": {
|
||||
"description": "The project's number.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"owner_type",
|
||||
"owner",
|
||||
"project_number",
|
||||
"item_type",
|
||||
"item_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "add_project_item"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"annotations": {
|
||||
"title": "Delete project item",
|
||||
"readOnlyHint": false
|
||||
},
|
||||
"description": "Delete a specific Project item for a user or org",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"description": "The internal project item ID to delete from the project (not the issue or pull request ID).",
|
||||
"type": "number"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"owner_type": {
|
||||
"description": "Owner type",
|
||||
"enum": [
|
||||
"user",
|
||||
"org"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"project_number": {
|
||||
"description": "The project's number.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"owner_type",
|
||||
"owner",
|
||||
"project_number",
|
||||
"item_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "delete_project_item"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"annotations": {
|
||||
"title": "Update project item",
|
||||
"readOnlyHint": false
|
||||
},
|
||||
"description": "Update a specific Project item for a user or org",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"fields": {
|
||||
"description": "A list of field updates to apply.",
|
||||
"type": "array"
|
||||
},
|
||||
"item_id": {
|
||||
"description": "The numeric ID of the project item to update (not the issue or pull request ID).",
|
||||
"type": "number"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"owner_type": {
|
||||
"description": "Owner type",
|
||||
"enum": [
|
||||
"user",
|
||||
"org"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"project_number": {
|
||||
"description": "The project's number.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"owner_type",
|
||||
"owner",
|
||||
"project_number",
|
||||
"item_id",
|
||||
"fields"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "update_project_item"
|
||||
}
|
||||
@@ -12,21 +12,21 @@ func GenerateInstructions(enabledToolsets []string) string {
|
||||
if os.Getenv("DISABLE_INSTRUCTIONS") == "true" {
|
||||
return "" // Baseline mode
|
||||
}
|
||||
|
||||
|
||||
var instructions []string
|
||||
|
||||
|
||||
// Core instruction - always included if context toolset enabled
|
||||
if slices.Contains(enabledToolsets, "context") {
|
||||
instructions = append(instructions, "Always call 'get_me' first to understand current user permissions and context.")
|
||||
}
|
||||
|
||||
|
||||
// Individual toolset instructions
|
||||
for _, toolset := range enabledToolsets {
|
||||
if inst := getToolsetInstructions(toolset); inst != "" {
|
||||
instructions = append(instructions, inst)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Base instruction with context management
|
||||
baseInstruction := `The GitHub MCP Server provides tools to interact with GitHub platform.
|
||||
|
||||
@@ -40,7 +40,7 @@ Context management:
|
||||
|
||||
allInstructions := []string{baseInstruction}
|
||||
allInstructions = append(allInstructions, instructions...)
|
||||
|
||||
|
||||
return strings.Join(allInstructions, " ")
|
||||
}
|
||||
|
||||
@@ -57,4 +57,3 @@ func getToolsetInstructions(toolset string) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,4 +163,4 @@ func TestGetToolsetInstructions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
ghErrors "github.com/github/github-mcp-server/pkg/errors"
|
||||
"github.com/github/github-mcp-server/pkg/translations"
|
||||
@@ -474,6 +475,289 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
|
||||
}
|
||||
}
|
||||
|
||||
func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
||||
return mcp.NewTool("add_project_item",
|
||||
mcp.WithDescription(t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org")),
|
||||
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_ADD_PROJECT_ITEM_USER_TITLE", "Add project item"), ReadOnlyHint: ToBoolPtr(false)}),
|
||||
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
|
||||
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.")),
|
||||
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number.")),
|
||||
mcp.WithString("item_type", mcp.Required(), mcp.Description("The item's type, either issue or pull_request."), mcp.Enum("issue", "pull_request")),
|
||||
mcp.WithNumber("item_id", mcp.Required(), mcp.Description("The numeric ID of the issue or pull request to add to the project.")),
|
||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
owner, err := RequiredParam[string](req, "owner")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
ownerType, err := RequiredParam[string](req, "owner_type")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
projectNumber, err := RequiredInt(req, "project_number")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
itemID, err := RequiredInt(req, "item_id")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
itemType, err := RequiredParam[string](req, "item_type")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
if itemType != "issue" && itemType != "pull_request" {
|
||||
return mcp.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil
|
||||
}
|
||||
|
||||
client, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
var projectsURL string
|
||||
if ownerType == "org" {
|
||||
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber)
|
||||
} else {
|
||||
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
|
||||
}
|
||||
|
||||
newProjectItem := &newProjectItem{
|
||||
ID: int64(itemID),
|
||||
Type: toNewProjectType(itemType),
|
||||
}
|
||||
httpRequest, err := client.NewRequest("POST", projectsURL, newProjectItem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
addedItem := projectV2Item{}
|
||||
|
||||
resp, err := client.Do(ctx, httpRequest, &addedItem)
|
||||
if err != nil {
|
||||
return ghErrors.NewGitHubAPIErrorResponse(ctx,
|
||||
"failed to add a project item",
|
||||
resp,
|
||||
err,
|
||||
), nil
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to add a project item: %s", string(body))), nil
|
||||
}
|
||||
r, err := json.Marshal(convertToMinimalProjectItem(&addedItem))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(r)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
||||
return mcp.NewTool("delete_project_item",
|
||||
mcp.WithDescription(t("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION", "Delete a specific Project item for a user or org")),
|
||||
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_DELETE_PROJECT_ITEM_USER_TITLE", "Delete project item"), ReadOnlyHint: ToBoolPtr(false)}),
|
||||
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
|
||||
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.")),
|
||||
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number.")),
|
||||
mcp.WithNumber("item_id", mcp.Required(), mcp.Description("The internal project item ID to delete from the project (not the issue or pull request ID).")),
|
||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
owner, err := RequiredParam[string](req, "owner")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
ownerType, err := RequiredParam[string](req, "owner_type")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
projectNumber, err := RequiredInt(req, "project_number")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
itemID, err := RequiredInt(req, "item_id")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
client, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
var projectsURL string
|
||||
if ownerType == "org" {
|
||||
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
|
||||
} else {
|
||||
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
|
||||
}
|
||||
|
||||
httpRequest, err := client.NewRequest("DELETE", projectsURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(ctx, httpRequest, nil)
|
||||
if err != nil {
|
||||
return ghErrors.NewGitHubAPIErrorResponse(ctx,
|
||||
"failed to delete a project item",
|
||||
resp,
|
||||
err,
|
||||
), nil
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to delete a project item: %s", string(body))), nil
|
||||
}
|
||||
return mcp.NewToolResultText("project item successfully deleted"), nil
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
||||
return mcp.NewTool("update_project_item",
|
||||
mcp.WithDescription(t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org")),
|
||||
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_UPDATE_PROJECT_ITEM_USER_TITLE", "Update project item"), ReadOnlyHint: ToBoolPtr(false)}),
|
||||
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
|
||||
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.")),
|
||||
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number.")),
|
||||
mcp.WithNumber("item_id", mcp.Required(), mcp.Description("The numeric ID of the project item to update (not the issue or pull request ID).")),
|
||||
mcp.WithArray("fields", mcp.Required(), mcp.Description("A list of field updates to apply.")),
|
||||
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
owner, err := RequiredParam[string](req, "owner")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
ownerType, err := RequiredParam[string](req, "owner_type")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
projectNumber, err := RequiredInt(req, "project_number")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
itemID, err := RequiredInt(req, "item_id")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
client, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
fieldsParam, ok := req.GetArguments()["fields"]
|
||||
if !ok {
|
||||
return mcp.NewToolResultError("missing required parameter: fields"), nil
|
||||
}
|
||||
|
||||
rawFields, ok := fieldsParam.([]any)
|
||||
if !ok {
|
||||
return mcp.NewToolResultError("parameter fields must be an array of objects"), nil
|
||||
}
|
||||
if len(rawFields) == 0 {
|
||||
return mcp.NewToolResultError("fields must contain at least one field update"), nil
|
||||
}
|
||||
|
||||
var projectsURL string
|
||||
if ownerType == "org" {
|
||||
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
|
||||
} else {
|
||||
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
|
||||
}
|
||||
|
||||
updateFields := make([]*newProjectV2Field, 0, len(rawFields))
|
||||
for idx, rawField := range rawFields {
|
||||
fieldMap, ok := rawField.(map[string]any)
|
||||
if !ok {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("fields[%d] must be an object", idx)), nil
|
||||
}
|
||||
|
||||
rawID, ok := fieldMap["id"]
|
||||
if !ok {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("fields[%d] is missing 'id'", idx)), nil
|
||||
}
|
||||
|
||||
var fieldID int64
|
||||
switch v := rawID.(type) {
|
||||
case float64:
|
||||
fieldID = int64(v)
|
||||
case int64:
|
||||
fieldID = v
|
||||
case json.Number:
|
||||
n, convErr := v.Int64()
|
||||
if convErr != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("fields[%d].id must be a numeric value", idx)), nil
|
||||
}
|
||||
fieldID = n
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("fields[%d].id must be a numeric value", idx)), nil
|
||||
}
|
||||
|
||||
value, ok := fieldMap["value"]
|
||||
if !ok {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("fields[%d] is missing 'value'", idx)), nil
|
||||
}
|
||||
|
||||
updateFields = append(updateFields, &newProjectV2Field{
|
||||
ID: github.Ptr(fieldID),
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
updateProjectItemOptions := &updateProjectItemOptions{Fields: updateFields}
|
||||
|
||||
httpRequest, err := client.NewRequest("PATCH", projectsURL, updateProjectItemOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
updatedItem := projectV2Item{}
|
||||
resp, err := client.Do(ctx, httpRequest, &updatedItem)
|
||||
if err != nil {
|
||||
return ghErrors.NewGitHubAPIErrorResponse(ctx,
|
||||
"failed to update a project item",
|
||||
resp,
|
||||
err,
|
||||
), nil
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to update a project item: %s", string(body))), nil
|
||||
}
|
||||
r, err := json.Marshal(convertToMinimalProjectItem(&updatedItem))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(r)), nil
|
||||
}
|
||||
}
|
||||
|
||||
type updateProjectItemOptions struct {
|
||||
Fields []*newProjectV2Field `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type newProjectV2Field struct {
|
||||
ID *int64 `json:"id,omitempty"`
|
||||
Value any `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type newProjectItem struct {
|
||||
ID int64 `json:"id,omitempty"` // Issue or Pull Request ID to add to the project.
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type projectV2Field struct {
|
||||
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
|
||||
NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field.
|
||||
@@ -500,6 +784,17 @@ type projectV2Item struct {
|
||||
Fields []*projectV2Field `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
func toNewProjectType(projType string) string {
|
||||
switch strings.ToLower(projType) {
|
||||
case "issue":
|
||||
return "Issue"
|
||||
case "pull_request":
|
||||
return "PullRequest"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type listProjectsOptions struct {
|
||||
// For paginated result sets, the number of results to include per page.
|
||||
PerPage int `url:"per_page,omitempty"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package github
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -928,3 +929,620 @@ func Test_GetProjectItem(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_AddProjectItem(t *testing.T) {
|
||||
mockClient := gh.NewClient(nil)
|
||||
tool, _ := AddProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
||||
require.NoError(t, toolsnaps.Test(tool.Name, tool))
|
||||
|
||||
assert.Equal(t, "add_project_item", tool.Name)
|
||||
assert.NotEmpty(t, tool.Description)
|
||||
assert.Contains(t, tool.InputSchema.Properties, "owner_type")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "project_number")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "item_type")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "item_id")
|
||||
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_type", "item_id"})
|
||||
|
||||
orgItem := map[string]any{
|
||||
"id": 601,
|
||||
"content_type": "Issue",
|
||||
"creator": map[string]any{
|
||||
"login": "octocat",
|
||||
"id": 1,
|
||||
"html_url": "https://github.com/octocat",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
|
||||
},
|
||||
}
|
||||
|
||||
userItem := map[string]any{
|
||||
"id": 701,
|
||||
"content_type": "PullRequest",
|
||||
"creator": map[string]any{
|
||||
"login": "hubot",
|
||||
"id": 2,
|
||||
"html_url": "https://github.com/hubot",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2?v=4",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockedClient *http.Client
|
||||
requestArgs map[string]any
|
||||
expectError bool
|
||||
expectedErrMsg string
|
||||
expectedID int
|
||||
expectedContentType string
|
||||
expectedCreatorLogin string
|
||||
}{
|
||||
{
|
||||
name: "success organization issue",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items", Method: http.MethodPost},
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
var payload struct {
|
||||
Type string `json:"type"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
assert.NoError(t, json.Unmarshal(body, &payload))
|
||||
assert.Equal(t, "Issue", payload.Type)
|
||||
assert.Equal(t, 9876, payload.ID)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write(mock.MustMarshal(orgItem))
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(321),
|
||||
"item_type": "issue",
|
||||
"item_id": float64(9876),
|
||||
},
|
||||
expectedID: 601,
|
||||
expectedContentType: "Issue",
|
||||
expectedCreatorLogin: "octocat",
|
||||
},
|
||||
{
|
||||
name: "success user pull request",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/users/{user}/projectsV2/{project}/items", Method: http.MethodPost},
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
var payload struct {
|
||||
Type string `json:"type"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
assert.NoError(t, json.Unmarshal(body, &payload))
|
||||
assert.Equal(t, "PullRequest", payload.Type)
|
||||
assert.Equal(t, 7654, payload.ID)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(mock.MustMarshal(userItem))
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octocat",
|
||||
"owner_type": "user",
|
||||
"project_number": float64(222),
|
||||
"item_type": "pull_request",
|
||||
"item_id": float64(7654),
|
||||
},
|
||||
expectedID: 701,
|
||||
expectedContentType: "PullRequest",
|
||||
expectedCreatorLogin: "hubot",
|
||||
},
|
||||
{
|
||||
name: "api error",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items", Method: http.MethodPost},
|
||||
mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(999),
|
||||
"item_type": "issue",
|
||||
"item_id": float64(8888),
|
||||
},
|
||||
expectError: true,
|
||||
expectedErrMsg: "failed to add a project item",
|
||||
},
|
||||
{
|
||||
name: "missing owner",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_type": "Issue",
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing owner_type",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"project_number": float64(1),
|
||||
"item_type": "Issue",
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing project_number",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"item_type": "Issue",
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing item_type",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing item_id",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_type": "Issue",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := gh.NewClient(tc.mockedClient)
|
||||
_, handler := AddProjectItem(stubGetClientFn(client), translations.NullTranslationHelper)
|
||||
request := createMCPRequest(tc.requestArgs)
|
||||
|
||||
result, err := handler(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.expectError {
|
||||
require.True(t, result.IsError)
|
||||
text := getTextResult(t, result).Text
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Contains(t, text, tc.expectedErrMsg)
|
||||
}
|
||||
switch tc.name {
|
||||
case "missing owner":
|
||||
assert.Contains(t, text, "missing required parameter: owner")
|
||||
case "missing owner_type":
|
||||
assert.Contains(t, text, "missing required parameter: owner_type")
|
||||
case "missing project_number":
|
||||
assert.Contains(t, text, "missing required parameter: project_number")
|
||||
case "missing item_type":
|
||||
assert.Contains(t, text, "missing required parameter: item_type")
|
||||
case "missing item_id":
|
||||
assert.Contains(t, text, "missing required parameter: item_id")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.False(t, result.IsError)
|
||||
textContent := getTextResult(t, result)
|
||||
var item map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &item))
|
||||
if tc.expectedID != 0 {
|
||||
assert.Equal(t, float64(tc.expectedID), item["id"])
|
||||
}
|
||||
if tc.expectedContentType != "" {
|
||||
assert.Equal(t, tc.expectedContentType, item["content_type"])
|
||||
}
|
||||
if tc.expectedCreatorLogin != "" {
|
||||
creator, ok := item["creator"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, tc.expectedCreatorLogin, creator["login"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DeleteProjectItem(t *testing.T) {
|
||||
mockClient := gh.NewClient(nil)
|
||||
tool, _ := DeleteProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
||||
require.NoError(t, toolsnaps.Test(tool.Name, tool))
|
||||
|
||||
assert.Equal(t, "delete_project_item", tool.Name)
|
||||
assert.NotEmpty(t, tool.Description)
|
||||
assert.Contains(t, tool.InputSchema.Properties, "owner_type")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "project_number")
|
||||
assert.Contains(t, tool.InputSchema.Properties, "item_id")
|
||||
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id"})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockedClient *http.Client
|
||||
requestArgs map[string]any
|
||||
expectError bool
|
||||
expectedErrMsg string
|
||||
expectedText string
|
||||
}{
|
||||
{
|
||||
name: "success organization delete",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodDelete},
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(123),
|
||||
"item_id": float64(555),
|
||||
},
|
||||
expectedText: "project item successfully deleted",
|
||||
},
|
||||
{
|
||||
name: "success user delete",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/users/{user}/projectsV2/{project}/items/{item_id}", Method: http.MethodDelete},
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octocat",
|
||||
"owner_type": "user",
|
||||
"project_number": float64(456),
|
||||
"item_id": float64(777),
|
||||
},
|
||||
expectedText: "project item successfully deleted",
|
||||
},
|
||||
{
|
||||
name: "api error",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodDelete},
|
||||
mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(321),
|
||||
"item_id": float64(999),
|
||||
},
|
||||
expectError: true,
|
||||
expectedErrMsg: "failed to delete a project item",
|
||||
},
|
||||
{
|
||||
name: "missing owner",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing owner_type",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing project_number",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"item_id": float64(10),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing item_id",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := gh.NewClient(tc.mockedClient)
|
||||
_, handler := DeleteProjectItem(stubGetClientFn(client), translations.NullTranslationHelper)
|
||||
request := createMCPRequest(tc.requestArgs)
|
||||
result, err := handler(context.Background(), request)
|
||||
|
||||
require.NoError(t, err)
|
||||
if tc.expectError {
|
||||
require.True(t, result.IsError)
|
||||
text := getTextResult(t, result).Text
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Contains(t, text, tc.expectedErrMsg)
|
||||
}
|
||||
switch tc.name {
|
||||
case "missing owner":
|
||||
assert.Contains(t, text, "missing required parameter: owner")
|
||||
case "missing owner_type":
|
||||
assert.Contains(t, text, "missing required parameter: owner_type")
|
||||
case "missing project_number":
|
||||
assert.Contains(t, text, "missing required parameter: project_number")
|
||||
case "missing item_id":
|
||||
assert.Contains(t, text, "missing required parameter: item_id")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.False(t, result.IsError)
|
||||
text := getTextResult(t, result).Text
|
||||
assert.Contains(t, text, tc.expectedText)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UpdateProjectItem(t *testing.T) {
|
||||
mockClient := gh.NewClient(nil)
|
||||
tool, _ := UpdateProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
||||
require.NoError(t, toolsnaps.Test(tool.Name, tool))
|
||||
|
||||
assert.Equal(t, "update_project_item", tool.Name)
|
||||
assert.NotEmpty(t, tool.Description)
|
||||
assert.Contains(t, tool.InputSchema.Properties, "owner_type")
|
||||
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", "fields"})
|
||||
|
||||
orgUpdated := map[string]any{
|
||||
"id": 801,
|
||||
"content_type": "Issue",
|
||||
"creator": map[string]any{"login": "octocat"},
|
||||
}
|
||||
userUpdated := map[string]any{
|
||||
"id": 901,
|
||||
"content_type": "PullRequest",
|
||||
"creator": map[string]any{"login": "hubot"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockedClient *http.Client
|
||||
requestArgs map[string]any
|
||||
expectError bool
|
||||
expectedErrMsg string
|
||||
expectedID int
|
||||
expectedCreatorLogin string
|
||||
}{
|
||||
{
|
||||
name: "success organization update",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodPatch},
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
var payload struct {
|
||||
Fields []struct {
|
||||
ID int `json:"id"`
|
||||
Value interface{} `json:"value"`
|
||||
} `json:"fields"`
|
||||
}
|
||||
assert.NoError(t, json.Unmarshal(body, &payload))
|
||||
assert.Len(t, payload.Fields, 1)
|
||||
if len(payload.Fields) == 1 {
|
||||
assert.Equal(t, 123, payload.Fields[0].ID)
|
||||
assert.Equal(t, "In Progress", payload.Fields[0].Value)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(mock.MustMarshal(orgUpdated))
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(111),
|
||||
"item_id": float64(2222),
|
||||
"fields": []any{
|
||||
map[string]any{"id": float64(123), "value": "In Progress"},
|
||||
},
|
||||
},
|
||||
expectedID: 801,
|
||||
expectedCreatorLogin: "octocat",
|
||||
},
|
||||
{
|
||||
name: "success user update",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/users/{user}/projectsV2/{project}/items/{item_id}", Method: http.MethodPatch},
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
var payload map[string]any
|
||||
assert.NoError(t, json.Unmarshal(body, &payload))
|
||||
fields, ok := payload["fields"].([]any)
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, fields, 1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(mock.MustMarshal(userUpdated))
|
||||
}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octocat",
|
||||
"owner_type": "user",
|
||||
"project_number": float64(222),
|
||||
"item_id": float64(3333),
|
||||
"fields": []any{
|
||||
map[string]any{"id": float64(456), "value": 42},
|
||||
},
|
||||
},
|
||||
expectedID: 901,
|
||||
expectedCreatorLogin: "hubot",
|
||||
},
|
||||
{
|
||||
name: "api error",
|
||||
mockedClient: mock.NewMockedHTTPClient(
|
||||
mock.WithRequestMatchHandler(
|
||||
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodPatch},
|
||||
mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}),
|
||||
),
|
||||
),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(333),
|
||||
"item_id": float64(4444),
|
||||
"fields": []any{
|
||||
map[string]any{"id": float64(789), "value": "Done"},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectedErrMsg: "failed to update a project item",
|
||||
},
|
||||
{
|
||||
name: "missing owner",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(1),
|
||||
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing owner_type",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(1),
|
||||
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing project_number",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"item_id": float64(1),
|
||||
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing item_id",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing fields",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(1),
|
||||
},
|
||||
expectError: true,
|
||||
expectedErrMsg: "missing required parameter: fields",
|
||||
},
|
||||
{
|
||||
name: "empty fields",
|
||||
mockedClient: mock.NewMockedHTTPClient(),
|
||||
requestArgs: map[string]any{
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"item_id": float64(1),
|
||||
"fields": []any{},
|
||||
},
|
||||
expectError: true,
|
||||
expectedErrMsg: "fields must contain at least one field update",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := gh.NewClient(tc.mockedClient)
|
||||
_, handler := UpdateProjectItem(stubGetClientFn(client), translations.NullTranslationHelper)
|
||||
request := createMCPRequest(tc.requestArgs)
|
||||
result, err := handler(context.Background(), request)
|
||||
|
||||
require.NoError(t, err)
|
||||
if tc.expectError {
|
||||
require.True(t, result.IsError)
|
||||
text := getTextResult(t, result).Text
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Contains(t, text, tc.expectedErrMsg)
|
||||
}
|
||||
switch tc.name {
|
||||
case "missing owner":
|
||||
assert.Contains(t, text, "missing required parameter: owner")
|
||||
case "missing owner_type":
|
||||
assert.Contains(t, text, "missing required parameter: owner_type")
|
||||
case "missing project_number":
|
||||
assert.Contains(t, text, "missing required parameter: project_number")
|
||||
case "missing item_id":
|
||||
assert.Contains(t, text, "missing required parameter: item_id")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.False(t, result.IsError)
|
||||
textContent := getTextResult(t, result)
|
||||
var item map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &item))
|
||||
if tc.expectedID != 0 {
|
||||
assert.Equal(t, float64(tc.expectedID), item["id"])
|
||||
}
|
||||
if tc.expectedCreatorLogin != "" {
|
||||
creator, ok := item["creator"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, tc.expectedCreatorLogin, creator["login"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
|
||||
toolsets.NewServerTool(GetProjectField(getClient, t)),
|
||||
toolsets.NewServerTool(ListProjectItems(getClient, t)),
|
||||
toolsets.NewServerTool(GetProjectItem(getClient, t)),
|
||||
).
|
||||
AddWriteTools(
|
||||
toolsets.NewServerTool(AddProjectItem(getClient, t)),
|
||||
toolsets.NewServerTool(DeleteProjectItem(getClient, t)),
|
||||
toolsets.NewServerTool(UpdateProjectItem(getClient, t)),
|
||||
)
|
||||
|
||||
// Add toolsets to the group
|
||||
|
||||
Reference in New Issue
Block a user