diff --git a/README.md b/README.md
index d4806fb84e..13b0586e4f 100644
--- a/README.md
+++ b/README.md
@@ -668,15 +668,6 @@ The following sets of tools are available (all are on by default):
- `title`: New title (string, optional)
- `type`: New issue type (string, optional)
-- **write_label** - Write operations on repository labels.
- - `color`: Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'. (string, optional)
- - `description`: Label description text. Optional for 'create' and 'update'. (string, optional)
- - `method`: Operation to perform: 'create', 'update', or 'delete' (string, required)
- - `name`: Label name - required for all operations (string, required)
- - `new_name`: New name for the label (used only with 'update' method to rename) (string, optional)
- - `owner`: Repository owner (username or organization name) (string, required)
- - `repo`: Repository name (string, required)
-
@@ -688,12 +679,7 @@ The following sets of tools are available (all are on by default):
- `owner`: Repository owner (username or organization name) (string, required)
- `repo`: Repository name (string, required)
-- **list_label** - List labels from a repository or an issue
- - `issue_number`: Issue number - if provided, lists labels on the specific issue (number, optional)
- - `owner`: Repository owner (username or organization name) - required for all operations (string, required)
- - `repo`: Repository name - required for all operations (string, required)
-
-- **write_label** - Write operations on repository labels.
+- **label_write** - Write operations on repository labels.
- `color`: Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'. (string, optional)
- `description`: Label description text. Optional for 'create' and 'update'. (string, optional)
- `method`: Operation to perform: 'create', 'update', or 'delete' (string, required)
@@ -702,6 +688,11 @@ The following sets of tools are available (all are on by default):
- `owner`: Repository owner (username or organization name) (string, required)
- `repo`: Repository name (string, required)
+- **list_label** - List labels from a repository or an issue
+ - `issue_number`: Issue number - if provided, lists labels on the specific issue (number, optional)
+ - `owner`: Repository owner (username or organization name) - required for all operations (string, required)
+ - `repo`: Repository name - required for all operations (string, required)
+
@@ -806,6 +797,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
+ - `item_id`: The unique identifier of the project item. This is 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)
+ - `updated_field`: Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set "value" to null. Example: {"id": 123456, "value": "New Value"} (object, required)
+
diff --git a/pkg/github/__toolsnaps__/write_label.snap b/pkg/github/__toolsnaps__/label_write.snap
similarity index 98%
rename from pkg/github/__toolsnaps__/write_label.snap
rename to pkg/github/__toolsnaps__/label_write.snap
index cb13f8bbb8..12d0bd4416 100644
--- a/pkg/github/__toolsnaps__/write_label.snap
+++ b/pkg/github/__toolsnaps__/label_write.snap
@@ -48,5 +48,5 @@
],
"type": "object"
},
- "name": "write_label"
+ "name": "label_write"
}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/update_project_item.snap b/pkg/github/__toolsnaps__/update_project_item.snap
index ff29052827..96a8e749a9 100644
--- a/pkg/github/__toolsnaps__/update_project_item.snap
+++ b/pkg/github/__toolsnaps__/update_project_item.snap
@@ -6,12 +6,8 @@
"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).",
+ "description": "The unique identifier of the project item. This is not the issue or pull request ID.",
"type": "number"
},
"owner": {
@@ -29,6 +25,11 @@
"project_number": {
"description": "The project's number.",
"type": "number"
+ },
+ "updated_field": {
+ "description": "Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set \"value\" to null. Example: {\"id\": 123456, \"value\": \"New Value\"}",
+ "properties": {},
+ "type": "object"
}
},
"required": [
@@ -36,7 +37,7 @@
"owner",
"project_number",
"item_id",
- "fields"
+ "updated_field"
],
"type": "object"
},
diff --git a/pkg/github/labels.go b/pkg/github/labels.go
index 91eae42745..f0cc0e6309 100644
--- a/pkg/github/labels.go
+++ b/pkg/github/labels.go
@@ -239,13 +239,13 @@ func ListLabels(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
}
}
-// WriteLabel handles create, update, and delete operations for GitHub labels
-func WriteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
+// LabelWrite handles create, update, and delete operations for GitHub labels
+func LabelWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
return mcp.NewTool(
- "write_label",
- mcp.WithDescription(t("TOOL_WRITE_LABEL_DESCRIPTION", "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool.")),
+ "label_write",
+ mcp.WithDescription(t("TOOL_LABEL_WRITE_DESCRIPTION", "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
- Title: t("TOOL_WRITE_LABEL_TITLE", "Write operations on repository labels."),
+ Title: t("TOOL_LABEL_WRITE_TITLE", "Write operations on repository labels."),
ReadOnlyHint: ToBoolPtr(false),
}),
mcp.WithString("method",
diff --git a/pkg/github/labels_test.go b/pkg/github/labels_test.go
index 8e1d7688ed..96b9f7f856 100644
--- a/pkg/github/labels_test.go
+++ b/pkg/github/labels_test.go
@@ -291,10 +291,10 @@ func TestWriteLabel(t *testing.T) {
// Verify tool definition
mockClient := githubv4.NewClient(nil)
- tool, _ := WriteLabel(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper)
+ tool, _ := LabelWrite(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
- assert.Equal(t, "write_label", tool.Name)
+ assert.Equal(t, "label_write", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "method")
assert.Contains(t, tool.InputSchema.Properties, "owner")
@@ -520,7 +520,7 @@ func TestWriteLabel(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := githubv4.NewClient(tc.mockedClient)
- _, handler := WriteLabel(stubGetGQLClientFn(client), translations.NullTranslationHelper)
+ _, handler := LabelWrite(stubGetGQLClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go
index 59cab6b432..766f630bb6 100644
--- a/pkg/github/minimal_types.go
+++ b/pkg/github/minimal_types.go
@@ -134,6 +134,8 @@ 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"`
@@ -192,6 +194,8 @@ func convertToMinimalProjectItem(item *projectV2Item) *MinimalProjectItem {
return &MinimalProjectItem{
ID: item.ID,
NodeID: item.NodeID,
+ Title: item.Title,
+ Description: item.Description,
ProjectNodeID: item.ProjectNodeID,
ContentNodeID: item.ContentNodeID,
ProjectURL: item.ProjectURL,
diff --git a/pkg/github/projects.go b/pkg/github/projects.go
index 95b859c039..f7bc94677a 100644
--- a/pkg/github/projects.go
+++ b/pkg/github/projects.go
@@ -18,14 +18,33 @@ import (
"github.com/mark3labs/mcp-go/server"
)
+const (
+ ProjectUpdateFailedError = "failed to update a project item"
+ ProjectAddFailedError = "failed to add a project item"
+ ProjectDeleteFailedError = "failed to delete a project item"
+ ProjectListFailedError = "failed to list project items"
+)
+
func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_projects",
mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", "List Projects for a user or org")),
- mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), ReadOnlyHint: ToBoolPtr(true)}),
- 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.WithString("query", mcp.Description("Filter projects by a search query (matches title and description)")),
- mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ 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.WithString("query",
+ mcp.Description("Filter projects by a search query (matches title and description)"),
+ ),
+ mcp.WithNumber("per_page",
+ mcp.Description("Number of results per page (max 100, default: 30)"),
+ ),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](req, "owner")
if err != nil {
@@ -105,10 +124,23 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (
func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_project",
mcp.WithDescription(t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for a user or org")),
- mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_GET_PROJECT_USER_TITLE", "Get project"), ReadOnlyHint: ToBoolPtr(true)}),
- mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number")),
- 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.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_GET_PROJECT_USER_TITLE", "Get project"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithNumber("project_number",
+ mcp.Required(),
+ mcp.Description("The project's number"),
+ ),
+ 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."),
+ ),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
projectNumber, err := RequiredInt(req, "project_number")
@@ -176,11 +208,25 @@ func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (to
func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_project_fields",
mcp.WithDescription(t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for a user or org")),
- mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECT_FIELDS_USER_TITLE", "List project fields"), ReadOnlyHint: ToBoolPtr(true)}),
- 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("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_PROJECT_FIELDS_USER_TITLE", "List project fields"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ 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("per_page",
+ mcp.Description("Number of results per page (max 100, default: 30)"),
+ ),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](req, "owner")
if err != nil {
@@ -251,11 +297,24 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_project_field",
mcp.WithDescription(t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org")),
- mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_GET_PROJECT_FIELD_USER_TITLE", "Get project field"), ReadOnlyHint: ToBoolPtr(true)}),
- 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("field_id", mcp.Required(), mcp.Description("The field's id.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_GET_PROJECT_FIELD_USER_TITLE", "Get project field"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ 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("field_id",
+ mcp.Required(),
+ mcp.Description("The field's id."),
+ ),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](req, "owner")
if err != nil {
@@ -321,12 +380,28 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc
func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_project_items",
mcp.WithDescription(t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", "List Project items for a user or org")),
- mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"), ReadOnlyHint: ToBoolPtr(true)}),
- 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("query", mcp.Description("Search query to filter items")),
- mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ 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("query",
+ mcp.Description("Search query to filter items"),
+ ),
+ mcp.WithNumber("per_page",
+ mcp.Description("Number of results per page (max 100, default: 30)"),
+ ),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](req, "owner")
if err != nil {
@@ -378,7 +453,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
resp, err := client.Do(ctx, httpRequest, &projectItems)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
- "failed to list project items",
+ ProjectListFailedError,
resp,
err,
), nil
@@ -390,7 +465,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
- return mcp.NewToolResultError(fmt.Sprintf("failed to list project items: %s", string(body))), nil
+ return mcp.NewToolResultError(fmt.Sprintf("%s: %s", ProjectListFailedError, string(body))), nil
}
minimalProjectItems := []MinimalProjectItem{}
for _, item := range projectItems {
@@ -408,11 +483,27 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun
func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_project_item",
mcp.WithDescription(t("TOOL_GET_PROJECT_ITEM_DESCRIPTION", "Get a specific Project item for a user or org")),
- mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_GET_PROJECT_ITEM_USER_TITLE", "Get project item"), ReadOnlyHint: ToBoolPtr(true)}),
- 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 item's ID.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_GET_PROJECT_ITEM_USER_TITLE", "Get project item"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ 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 item's ID."),
+ ),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](req, "owner")
if err != nil {
@@ -478,12 +569,31 @@ 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.")),
+ 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 {
@@ -522,11 +632,11 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
}
- newProjectItem := &newProjectItem{
+ newItem := &newProjectItem{
ID: int64(itemID),
Type: toNewProjectType(itemType),
}
- httpRequest, err := client.NewRequest("POST", projectsURL, newProjectItem)
+ httpRequest, err := client.NewRequest("POST", projectsURL, newItem)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
@@ -535,19 +645,19 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
resp, err := client.Do(ctx, httpRequest, &addedItem)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
- "failed to add a project item",
+ ProjectAddFailedError,
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
+ if resp.StatusCode != http.StatusCreated {
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
+ return mcp.NewToolResultError(fmt.Sprintf("%s: %s", ProjectAddFailedError, string(body))), nil
}
r, err := json.Marshal(convertToMinimalProjectItem(&addedItem))
if err != nil {
@@ -558,14 +668,135 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc)
}
}
+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 unique identifier of the project item. This is not the issue or pull request ID."),
+ ),
+ mcp.WithObject("updated_field",
+ mcp.Required(),
+ mcp.Description("Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set \"value\" to null. Example: {\"id\": 123456, \"value\": \"New Value\"}"),
+ ),
+ ), 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
+ }
+
+ rawUpdatedField, exists := req.GetArguments()["updated_field"]
+ if !exists {
+ return mcp.NewToolResultError("missing required parameter: updated_field"), nil
+ }
+
+ fieldValue, ok := rawUpdatedField.(map[string]any)
+ if !ok || fieldValue == nil {
+ return mcp.NewToolResultError("field_value must be an object"), nil
+ }
+
+ updatePayload, err := buildUpdateProjectItem(fieldValue)
+ 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("PATCH", projectsURL, updateProjectItemPayload{
+ Fields: []updateProjectItem{*updatePayload},
+ })
+ 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,
+ ProjectUpdateFailedError,
+ 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("%s: %s", ProjectUpdateFailedError, 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
+ }
+}
+
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).")),
+ 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 {
@@ -603,7 +834,7 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
resp, err := client.Do(ctx, httpRequest, nil)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
- "failed to delete a project item",
+ ProjectDeleteFailedError,
resp,
err,
), nil
@@ -615,22 +846,31 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
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.NewToolResultError(fmt.Sprintf("%s: %s", ProjectDeleteFailedError, string(body))), nil
}
return mcp.NewToolResultText("project item successfully deleted"), nil
}
}
type newProjectItem struct {
- ID int64 `json:"id,omitempty"` // Issue or Pull Request ID to add to the project.
+ ID int64 `json:"id,omitempty"`
Type string `json:"type,omitempty"`
}
+type updateProjectItemPayload struct {
+ Fields []updateProjectItem `json:"fields"`
+}
+
+type updateProjectItem struct {
+ ID int `json:"id"`
+ Value any `json:"value"`
+}
+
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.
Name string `json:"name,omitempty"` // The display name of the field.
- DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select").
+ DataType string `json:"data_type,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select").
URL string `json:"url,omitempty"` // The API URL for this field.
Options []*any `json:"options,omitempty"` // Available options for single_select and multi_select fields.
CreatedAt *github.Timestamp `json:"created_at,omitempty"` // The time when this field was created.
@@ -639,6 +879,8 @@ type projectV2Field struct {
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"`
@@ -671,6 +913,30 @@ type listProjectsOptions struct {
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")
+ }
+
+ idField, ok := input["id"]
+ if !ok {
+ return nil, fmt.Errorf("updated_field.id is required")
+ }
+
+ idFieldAsFloat64, ok := idField.(float64) // JSON numbers are float64
+ if !ok {
+ return nil, fmt.Errorf("updated_field.id must be a number")
+ }
+
+ valueField, ok := input["value"]
+ if !ok {
+ return nil, fmt.Errorf("updated_field.value is required")
+ }
+ payload := &updateProjectItem{ID: int(idFieldAsFloat64), Value: valueField}
+
+ return payload, nil
+}
+
// addOptions adds the parameters in opts as URL query parameters to s. opts
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opts any) (string, error) {
diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go
index 19f23510bf..52adb73e65 100644
--- a/pkg/github/projects_test.go
+++ b/pkg/github/projects_test.go
@@ -697,7 +697,7 @@ func Test_ListProjectItems(t *testing.T) {
"project_number": float64(789),
},
expectError: true,
- expectedErrMsg: "failed to list project items",
+ expectedErrMsg: ProjectListFailedError,
},
{
name: "missing owner",
@@ -1022,7 +1022,7 @@ func Test_AddProjectItem(t *testing.T) {
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "PullRequest", payload.Type)
assert.Equal(t, 7654, payload.ID)
- w.WriteHeader(http.StatusOK)
+ w.WriteHeader(http.StatusCreated)
_, _ = w.Write(mock.MustMarshal(userItem))
}),
),
@@ -1054,7 +1054,7 @@ func Test_AddProjectItem(t *testing.T) {
"item_id": float64(8888),
},
expectError: true,
- expectedErrMsg: "failed to add a project item",
+ expectedErrMsg: ProjectAddFailedError,
},
{
name: "missing owner",
@@ -1139,6 +1139,8 @@ func Test_AddProjectItem(t *testing.T) {
assert.Contains(t, text, "missing required parameter: item_type")
case "missing item_id":
assert.Contains(t, text, "missing required parameter: item_id")
+ // case "api error":
+ // assert.Contains(t, text, ProjectAddFailedError)
}
return
}
@@ -1162,6 +1164,282 @@ func Test_AddProjectItem(t *testing.T) {
}
}
+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, "updated_field")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id", "updated_field"})
+
+ orgUpdatedItem := map[string]any{
+ "id": 801,
+ "content_type": "Issue",
+ }
+ userUpdatedItem := map[string]any{
+ "id": 802,
+ "content_type": "PullRequest",
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]any
+ expectError bool
+ expectedErrMsg string
+ expectedID int
+ }{
+ {
+ 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))
+ require.Len(t, payload.Fields, 1)
+ assert.Equal(t, 101, payload.Fields[0].ID)
+ assert.Equal(t, "Done", payload.Fields[0].Value)
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(mock.MustMarshal(orgUpdatedItem))
+ }),
+ ),
+ ),
+ requestArgs: map[string]any{
+ "owner": "octo-org",
+ "owner_type": "org",
+ "project_number": float64(1001),
+ "item_id": float64(5555),
+ "updated_field": map[string]any{
+ "id": float64(101),
+ "value": "Done",
+ },
+ },
+ expectedID: 801,
+ },
+ {
+ 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 struct {
+ Fields []struct {
+ ID int `json:"id"`
+ Value interface{} `json:"value"`
+ } `json:"fields"`
+ }
+ assert.NoError(t, json.Unmarshal(body, &payload))
+ require.Len(t, payload.Fields, 1)
+ assert.Equal(t, 202, payload.Fields[0].ID)
+ assert.Equal(t, 42.0, payload.Fields[0].Value)
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(mock.MustMarshal(userUpdatedItem))
+ }),
+ ),
+ ),
+ requestArgs: map[string]any{
+ "owner": "octocat",
+ "owner_type": "user",
+ "project_number": float64(2002),
+ "item_id": float64(6666),
+ "updated_field": map[string]any{
+ "id": float64(202),
+ "value": float64(42),
+ },
+ },
+ expectedID: 802,
+ },
+ {
+ 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(3003),
+ "item_id": float64(7777),
+ "updated_field": map[string]any{
+ "id": float64(303),
+ "value": "In Progress",
+ },
+ },
+ 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(2),
+ "field_id": float64(1),
+ "new_field": map[string]any{
+ "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(2),
+ "new_field": 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(2),
+ "new_field": 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),
+ "new_field": map[string]any{
+ "id": float64(1),
+ "value": "X",
+ },
+ },
+ expectError: true,
+ },
+ {
+ name: "missing field_value",
+ mockedClient: mock.NewMockedHTTPClient(),
+ requestArgs: map[string]any{
+ "owner": "octo-org",
+ "owner_type": "org",
+ "project_number": float64(1),
+ "item_id": float64(2),
+ "field_id": float64(2),
+ },
+ expectError: true,
+ },
+ {
+ name: "new_field not object",
+ mockedClient: mock.NewMockedHTTPClient(),
+ requestArgs: map[string]any{
+ "owner": "octo-org",
+ "owner_type": "org",
+ "project_number": float64(1),
+ "item_id": float64(2),
+ "updated_field": "not-an-object",
+ },
+ expectError: true,
+ },
+ {
+ name: "new_field missing id",
+ mockedClient: mock.NewMockedHTTPClient(),
+ requestArgs: map[string]any{
+ "owner": "octo-org",
+ "owner_type": "org",
+ "project_number": float64(1),
+ "item_id": float64(2),
+ "updated_field": map[string]any{},
+ },
+ expectError: true,
+ },
+ {
+ name: "new_field missing value",
+ mockedClient: mock.NewMockedHTTPClient(),
+ requestArgs: map[string]any{
+ "owner": "octo-org",
+ "owner_type": "org",
+ "project_number": float64(1),
+ "item_id": float64(2),
+ "updated_field": map[string]any{
+ "id": float64(9),
+ },
+ },
+ expectError: true,
+ },
+ }
+
+ 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")
+ case "missing field_value":
+ assert.Contains(t, text, "missing required parameter: updated_field")
+ case "field_value not object":
+ assert.Contains(t, text, "field_value must be an object")
+ case "field_value missing id":
+ assert.Contains(t, text, "missing required parameter: field_id")
+ case "field_value missing value":
+ assert.Contains(t, text, "field_value.value is required")
+ }
+ 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"])
+ }
+ })
+ }
+}
+
func Test_DeleteProjectItem(t *testing.T) {
mockClient := gh.NewClient(nil)
tool, _ := DeleteProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper)
@@ -1234,7 +1512,7 @@ func Test_DeleteProjectItem(t *testing.T) {
"item_id": float64(999),
},
expectError: true,
- expectedErrMsg: "failed to delete a project item",
+ expectedErrMsg: ProjectDeleteFailedError,
},
{
name: "missing owner",
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index cd0bb28e2d..495135ae44 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -188,7 +188,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(AddSubIssue(getClient, t)),
toolsets.NewServerTool(RemoveSubIssue(getClient, t)),
toolsets.NewServerTool(ReprioritizeSubIssue(getClient, t)),
- toolsets.NewServerTool(WriteLabel(getGQLClient, t)),
).AddPrompts(
toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)),
toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)),
@@ -316,6 +315,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
AddWriteTools(
toolsets.NewServerTool(AddProjectItem(getClient, t)),
toolsets.NewServerTool(DeleteProjectItem(getClient, t)),
+ toolsets.NewServerTool(UpdateProjectItem(getClient, t)),
)
stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description).
AddReadTools(
@@ -334,7 +334,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
).
AddWriteTools(
// create or update
- toolsets.NewServerTool(WriteLabel(getGQLClient, t)),
+ toolsets.NewServerTool(LabelWrite(getGQLClient, t)),
)
// Add toolsets to the group
tsg.AddToolset(contextTools)