From 05021def6014b587136f10bacd52cd2abd9e9fab Mon Sep 17 00:00:00 2001 From: Ilia Shutov Date: Sat, 3 Jan 2026 13:10:35 +0100 Subject: [PATCH] feat(labels): add label CRUD tools and list_labels --- README.md | 30 +- pkg/github/__toolsnaps__/create_label.snap | 38 +++ pkg/github/__toolsnaps__/delete_label.snap | 30 ++ pkg/github/__toolsnaps__/list_labels.snap | 25 ++ pkg/github/__toolsnaps__/update_label.snap | 41 +++ pkg/github/deprecated_tool_aliases.go | 3 + pkg/github/labels.go | 274 ++++++++++++++++- pkg/github/labels_test.go | 334 ++++++++++++++++++++- pkg/github/tools.go | 3 + 9 files changed, 770 insertions(+), 8 deletions(-) create mode 100644 pkg/github/__toolsnaps__/create_label.snap create mode 100644 pkg/github/__toolsnaps__/delete_label.snap create mode 100644 pkg/github/__toolsnaps__/list_labels.snap create mode 100644 pkg/github/__toolsnaps__/update_label.snap diff --git a/README.md b/README.md index 29795dc8f..3f3b12979 100644 --- a/README.md +++ b/README.md @@ -849,7 +849,31 @@ The following sets of tools are available: - `owner`: Repository owner (username or organization name) (string, required) - `repo`: Repository name (string, required) -- **label_write** - Write operations on repository labels. +- **list_labels** - List labels from a repository + - `owner`: Repository owner (username or organization name) - required for all operations (string, required) + - `repo`: Repository name - required for all operations (string, required) + +- **create_label** - Create a label in a repository. + - `color`: Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). (string, required) + - `description`: Label description text (optional). (string, optional) + - `name`: Label name. (string, required) + - `owner`: Repository owner (username or organization name) (string, required) + - `repo`: Repository name (string, required) + +- **update_label** - Update a label in a repository. + - `color`: Label color as 6-character hex code without '#' prefix (optional). (string, optional) + - `description`: Label description text (optional). (string, optional) + - `name`: Label name to update. (string, required) + - `new_name`: New name for the label (optional). (string, optional) + - `owner`: Repository owner (username or organization name) (string, required) + - `repo`: Repository name (string, required) + +- **delete_label** - Delete a label from a repository. + - `name`: Label name to delete. (string, required) + - `owner`: Repository owner (username or organization name) (string, required) + - `repo`: Repository name (string, required) + +- **label_write** - Perform write operations on repository labels (advanced; supports create/update/delete via `method`). - `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) @@ -858,10 +882,6 @@ The following sets of tools are available: - `owner`: Repository owner (username or organization name) (string, required) - `repo`: Repository name (string, required) -- **list_label** - List labels from a repository - - `owner`: Repository owner (username or organization name) - required for all operations (string, required) - - `repo`: Repository name - required for all operations (string, required) -
diff --git a/pkg/github/__toolsnaps__/create_label.snap b/pkg/github/__toolsnaps__/create_label.snap new file mode 100644 index 000000000..1696bd365 --- /dev/null +++ b/pkg/github/__toolsnaps__/create_label.snap @@ -0,0 +1,38 @@ +{ + "annotations": { + "title": "Create a label in a repository." + }, + "description": "Create a label in a repository.", + "inputSchema": { + "type": "object", + "properties": { + "color": { + "type": "string", + "description": "Label color as 6-character hex code without '#' prefix (e.g., 'f29513')." + }, + "description": { + "type": "string", + "description": "Label description text (optional)." + }, + "name": { + "type": "string", + "description": "Label name." + }, + "owner": { + "type": "string", + "description": "Repository owner (username or organization name)" + }, + "repo": { + "type": "string", + "description": "Repository name" + } + }, + "required": [ + "owner", + "repo", + "name", + "color" + ] + }, + "name": "create_label" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_label.snap b/pkg/github/__toolsnaps__/delete_label.snap new file mode 100644 index 000000000..5906f4a12 --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_label.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "destructiveHint": true, + "title": "Delete a label from a repository." + }, + "description": "Delete a label from a repository.", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Label name to delete." + }, + "owner": { + "type": "string", + "description": "Repository owner (username or organization name)" + }, + "repo": { + "type": "string", + "description": "Repository name" + } + }, + "required": [ + "owner", + "repo", + "name" + ] + }, + "name": "delete_label" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_labels.snap b/pkg/github/__toolsnaps__/list_labels.snap new file mode 100644 index 000000000..34f361e20 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_labels.snap @@ -0,0 +1,25 @@ +{ + "annotations": { + "readOnlyHint": true, + "title": "List labels from a repository." + }, + "description": "List labels from a repository", + "inputSchema": { + "type": "object", + "properties": { + "owner": { + "type": "string", + "description": "Repository owner (username or organization name) - required for all operations" + }, + "repo": { + "type": "string", + "description": "Repository name - required for all operations" + } + }, + "required": [ + "owner", + "repo" + ] + }, + "name": "list_labels" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_label.snap b/pkg/github/__toolsnaps__/update_label.snap new file mode 100644 index 000000000..d374b2d40 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_label.snap @@ -0,0 +1,41 @@ +{ + "annotations": { + "title": "Update a label in a repository." + }, + "description": "Update a label in a repository.", + "inputSchema": { + "type": "object", + "properties": { + "color": { + "type": "string", + "description": "Label color as 6-character hex code without '#' prefix (optional)." + }, + "description": { + "type": "string", + "description": "Label description text (optional)." + }, + "name": { + "type": "string", + "description": "Label name to update." + }, + "new_name": { + "type": "string", + "description": "New name for the label (optional)." + }, + "owner": { + "type": "string", + "description": "Repository owner (username or organization name)" + }, + "repo": { + "type": "string", + "description": "Repository name" + } + }, + "required": [ + "owner", + "repo", + "name" + ] + }, + "name": "update_label" +} \ No newline at end of file diff --git a/pkg/github/deprecated_tool_aliases.go b/pkg/github/deprecated_tool_aliases.go index 63394770e..70f51336f 100644 --- a/pkg/github/deprecated_tool_aliases.go +++ b/pkg/github/deprecated_tool_aliases.go @@ -28,4 +28,7 @@ var DeprecatedToolAliases = map[string]string{ "rerun_failed_jobs": "actions_run_trigger", "cancel_workflow_run": "actions_run_trigger", "delete_workflow_run_logs": "actions_run_trigger", + + // Labels tools naming consistency + "list_label": "list_labels", } diff --git a/pkg/github/labels.go b/pkg/github/labels.go index 2811cf66e..2a02d6d25 100644 --- a/pkg/github/labels.go +++ b/pkg/github/labels.go @@ -121,7 +121,7 @@ func ListLabels(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( ToolsetLabels, mcp.Tool{ - Name: "list_label", + Name: "list_labels", Description: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository"), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository."), @@ -206,6 +206,278 @@ func ListLabels(t translations.TranslationHelperFunc) inventory.ServerTool { ) } +// CreateLabel creates a new label in a repository +func CreateLabel(t translations.TranslationHelperFunc) inventory.ServerTool { + return NewTool( + ToolsetLabels, + mcp.Tool{ + Name: "create_label", + Description: t("TOOL_CREATE_LABEL_DESCRIPTION", "Create a label in a repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_CREATE_LABEL_TITLE", "Create a label in a repository."), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization name)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "name": { + Type: "string", + Description: "Label name.", + }, + "color": { + Type: "string", + Description: "Label color as 6-character hex code without '#' prefix (e.g., 'f29513').", + }, + "description": { + Type: "string", + Description: "Label description text (optional).", + }, + }, + Required: []string{"owner", "repo", "name", "color"}, + }, + }, + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + name, err := RequiredParam[string](args, "name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + color, err := RequiredParam[string](args, "color") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + description, _ := OptionalParam[string](args, "description") + + client, err := deps.GetGQLClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + repoID, err := getRepositoryID(ctx, client, owner, repo) + if err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil, nil + } + + input := githubv4.CreateLabelInput{ + RepositoryID: repoID, + Name: githubv4.String(name), + Color: githubv4.String(color), + } + if description != "" { + d := githubv4.String(description) + input.Description = &d + } + + var mutation struct { + CreateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"createLabel(input: $input)"` + } + + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), nil, nil + } + + return utils.NewToolResultText(fmt.Sprintf("label '%s' created successfully", mutation.CreateLabel.Label.Name)), nil, nil + }, + ) +} + +// UpdateLabel updates an existing label in a repository +func UpdateLabel(t translations.TranslationHelperFunc) inventory.ServerTool { + return NewTool( + ToolsetLabels, + mcp.Tool{ + Name: "update_label", + Description: t("TOOL_UPDATE_LABEL_DESCRIPTION", "Update a label in a repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_UPDATE_LABEL_TITLE", "Update a label in a repository."), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization name)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "name": { + Type: "string", + Description: "Label name to update.", + }, + "new_name": { + Type: "string", + Description: "New name for the label (optional).", + }, + "color": { + Type: "string", + Description: "Label color as 6-character hex code without '#' prefix (optional).", + }, + "description": { + Type: "string", + Description: "Label description text (optional).", + }, + }, + Required: []string{"owner", "repo", "name"}, + }, + }, + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + name, err := RequiredParam[string](args, "name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + newName, _ := OptionalParam[string](args, "new_name") + color, _ := OptionalParam[string](args, "color") + description, _ := OptionalParam[string](args, "description") + + if newName == "" && color == "" && description == "" { + return utils.NewToolResultError("at least one of new_name, color, or description must be provided for update"), nil, nil + } + + client, err := deps.GetGQLClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + labelID, err := getLabelID(ctx, client, owner, repo, name) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + input := githubv4.UpdateLabelInput{ID: labelID} + if newName != "" { + n := githubv4.String(newName) + input.Name = &n + } + if color != "" { + c := githubv4.String(color) + input.Color = &c + } + if description != "" { + d := githubv4.String(description) + input.Description = &d + } + + var mutation struct { + UpdateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"updateLabel(input: $input)"` + } + + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to update label", err), nil, nil + } + + return utils.NewToolResultText(fmt.Sprintf("label '%s' updated successfully", mutation.UpdateLabel.Label.Name)), nil, nil + }, + ) +} + +// DeleteLabel deletes a label from a repository +func DeleteLabel(t translations.TranslationHelperFunc) inventory.ServerTool { + return NewTool( + ToolsetLabels, + mcp.Tool{ + Name: "delete_label", + Description: t("TOOL_DELETE_LABEL_DESCRIPTION", "Delete a label from a repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_DELETE_LABEL_TITLE", "Delete a label from a repository."), + ReadOnlyHint: false, + DestructiveHint: ToBoolPtr(true), + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization name)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "name": { + Type: "string", + Description: "Label name to delete.", + }, + }, + Required: []string{"owner", "repo", "name"}, + }, + }, + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + name, err := RequiredParam[string](args, "name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := deps.GetGQLClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + labelID, err := getLabelID(ctx, client, owner, repo, name) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + input := githubv4.DeleteLabelInput{ID: labelID} + var mutation struct { + DeleteLabel struct { + ClientMutationID githubv4.String + } `graphql:"deleteLabel(input: $input)"` + } + + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil, nil + } + + return utils.NewToolResultText(fmt.Sprintf("label '%s' deleted successfully", name)), nil, nil + }, + ) +} + // LabelWrite handles create, update, and delete operations for GitHub labels func LabelWrite(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( diff --git a/pkg/github/labels_test.go b/pkg/github/labels_test.go index 88102ba3c..e4d57b562 100644 --- a/pkg/github/labels_test.go +++ b/pkg/github/labels_test.go @@ -146,9 +146,9 @@ func TestListLabels(t *testing.T) { tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) - assert.Equal(t, "list_label", tool.Name) + assert.Equal(t, "list_labels", tool.Name) assert.NotEmpty(t, tool.Description) - assert.True(t, tool.Annotations.ReadOnlyHint, "list_label tool should be read-only") + assert.True(t, tool.Annotations.ReadOnlyHint, "list_labels tool should be read-only") tests := []struct { name string @@ -236,6 +236,336 @@ func TestListLabels(t *testing.T) { } } +func TestCreateLabel(t *testing.T) { + t.Parallel() + + // Verify tool definition + serverTool := CreateLabel(translations.NullTranslationHelper) + tool := serverTool.Tool + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "create_label", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.False(t, tool.Annotations.ReadOnlyHint, "create_label tool should not be read-only") + + tests := []struct { + name string + requestArgs map[string]any + mockedClient *http.Client + expectToolError bool + expectedToolErrMsg string + }{ + { + name: "successful label creation", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "name": "new-label", + "color": "f29513", + "description": "A new test label", + }, + mockedClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewQueryMatcher( + struct { + Repository struct { + ID githubv4.ID + } `graphql:"repository(owner: $owner, name: $repo)"` + }{}, + map[string]any{ + "owner": githubv4.String("owner"), + "repo": githubv4.String("repo"), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "id": githubv4.ID("test-repo-id"), + }, + }), + ), + githubv4mock.NewMutationMatcher( + struct { + CreateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"createLabel(input: $input)"` + }{}, + githubv4.CreateLabelInput{ + RepositoryID: githubv4.ID("test-repo-id"), + Name: githubv4.String("new-label"), + Color: githubv4.String("f29513"), + Description: func() *githubv4.String { s := githubv4.String("A new test label"); return &s }(), + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "createLabel": map[string]any{ + "label": map[string]any{ + "id": githubv4.ID("new-label-id"), + "name": githubv4.String("new-label"), + }, + }, + }), + ), + ), + expectToolError: false, + }, + { + name: "create label without color", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "name": "new-label", + }, + mockedClient: githubv4mock.NewMockedHTTPClient(), + expectToolError: true, + expectedToolErrMsg: "color", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := githubv4.NewClient(tc.mockedClient) + deps := BaseDeps{ + GQLClient: client, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(tc.requestArgs) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + + require.NoError(t, err) + assert.NotNil(t, result) + + if tc.expectToolError { + assert.True(t, result.IsError) + if tc.expectedToolErrMsg != "" { + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) + } + } else { + assert.False(t, result.IsError) + } + }) + } +} + +func TestUpdateLabel(t *testing.T) { + t.Parallel() + + // Verify tool definition + serverTool := UpdateLabel(translations.NullTranslationHelper) + tool := serverTool.Tool + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "update_label", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.False(t, tool.Annotations.ReadOnlyHint, "update_label tool should not be read-only") + + tests := []struct { + name string + requestArgs map[string]any + mockedClient *http.Client + expectToolError bool + expectedToolErrMsg string + }{ + { + name: "successful label update", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "name": "bug", + "new_name": "defect", + "color": "ff0000", + }, + mockedClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewQueryMatcher( + struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + }{}, + map[string]any{ + "owner": githubv4.String("owner"), + "repo": githubv4.String("repo"), + "name": githubv4.String("bug"), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "label": map[string]any{ + "id": githubv4.ID("bug-label-id"), + "name": githubv4.String("bug"), + }, + }, + }), + ), + githubv4mock.NewMutationMatcher( + struct { + UpdateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"updateLabel(input: $input)"` + }{}, + githubv4.UpdateLabelInput{ + ID: githubv4.ID("bug-label-id"), + Name: func() *githubv4.String { s := githubv4.String("defect"); return &s }(), + Color: func() *githubv4.String { s := githubv4.String("ff0000"); return &s }(), + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "updateLabel": map[string]any{ + "label": map[string]any{ + "id": githubv4.ID("bug-label-id"), + "name": githubv4.String("defect"), + }, + }, + }), + ), + ), + expectToolError: false, + }, + { + name: "update label without any changes", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "name": "bug", + }, + mockedClient: githubv4mock.NewMockedHTTPClient(), + expectToolError: true, + expectedToolErrMsg: "at least one of new_name, color, or description must be provided for update", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := githubv4.NewClient(tc.mockedClient) + deps := BaseDeps{ + GQLClient: client, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(tc.requestArgs) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + + require.NoError(t, err) + assert.NotNil(t, result) + + if tc.expectToolError { + assert.True(t, result.IsError) + if tc.expectedToolErrMsg != "" { + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) + } + } else { + assert.False(t, result.IsError) + } + }) + } +} + +func TestDeleteLabel(t *testing.T) { + t.Parallel() + + // Verify tool definition + serverTool := DeleteLabel(translations.NullTranslationHelper) + tool := serverTool.Tool + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "delete_label", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.False(t, tool.Annotations.ReadOnlyHint, "delete_label tool should not be read-only") + + tests := []struct { + name string + requestArgs map[string]any + mockedClient *http.Client + expectToolError bool + expectedToolErrMsg string + }{ + { + name: "successful label deletion", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "name": "bug", + }, + mockedClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewQueryMatcher( + struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + }{}, + map[string]any{ + "owner": githubv4.String("owner"), + "repo": githubv4.String("repo"), + "name": githubv4.String("bug"), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "label": map[string]any{ + "id": githubv4.ID("bug-label-id"), + "name": githubv4.String("bug"), + }, + }, + }), + ), + githubv4mock.NewMutationMatcher( + struct { + DeleteLabel struct { + ClientMutationID githubv4.String + } `graphql:"deleteLabel(input: $input)"` + }{}, + githubv4.DeleteLabelInput{ + ID: githubv4.ID("bug-label-id"), + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "deleteLabel": map[string]any{ + "clientMutationId": githubv4.String("test-mutation-id"), + }, + }), + ), + ), + expectToolError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := githubv4.NewClient(tc.mockedClient) + deps := BaseDeps{ + GQLClient: client, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(tc.requestArgs) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + + require.NoError(t, err) + assert.NotNil(t, result) + + if tc.expectToolError { + assert.True(t, result.IsError) + if tc.expectedToolErrMsg != "" { + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) + } + } else { + assert.False(t, result.IsError) + } + }) + } +} + func TestWriteLabel(t *testing.T) { t.Parallel() diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 6600ec2bd..d08450658 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -288,6 +288,9 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool { GetLabel(t), GetLabelForLabelsToolset(t), ListLabels(t), + CreateLabel(t), + UpdateLabel(t), + DeleteLabel(t), LabelWrite(t), } }